@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,442 @@
1
+ /**
2
+ * The box checks, WSP121 and WSP122 (#2726).
3
+ *
4
+ * A member whose entry has a `box` block is a box, or holds a box's
5
+ * declarations. A box holds no credential: each capability it needs outside
6
+ * itself (inference, Fountain, a third-party API) is reached through a
7
+ * broker, a runtime such as a lobby or a door, which holds the credential
8
+ * and enforces the scope the block declares. chant only states and checks
9
+ * this; it never runs a broker (ws-052).
10
+ *
11
+ * | Id | Code | Fails when |
12
+ * |---|---|---|
13
+ * | WSP121 | `box-credential-declared` | a file in the box member's directory carries a literal secret (fixed) |
14
+ * | WSP122 | `box-capability-unbrokered` | a capability in the block names no broker |
15
+ *
16
+ * WSP121 reads the member's files from the tree checked, so it works under
17
+ * `--at`, and runs no member code. A literal secret is either a value with a
18
+ * well-known credential shape (an AWS key id, a GitHub or Slack token, an
19
+ * `sk-` key, private key material), wherever it sits, or a literal string
20
+ * where a credential goes: under a key named like one (`API_KEY`, `token`,
21
+ * `credential`, `password`, ...), or the `value` of a `{ key, value }` vault
22
+ * secret. A reference is never one: `${VAR}` and `$VAR`, a secret-manager
23
+ * reference (`op://`, `bws://`, `infisical://`), a `{{...}}` template
24
+ * placeholder, and any expression, such as `process.env.X`, that is not a
25
+ * string literal. WSP121 is fixed: a credential in a box's files is in git.
26
+ *
27
+ * WSP123 and WSP124, box isolation (#2727), are in `./box-isolation.ts` and
28
+ * listed here after these two.
29
+ *
30
+ * WSP125 (`box-fountain-callback-undeclared`, #2780) is the one credential a
31
+ * box gets without asking. fountain v0.21.0 gives every persistent sandbox a
32
+ * callback token scoped to its owner (`FOUNTAIN_TOKEN`, `sandbox_api_access:
33
+ * owner`), and a fountain `Box` is always persistent, so its member's block
34
+ * has to say so: the capability `fountain-callback`, brokered by `fountain`,
35
+ * with scope `owner`. It is named apart from `fountain`, the API access a
36
+ * lobby may broker for the same box. A member builds a Box when one of its TypeScript or JavaScript files
37
+ * imports `Box` from `@intentius/chant-lexicon-fountain` and calls it, read as
38
+ * syntax and never run. Upstream, managoat/fountain#2497 asks for a way to run
39
+ * a persistent agent with no callback token.
40
+ */
41
+
42
+ import * as ts from "typescript";
43
+ import type { WorkspaceCheck, WorkspaceCheckContext, WorkspaceDiagnostic } from "../checks";
44
+ import type { Member } from "../declaration";
45
+ import type { ReasonCode } from "../reason-codes";
46
+ import { joinPath, skippedDir, type WorkspaceTree } from "../tree";
47
+ import { BOX_ISOLATION_CHECKS } from "./box-isolation";
48
+
49
+ /** The read contract's codes for the box findings, carried as `code` on each. */
50
+ export const BOX_FINDING_CODES = [
51
+ "box-credential-declared",
52
+ "box-capability-unbrokered",
53
+ "box-isolation-collision",
54
+ "box-isolation-literal",
55
+ "box-fountain-callback-undeclared",
56
+ ] as const satisfies readonly ReasonCode[];
57
+
58
+ export const WSP_BOX_CREDENTIAL = "WSP121";
59
+ export const WSP_BOX_UNBROKERED = "WSP122";
60
+ export const WSP_BOX_FOUNTAIN_CALLBACK = "WSP125";
61
+
62
+ /** The fountain lexicon's package, whose `Box` composite runs a persistent sandbox. */
63
+ const FOUNTAIN_LEXICON = "@intentius/chant-lexicon-fountain";
64
+
65
+ /**
66
+ * The capability a box block declares for fountain's callback token (#2780).
67
+ * The fountain lexicon exports the same value as `BOX_FOUNTAIN_CALLBACK_CAPABILITY`.
68
+ */
69
+ export const FOUNTAIN_CALLBACK_CAPABILITY = { name: "fountain-callback", broker: "fountain", scope: ["owner"] } as const;
70
+
71
+ /** Where a file calls the fountain lexicon's `Box`, 1-based. */
72
+ export interface FountainBoxCall {
73
+ line: number;
74
+ column: number;
75
+ }
76
+
77
+ /**
78
+ * The first call of the fountain lexicon's `Box` in a TypeScript or
79
+ * JavaScript file, read as syntax and never run: `Box` imported by name
80
+ * (aliased or not) and called, or called through a namespace import. Any
81
+ * other `Box` is not the composite.
82
+ */
83
+ export function fountainBoxCall(text: string, file: string, kind: ts.ScriptKind): FountainBoxCall | undefined {
84
+ if (!text.includes(FOUNTAIN_LEXICON)) return undefined;
85
+ const source = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, kind);
86
+ const names = new Set<string>();
87
+ const namespaces = new Set<string>();
88
+ for (const stmt of source.statements) {
89
+ if (!ts.isImportDeclaration(stmt) || !ts.isStringLiteral(stmt.moduleSpecifier)) continue;
90
+ const from = stmt.moduleSpecifier.text;
91
+ if (from !== FOUNTAIN_LEXICON && !from.startsWith(`${FOUNTAIN_LEXICON}/`)) continue;
92
+ const bindings = stmt.importClause?.namedBindings;
93
+ if (!bindings) continue;
94
+ if (ts.isNamespaceImport(bindings)) namespaces.add(bindings.name.text);
95
+ else for (const el of bindings.elements) if ((el.propertyName ?? el.name).text === "Box") names.add(el.name.text);
96
+ }
97
+ if (names.size === 0 && namespaces.size === 0) return undefined;
98
+ let found: ts.Node | undefined;
99
+ const visit = (node: ts.Node) => {
100
+ if (found) return;
101
+ if (ts.isCallExpression(node)) {
102
+ const callee = node.expression;
103
+ if (
104
+ (ts.isIdentifier(callee) && names.has(callee.text)) ||
105
+ (ts.isPropertyAccessExpression(callee) &&
106
+ callee.name.text === "Box" &&
107
+ ts.isIdentifier(callee.expression) &&
108
+ namespaces.has(callee.expression.text))
109
+ ) {
110
+ found = node;
111
+ return;
112
+ }
113
+ }
114
+ ts.forEachChild(node, visit);
115
+ };
116
+ visit(source);
117
+ if (!found) return undefined;
118
+ const { line, character } = source.getLineAndCharacterOfPosition(found.getStart(source));
119
+ return { line: line + 1, column: character + 1 };
120
+ }
121
+
122
+ /** Values whose shape alone says they are a credential. The list FTN001 uses, plus Anthropic keys. */
123
+ export const CREDENTIAL_SHAPES: readonly { pattern: RegExp; label: string }[] = [
124
+ { pattern: /\bAKIA[0-9A-Z]{16}\b/, label: "an AWS access key id" },
125
+ { pattern: /\b(?:ghp|gho|ghs|ghu)_[A-Za-z0-9]{20,}/, label: "a GitHub token" },
126
+ { pattern: /\bgithub_pat_[A-Za-z0-9_]{20,}/, label: "a GitHub fine-grained token" },
127
+ { pattern: /\bsk-ant-[A-Za-z0-9_-]{20,}/, label: "an Anthropic API key" },
128
+ { pattern: /\bsk-[A-Za-z0-9_-]{20,}/, label: "a secret API key (sk-)" },
129
+ { pattern: /\bftn_[A-Za-z0-9]{16,}/, label: "a Fountain API key" },
130
+ { pattern: /\bxox[baprs]-[A-Za-z0-9-]{10,}/, label: "a Slack token" },
131
+ { pattern: /-----BEGIN [A-Z ]*PRIVATE KEY-----/, label: "private key material" },
132
+ ];
133
+
134
+ /** Secret-manager references a value may be instead of the secret. */
135
+ export const SECRET_REFERENCE_SCHEMES = ["op://", "bws://", "infisical://"] as const;
136
+
137
+ /**
138
+ * A key whose value is a credential: `API_KEY`, `GITHUB_TOKEN`, `apiKey`,
139
+ * `token`, `credential`, `password`, `AWS_SECRET_ACCESS_KEY`. A key that
140
+ * names where a secret is (`secretKey`, `TOKEN_FILE`, `tokenHash`) is not.
141
+ */
142
+ const CREDENTIAL_KEY = /(?:^|[_-])(?:API_?KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIALS?|PRIVATE_?KEY|ACCESS_?KEY)$|^(?:apiKey|token|secret|password|credentials?|privateKey|accessKey|authToken)$|[a-z](?:ApiKey|Token|Secret|Password|Credentials?|PrivateKey|AccessKey)$/i;
143
+
144
+ /** Whether `key` names a credential. Exported for tests. */
145
+ export function isCredentialKey(key: string): boolean {
146
+ return CREDENTIAL_KEY.test(key);
147
+ }
148
+
149
+ /** Whether `value` refers to a secret rather than being one. Exported for tests. */
150
+ export function isSecretReference(value: string): boolean {
151
+ const v = value.trim();
152
+ if (v === "") return true;
153
+ if (v.includes("${")) return true;
154
+ // $VAR, $1, $(command): the shell fills it in.
155
+ if (v.startsWith("$")) return true;
156
+ if (/^\{\{[^}]*\}\}$/.test(v)) return true;
157
+ return SECRET_REFERENCE_SCHEMES.some((s) => v.startsWith(s));
158
+ }
159
+
160
+ /**
161
+ * Whether a value under a credential key says where a secret is rather than
162
+ * being one: a path (`~/box/llm-token`, `/run/secrets/token`), a URL, or an
163
+ * environment variable's name (`GIT_TOKEN`).
164
+ */
165
+ function namesWhereSecretIs(value: string): boolean {
166
+ const v = value.trim();
167
+ return /^(?:\/|~|\.\.?\/)/.test(v) || /^[a-z][a-z0-9+.-]*:\/\//i.test(v) || /^[A-Z][A-Z0-9_]*$/.test(v);
168
+ }
169
+
170
+ /** The credential shape `value` has, or undefined. */
171
+ export function credentialShape(value: string): string | undefined {
172
+ return CREDENTIAL_SHAPES.find((s) => s.pattern.test(value))?.label;
173
+ }
174
+
175
+ /** One literal secret found in a file. */
176
+ export interface LiteralSecret {
177
+ /** 1-based. */
178
+ line: number;
179
+ column: number;
180
+ /** What was found, for the message: never the value itself. */
181
+ what: string;
182
+ }
183
+
184
+ const CODE_EXTENSIONS: Record<string, ts.ScriptKind> = {
185
+ ".ts": ts.ScriptKind.TS,
186
+ ".mts": ts.ScriptKind.TS,
187
+ ".cts": ts.ScriptKind.TS,
188
+ ".tsx": ts.ScriptKind.TSX,
189
+ ".js": ts.ScriptKind.JS,
190
+ ".mjs": ts.ScriptKind.JS,
191
+ ".cjs": ts.ScriptKind.JS,
192
+ ".jsx": ts.ScriptKind.JSX,
193
+ ".json": ts.ScriptKind.JSON,
194
+ ".jsonc": ts.ScriptKind.JSON,
195
+ };
196
+
197
+ const PROSE_EXTENSIONS = new Set([".md", ".mdx", ".txt", ".html", ".rst"]);
198
+
199
+ /** Files larger than this are not read: a box's declarations are small, and a large file is data or a build. */
200
+ const MAX_FILE_BYTES = 1024 * 1024;
201
+
202
+ function propertyName(name: ts.PropertyName | undefined): string | undefined {
203
+ if (!name) return undefined;
204
+ if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNoSubstitutionTemplateLiteral(name)) return name.text;
205
+ return undefined;
206
+ }
207
+
208
+ const isStringLiteral = (n: ts.Node): n is ts.StringLiteral | ts.NoSubstitutionTemplateLiteral => ts.isStringLiteral(n) || ts.isNoSubstitutionTemplateLiteral(n);
209
+
210
+ /**
211
+ * The literal secrets in a TypeScript, JavaScript or JSON file, read as
212
+ * syntax and never run.
213
+ */
214
+ export function literalSecretsInCode(text: string, file: string, kind: ts.ScriptKind): LiteralSecret[] {
215
+ const source = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, kind);
216
+ const out: LiteralSecret[] = [];
217
+ const at = (node: ts.Node, what: string) => {
218
+ const { line, character } = source.getLineAndCharacterOfPosition(node.getStart(source));
219
+ out.push({ line: line + 1, column: character + 1, what });
220
+ };
221
+ const reported = new Set<ts.Node>();
222
+ const visit = (node: ts.Node) => {
223
+ if (isStringLiteral(node) && !reported.has(node)) {
224
+ const shape = credentialShape(node.text);
225
+ if (shape && !isSecretReference(node.text)) {
226
+ reported.add(node);
227
+ at(node, shape);
228
+ }
229
+ }
230
+ if (ts.isPropertyAssignment(node) && isStringLiteral(node.initializer) && !reported.has(node.initializer)) {
231
+ const key = propertyName(node.name);
232
+ const value = node.initializer.text;
233
+ // A value with a credential's shape is reported by that shape, below.
234
+ if (key !== undefined && !isSecretReference(value) && !namesWhereSecretIs(value) && credentialShape(value) === undefined) {
235
+ if (isCredentialKey(key)) {
236
+ reported.add(node.initializer);
237
+ at(node.initializer, `a literal value for ${key}`);
238
+ } else if (key === "value" && ts.isObjectLiteralExpression(node.parent) && isVaultSecret(node.parent)) {
239
+ reported.add(node.initializer);
240
+ at(node.initializer, `a literal vault secret value for ${vaultSecretKey(node.parent) ?? "a key"}`);
241
+ }
242
+ }
243
+ }
244
+ ts.forEachChild(node, visit);
245
+ };
246
+ visit(source);
247
+ return out;
248
+ }
249
+
250
+ /** `{ key, value }` with nothing else but a description: the shape of a vault secret. */
251
+ function isVaultSecret(obj: ts.ObjectLiteralExpression): boolean {
252
+ const names = obj.properties.map((p) => (ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p) ? propertyName(p.name) : undefined));
253
+ return names.includes("key") && names.includes("value") && names.every((n) => n === "key" || n === "value" || n === "description");
254
+ }
255
+
256
+ function vaultSecretKey(obj: ts.ObjectLiteralExpression): string | undefined {
257
+ for (const p of obj.properties) {
258
+ if (ts.isPropertyAssignment(p) && propertyName(p.name) === "key" && isStringLiteral(p.initializer)) return p.initializer.text;
259
+ }
260
+ return undefined;
261
+ }
262
+
263
+ /** `KEY=value`, `export KEY=value` or `KEY: value`, the value optionally quoted. */
264
+ const ASSIGNMENT = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_.-]*)\s*[=:]\s*(["']?)([^"'\s#]+)\2\s*(?:#.*)?$/;
265
+
266
+ /**
267
+ * The literal secrets in any other text file, such as a shell script, an
268
+ * env file or YAML: a credential shape anywhere, or a line assigning a
269
+ * literal to a credential-named key.
270
+ */
271
+ export function literalSecretsInText(text: string, assignments = true): LiteralSecret[] {
272
+ const out: LiteralSecret[] = [];
273
+ text.split(/\r?\n/).forEach((lineText, i) => {
274
+ for (const { pattern, label } of CREDENTIAL_SHAPES) {
275
+ const m = pattern.exec(lineText);
276
+ if (m && !isSecretReference(m[0])) {
277
+ out.push({ line: i + 1, column: m.index + 1, what: label });
278
+ return;
279
+ }
280
+ }
281
+ const a = assignments ? ASSIGNMENT.exec(lineText) : null;
282
+ if (a && isCredentialKey(a[1]) && !isSecretReference(a[3]) && !namesWhereSecretIs(a[3])) {
283
+ out.push({ line: i + 1, column: lineText.indexOf(a[3]) + 1, what: `a literal value for ${a[1]}` });
284
+ }
285
+ });
286
+ return out;
287
+ }
288
+
289
+ function extension(path: string): string {
290
+ const base = path.slice(path.lastIndexOf("/") + 1);
291
+ const dot = base.lastIndexOf(".");
292
+ return dot > 0 ? base.slice(dot).toLowerCase() : "";
293
+ }
294
+
295
+ /** The literal secrets in one file, or none when the file is binary or too large. */
296
+ export function literalSecretsInFile(text: string, path: string): LiteralSecret[] {
297
+ if (text.length > MAX_FILE_BYTES || text.includes("\0")) return [];
298
+ const kind = CODE_EXTENSIONS[extension(path)];
299
+ if (kind !== undefined) return literalSecretsInCode(text, path, kind);
300
+ // Prose says "Token: ..." without meaning one, so only a credential's shape counts there.
301
+ return literalSecretsInText(text, !PROSE_EXTENSIONS.has(extension(path)));
302
+ }
303
+
304
+ /** Every file under `dir` in `tree`, skipping node_modules and dot-directories, sorted. */
305
+ function filesUnder(tree: WorkspaceTree, dir: string): string[] {
306
+ const out: string[] = [];
307
+ const walk = (d: string) => {
308
+ for (const e of tree.list(d) ?? []) {
309
+ const p = joinPath(d, e.name);
310
+ if (e.type === "dir") {
311
+ if (!skippedDir(e.name)) walk(p);
312
+ } else out.push(p);
313
+ }
314
+ };
315
+ walk(dir);
316
+ return out.sort();
317
+ }
318
+
319
+ const boxMembers = (ctx: WorkspaceCheckContext): Member[] => ctx.declaration.members.filter((m) => m.box !== null);
320
+
321
+ export const BOX_CHECKS: readonly WorkspaceCheck[] = [
322
+ {
323
+ id: WSP_BOX_CREDENTIAL,
324
+ name: "box-credential-declared",
325
+ description:
326
+ "A box holds no credential: no file in a box member's directory carries a literal secret, in its declarations, an env or a vault's defaults. Variable references and secret-manager references (op://, bws://, infisical://) are not secrets.",
327
+ severity: "error",
328
+ configurable: false,
329
+ check(ctx) {
330
+ const out: WorkspaceDiagnostic[] = [];
331
+ for (const m of boxMembers(ctx)) {
332
+ const dir = m.dir === "." ? "" : m.dir;
333
+ if (ctx.tree.stat(dir) !== "dir") continue;
334
+ for (const path of filesUnder(ctx.tree, dir)) {
335
+ let text: string;
336
+ try {
337
+ text = ctx.tree.read(path);
338
+ } catch {
339
+ continue;
340
+ }
341
+ for (const s of literalSecretsInFile(text, path)) {
342
+ out.push({
343
+ checkId: this.id,
344
+ severity: this.severity,
345
+ code: "box-credential-declared",
346
+ message: `box-credential-declared: ${path} holds ${s.what}, and member ${m.name} is a box, which holds no credential; replace it with a \${VAR} or secret-manager reference, or declare the capability as brokered in the member's box block`,
347
+ entity: m.name,
348
+ pointer: m.box!.pointer,
349
+ treeFile: path,
350
+ line: s.line,
351
+ column: s.column,
352
+ });
353
+ }
354
+ }
355
+ }
356
+ return out;
357
+ },
358
+ },
359
+ {
360
+ id: WSP_BOX_UNBROKERED,
361
+ name: "box-capability-unbrokered",
362
+ description: "Every capability a box declares names the broker that holds its credential and enforces its scope.",
363
+ severity: "error",
364
+ configurable: true,
365
+ check(ctx) {
366
+ const out: WorkspaceDiagnostic[] = [];
367
+ for (const m of boxMembers(ctx)) {
368
+ for (const c of m.box!.capabilities) {
369
+ if (c.broker !== null) continue;
370
+ out.push({
371
+ checkId: this.id,
372
+ severity: this.severity,
373
+ code: "box-capability-unbrokered",
374
+ message: `box-capability-unbrokered: member ${m.name}'s box needs ${c.name} and names no broker for it, so the box would hold its credential; set broker to the runtime that holds it, such as the lobby`,
375
+ entity: m.name,
376
+ pointer: c.pointer,
377
+ });
378
+ }
379
+ }
380
+ return out;
381
+ },
382
+ },
383
+ // Box isolation (#2727), WSP123 and WSP124.
384
+ ...BOX_ISOLATION_CHECKS,
385
+ {
386
+ id: WSP_BOX_FOUNTAIN_CALLBACK,
387
+ name: "box-fountain-callback-undeclared",
388
+ description:
389
+ "A box member that builds a fountain Box declares the callback token fountain gives its persistent sandbox: the fountain-callback capability, brokered by fountain, with scope owner.",
390
+ severity: "error",
391
+ configurable: true,
392
+ check(ctx) {
393
+ const out: WorkspaceDiagnostic[] = [];
394
+ const want = FOUNTAIN_CALLBACK_CAPABILITY;
395
+ const expected = `{ "name": "${want.name}", "broker": "${want.broker}", "scope": ["${want.scope[0]}"] }`;
396
+ for (const m of boxMembers(ctx)) {
397
+ const dir = m.dir === "." ? "" : m.dir;
398
+ if (ctx.tree.stat(dir) !== "dir") continue;
399
+ let at: { path: string; call: FountainBoxCall } | undefined;
400
+ for (const path of filesUnder(ctx.tree, dir)) {
401
+ const kind = CODE_EXTENSIONS[extension(path)];
402
+ if (kind === undefined || kind === ts.ScriptKind.JSON) continue;
403
+ let text: string;
404
+ try {
405
+ text = ctx.tree.read(path);
406
+ } catch {
407
+ continue;
408
+ }
409
+ if (text.length > MAX_FILE_BYTES) continue;
410
+ const call = fountainBoxCall(text, path, kind);
411
+ if (call) {
412
+ at = { path, call };
413
+ break;
414
+ }
415
+ }
416
+ if (!at) continue;
417
+ const declared = m.box!.capabilities.find((c) => c.name === want.name);
418
+ if (declared && declared.broker === want.broker && declared.scope.includes(want.scope[0])) continue;
419
+ const why = !declared
420
+ ? "the member's box block does not declare it"
421
+ : declared.broker !== want.broker
422
+ ? `the box block's ${want.name} capability names the broker ${declared.broker ?? "(none)"}, but fountain hands this token to the sandbox itself`
423
+ : `the box block's ${want.name} capability has scope [${declared.scope.join(", ")}], not ${want.scope[0]}`;
424
+ out.push({
425
+ checkId: this.id,
426
+ severity: this.severity,
427
+ code: "box-fountain-callback-undeclared",
428
+ message:
429
+ `box-fountain-callback-undeclared: member ${m.name} builds a fountain Box (${at.path}), and fountain v0.21.0 gives a persistent box's sandbox ` +
430
+ `a callback token scoped to its owner (FOUNTAIN_TOKEN); ${why}. Declare ${expected} in the box block ` +
431
+ `(managoat/fountain#2497 tracks running without it)`,
432
+ entity: m.name,
433
+ pointer: declared?.pointer ?? m.box!.pointer,
434
+ treeFile: at.path,
435
+ line: at.call.line,
436
+ column: at.call.column,
437
+ });
438
+ }
439
+ return out;
440
+ },
441
+ },
442
+ ];
@@ -0,0 +1,134 @@
1
+ /**
2
+ * The diagram checks, WSP131 to WSP133 (#2764).
3
+ */
4
+
5
+ import { execFileSync } from "node:child_process";
6
+ import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
7
+ import { tmpdir } from "node:os";
8
+ import { dirname, join } from "node:path";
9
+ import { afterAll, describe, expect, test } from "vitest";
10
+ import { runDeclarationChecks } from "../checks";
11
+ import { sha256Hex } from "../../content-digest";
12
+
13
+ const scratch: string[] = [];
14
+ afterAll(() => {
15
+ for (const d of scratch) rmSync(d, { recursive: true, force: true });
16
+ });
17
+ function repo(files: Record<string, string>): string {
18
+ const root = realpathSync(mkdtempSync(join(tmpdir(), "chant-diagrams-")));
19
+ scratch.push(root);
20
+ execFileSync("git", ["init", "-q"], { cwd: root });
21
+ for (const [path, text] of Object.entries(files)) {
22
+ mkdirSync(dirname(join(root, path)), { recursive: true });
23
+ writeFileSync(join(root, path), text);
24
+ }
25
+ return root;
26
+ }
27
+
28
+ const renderer = { tool: "d2", version: "0.9.0", args: ["--layout=elk", "--theme=0", "--pad=40", "--omit-version"] };
29
+ const diagram = (extra: Record<string, unknown> = {}) => ({
30
+ name: "architecture",
31
+ title: "Studio architecture",
32
+ source: "docs/diagrams/architecture.d2",
33
+ render: "docs/diagrams/architecture.svg",
34
+ renderer,
35
+ ...extra,
36
+ });
37
+
38
+ const declaration = (diagrams: unknown[], extra: Record<string, unknown> = {}) =>
39
+ JSON.stringify({ name: "acme", schema: 1, members: [{ name: "docs", dir: "docs", kind: "other", because: "the docs site", diagrams }], ...extra }, null, 2);
40
+
41
+ /** The findings, less WSP009, which every member of kind other gets. */
42
+ const found = async (root: string) => (await runDeclarationChecks(root, (f) => f, { gather: false })).diagnostics.filter((d) => d.ruleId !== "WSP009");
43
+
44
+ describe("diagram-source-missing (WSP131) and diagram-render-missing (WSP132)", () => {
45
+ test("both files present passes", async () => {
46
+ const root = repo({
47
+ "chant.workspace.json": declaration([diagram()]),
48
+ "docs/diagrams/architecture.d2": "x -> y",
49
+ "docs/diagrams/architecture.svg": "<svg></svg>",
50
+ });
51
+ expect(await found(root)).toEqual([]);
52
+ });
53
+
54
+ test("a missing source fails WSP131, and a missing render fails WSP132, each once", async () => {
55
+ const root = repo({
56
+ "chant.workspace.json": declaration([diagram()]),
57
+ "docs/diagrams/architecture.svg": "<svg></svg>",
58
+ });
59
+ const d = await found(root);
60
+ expect(d.map((x) => [x.ruleId, x.code, x.entity])).toEqual([["WSP131", "diagram-source-missing", "docs"]]);
61
+ expect(d[0].message).toContain("member docs's diagram architecture names the source docs/diagrams/architecture.d2, which does not exist");
62
+ });
63
+
64
+ test("no source (an SVG with none) is never checked for a source", async () => {
65
+ const root = repo({
66
+ "chant.workspace.json": declaration([diagram({ source: null })]),
67
+ "docs/diagrams/architecture.svg": "<svg></svg>",
68
+ });
69
+ expect(await found(root)).toEqual([]);
70
+ });
71
+
72
+ test("a missing render fails WSP132", async () => {
73
+ const root = repo({
74
+ "chant.workspace.json": declaration([diagram()]),
75
+ "docs/diagrams/architecture.d2": "x -> y",
76
+ });
77
+ const d = await found(root);
78
+ expect(d.map((x) => [x.ruleId, x.code])).toEqual([["WSP132", "diagram-render-missing"]]);
79
+ });
80
+
81
+ test("the top-level (workspace's own) diagrams are checked too, reported with no entity", async () => {
82
+ const root = repo({ "chant.workspace.json": declaration([], { diagrams: [diagram({ name: "boundary" })] }), "docs/README.md": "" });
83
+ const d = await found(root);
84
+ expect(d.map((x) => [x.ruleId, x.entity])).toEqual([
85
+ ["WSP131", undefined],
86
+ ["WSP132", undefined],
87
+ ]);
88
+ expect(d[0].message).toContain("the workspace's own diagram boundary names the source");
89
+ });
90
+ });
91
+
92
+ describe("diagram-render-drift (WSP133)", () => {
93
+ const SOURCE = "x -> y\n";
94
+ const HASH = sha256Hex(Buffer.from(SOURCE, "utf-8"));
95
+
96
+ test("no recorded sourceHash: never checked for drift, even when the source changed", async () => {
97
+ const root = repo({
98
+ "chant.workspace.json": declaration([diagram()]),
99
+ "docs/diagrams/architecture.d2": "something else entirely",
100
+ "docs/diagrams/architecture.svg": "<svg></svg>",
101
+ });
102
+ expect(await found(root)).toEqual([]);
103
+ });
104
+
105
+ test("a recorded sourceHash that matches the source passes", async () => {
106
+ const root = repo({
107
+ "chant.workspace.json": declaration([diagram({ sourceHash: HASH })]),
108
+ "docs/diagrams/architecture.d2": SOURCE,
109
+ "docs/diagrams/architecture.svg": "<svg></svg>",
110
+ });
111
+ expect(await found(root)).toEqual([]);
112
+ });
113
+
114
+ test("a source that changed since the recorded hash fails WSP133, without running any renderer", async () => {
115
+ const root = repo({
116
+ "chant.workspace.json": declaration([diagram({ sourceHash: HASH })]),
117
+ "docs/diagrams/architecture.d2": "x -> y -> z\n",
118
+ "docs/diagrams/architecture.svg": "<svg></svg>",
119
+ });
120
+ const d = await found(root);
121
+ expect(d.map((x) => [x.ruleId, x.code])).toEqual([["WSP133", "diagram-render-drift"]]);
122
+ expect(d[0].message).toContain("source docs/diagrams/architecture.d2 changed since docs/diagrams/architecture.svg was rendered from it");
123
+ expect(d[0].message).toContain(`recorded sourceHash ${HASH.slice(0, 12)}`);
124
+ });
125
+
126
+ test("a missing source is left to WSP131; WSP133 finds nothing to compare", async () => {
127
+ const root = repo({
128
+ "chant.workspace.json": declaration([diagram({ sourceHash: HASH })]),
129
+ "docs/diagrams/architecture.svg": "<svg></svg>",
130
+ });
131
+ const d = await found(root);
132
+ expect(d.map((x) => x.ruleId)).toEqual(["WSP131"]);
133
+ });
134
+ });
@@ -0,0 +1,108 @@
1
+ /**
2
+ * The diagram checks, WSP131 to WSP133 (#2764).
3
+ *
4
+ * A declared diagram names a source and a render, pinned to the renderer
5
+ * its render was made with. chant never runs a renderer (ws-052), on a read
6
+ * path or here: these checks read the declaration and the tree checked
7
+ * only, and a render's drift is caught by comparing a recorded content hash,
8
+ * never by re-rendering the source.
9
+ *
10
+ * | Id | Code | Fails when |
11
+ * |---|---|---|
12
+ * | WSP131 | `diagram-source-missing` | a diagram names a source, and it does not exist in the tree read |
13
+ * | WSP132 | `diagram-render-missing` | a diagram's render does not exist in the tree read |
14
+ * | WSP133 | `diagram-render-drift` | a diagram records a `sourceHash`, and the source's bytes now hash to something else |
15
+ *
16
+ * WSP133 is opt-in per diagram: without a recorded `sourceHash` there is
17
+ * nothing to compare, so the entry is silently not checked for drift. A
18
+ * declaration records one by hashing the source when it commits a fresh
19
+ * render, the way a decision's evidence pins a file by hash
20
+ * (`../record-assets.ts`). Comparing that one recorded hash against the
21
+ * source in the tree read is cheap and deterministic, unlike shelling out to
22
+ * the pinned renderer and diffing its output, which chant does nowhere.
23
+ */
24
+
25
+ import { sha256Hex } from "../../content-digest";
26
+ import type { WorkspaceCheck, WorkspaceCheckContext, WorkspaceDiagnostic } from "../checks";
27
+ import { declaredDiagrams, type DiagramDeclaration } from "../declaration";
28
+ import type { ReasonCode } from "../reason-codes";
29
+
30
+ /** The read contract's codes for the diagram findings, carried as `code` on each. */
31
+ export const DIAGRAM_FINDING_CODES = ["diagram-source-missing", "diagram-render-missing", "diagram-render-drift"] as const satisfies readonly ReasonCode[];
32
+
33
+ export const WSP_DIAGRAM_SOURCE_MISSING = "WSP131";
34
+ export const WSP_DIAGRAM_RENDER_MISSING = "WSP132";
35
+ export const WSP_DIAGRAM_RENDER_DRIFT = "WSP133";
36
+
37
+ const where = (d: DiagramDeclaration) => (d.member === null ? "the workspace's own" : `member ${d.member}'s`);
38
+
39
+ function diagramFinding(check: WorkspaceCheck, d: DiagramDeclaration, field: "source" | "render" | "sourceHash", code: (typeof DIAGRAM_FINDING_CODES)[number], message: string): WorkspaceDiagnostic {
40
+ return {
41
+ checkId: check.id,
42
+ severity: check.severity,
43
+ code,
44
+ message: `${code}: ${message}`,
45
+ entity: d.member ?? undefined,
46
+ pointer: `${d.pointer}/${field}`,
47
+ };
48
+ }
49
+
50
+ export const DIAGRAM_CHECKS: readonly WorkspaceCheck[] = [
51
+ {
52
+ id: WSP_DIAGRAM_SOURCE_MISSING,
53
+ name: "diagram-source-missing",
54
+ description: "A diagram's source file, when it names one, exists in the tree read.",
55
+ severity: "error",
56
+ configurable: true,
57
+ check(ctx: WorkspaceCheckContext) {
58
+ const out: WorkspaceDiagnostic[] = [];
59
+ for (const d of declaredDiagrams(ctx.declaration)) {
60
+ if (d.source === null || ctx.tree.stat(d.source) === "file") continue;
61
+ out.push(diagramFinding(this, d, "source", "diagram-source-missing", `${where(d)} diagram ${d.name} names the source ${d.source}, which does not exist${ctx.tree.label}`));
62
+ }
63
+ return out;
64
+ },
65
+ },
66
+ {
67
+ id: WSP_DIAGRAM_RENDER_MISSING,
68
+ name: "diagram-render-missing",
69
+ description: "A diagram's render exists in the tree read.",
70
+ severity: "error",
71
+ configurable: true,
72
+ check(ctx: WorkspaceCheckContext) {
73
+ const out: WorkspaceDiagnostic[] = [];
74
+ for (const d of declaredDiagrams(ctx.declaration)) {
75
+ if (ctx.tree.stat(d.render) === "file") continue;
76
+ out.push(diagramFinding(this, d, "render", "diagram-render-missing", `${where(d)} diagram ${d.name} names the render ${d.render}, which does not exist${ctx.tree.label}`));
77
+ }
78
+ return out;
79
+ },
80
+ },
81
+ {
82
+ id: WSP_DIAGRAM_RENDER_DRIFT,
83
+ name: "diagram-render-drift",
84
+ description: "A diagram that records a sourceHash is unchanged since its render was made: the source's bytes still hash to it. Runs no renderer.",
85
+ severity: "error",
86
+ configurable: true,
87
+ check(ctx: WorkspaceCheckContext) {
88
+ const out: WorkspaceDiagnostic[] = [];
89
+ for (const d of declaredDiagrams(ctx.declaration)) {
90
+ if (d.sourceHash === null || d.source === null) continue;
91
+ if (ctx.tree.stat(d.source) !== "file") continue; // diagram-source-missing already reports it
92
+ const bytes = ctx.tree.bytes ? ctx.tree.bytes(d.source) : Buffer.from(ctx.tree.read(d.source), "utf-8");
93
+ const actual = sha256Hex(bytes);
94
+ if (actual === d.sourceHash) continue;
95
+ out.push(
96
+ diagramFinding(
97
+ this,
98
+ d,
99
+ "sourceHash",
100
+ "diagram-render-drift",
101
+ `${where(d)} diagram ${d.name}'s source ${d.source} changed since ${d.render} was rendered from it: recorded sourceHash ${d.sourceHash.slice(0, 12)} does not match the source's current hash ${actual.slice(0, 12)}${ctx.tree.label}; rerun the renderer and commit the result, or update sourceHash`,
102
+ ),
103
+ );
104
+ }
105
+ return out;
106
+ },
107
+ },
108
+ ];