@intentius/chant 0.91.0 → 0.94.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 (361) 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-plugins.d.ts +6 -6
  9. package/dist/cli/mcp/workspace-tools.d.ts +2 -1
  10. package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
  11. package/dist/cli/plugins.d.ts.map +1 -1
  12. package/dist/cli/registry.d.ts +12 -2
  13. package/dist/cli/registry.d.ts.map +1 -1
  14. package/dist/codegen/pinned-upgrade.d.ts.map +1 -1
  15. package/dist/config.d.ts +22 -0
  16. package/dist/config.d.ts.map +1 -1
  17. package/dist/lexicon-module.d.ts +22 -0
  18. package/dist/lexicon-module.d.ts.map +1 -1
  19. package/dist/lifecycle/converge-ledger.d.ts +19 -0
  20. package/dist/lifecycle/converge-ledger.d.ts.map +1 -1
  21. package/dist/lifecycle/run-ledger.d.ts +2 -0
  22. package/dist/lifecycle/run-ledger.d.ts.map +1 -1
  23. package/dist/lifecycle/symptoms.d.ts +51 -0
  24. package/dist/lifecycle/symptoms.d.ts.map +1 -1
  25. package/dist/lint/rules/index.d.ts +4 -2
  26. package/dist/lint/rules/index.d.ts.map +1 -1
  27. package/dist/lint/rules/op/index.d.ts +14 -3
  28. package/dist/lint/rules/op/index.d.ts.map +1 -1
  29. package/dist/lint/rules/op/ops014-converge-rule-refusals.d.ts.map +1 -1
  30. package/dist/lint/rules/op/sys010-key-over-plain-http.d.ts +29 -0
  31. package/dist/lint/rules/op/sys010-key-over-plain-http.d.ts.map +1 -0
  32. package/dist/lint/rules/sys001-no-literal-key.d.ts +3 -0
  33. package/dist/lint/rules/sys001-no-literal-key.d.ts.map +1 -0
  34. package/dist/op/__fixtures__/decide-stub-backend.d.ts +53 -0
  35. package/dist/op/__fixtures__/decide-stub-backend.d.ts.map +1 -0
  36. package/dist/op/activities/activity-contracts.d.ts +201 -0
  37. package/dist/op/activities/activity-contracts.d.ts.map +1 -1
  38. package/dist/op/activities/apply.d.ts.map +1 -1
  39. package/dist/op/activities/change-coverage.d.ts +44 -0
  40. package/dist/op/activities/change-coverage.d.ts.map +1 -0
  41. package/dist/op/activities/converge.d.ts +24 -1
  42. package/dist/op/activities/converge.d.ts.map +1 -1
  43. package/dist/op/activities/decide.d.ts +107 -0
  44. package/dist/op/activities/decide.d.ts.map +1 -0
  45. package/dist/op/activities/index.d.ts +10 -0
  46. package/dist/op/activities/index.d.ts.map +1 -1
  47. package/dist/op/activities/shell.d.ts +32 -0
  48. package/dist/op/activities/shell.d.ts.map +1 -1
  49. package/dist/op/activities/source-release.d.ts +86 -0
  50. package/dist/op/activities/source-release.d.ts.map +1 -0
  51. package/dist/op/activities/source-rollback.d.ts +100 -0
  52. package/dist/op/activities/source-rollback.d.ts.map +1 -0
  53. package/dist/op/activities/work-evidence.d.ts +42 -0
  54. package/dist/op/activities/work-evidence.d.ts.map +1 -0
  55. package/dist/op/activity-contract.d.ts +1 -0
  56. package/dist/op/activity-contract.d.ts.map +1 -1
  57. package/dist/op/activity-profiles.d.ts +16 -0
  58. package/dist/op/activity-profiles.d.ts.map +1 -1
  59. package/dist/op/builders.d.ts +84 -2
  60. package/dist/op/builders.d.ts.map +1 -1
  61. package/dist/op/composites/converge-op.d.ts +31 -4
  62. package/dist/op/composites/converge-op.d.ts.map +1 -1
  63. package/dist/op/decide-backend.d.ts +116 -0
  64. package/dist/op/decide-backend.d.ts.map +1 -0
  65. package/dist/op/decide-config.d.ts +120 -0
  66. package/dist/op/decide-config.d.ts.map +1 -0
  67. package/dist/op/decide-read-inputs.d.ts +40 -0
  68. package/dist/op/decide-read-inputs.d.ts.map +1 -0
  69. package/dist/op/gate-wait.d.ts +43 -0
  70. package/dist/op/gate-wait.d.ts.map +1 -0
  71. package/dist/op/gate.d.ts +5 -2
  72. package/dist/op/gate.d.ts.map +1 -1
  73. package/dist/op/index.d.ts +11 -4
  74. package/dist/op/index.d.ts.map +1 -1
  75. package/dist/op/local-executor.d.ts +29 -4
  76. package/dist/op/local-executor.d.ts.map +1 -1
  77. package/dist/op/local-output.d.ts +2 -0
  78. package/dist/op/local-output.d.ts.map +1 -1
  79. package/dist/op/op-ir.d.ts +2 -0
  80. package/dist/op/op-ir.d.ts.map +1 -1
  81. package/dist/op/op-verb-class.d.ts.map +1 -1
  82. package/dist/op/operator.d.ts +97 -1
  83. package/dist/op/operator.d.ts.map +1 -1
  84. package/dist/op/runtime.d.ts +37 -5
  85. package/dist/op/runtime.d.ts.map +1 -1
  86. package/dist/op/runtimes/local.d.ts.map +1 -1
  87. package/dist/op/source-archive.d.ts +63 -0
  88. package/dist/op/source-archive.d.ts.map +1 -0
  89. package/dist/op/steward-points.d.ts +136 -0
  90. package/dist/op/steward-points.d.ts.map +1 -0
  91. package/dist/op/steward-turn.d.ts +53 -0
  92. package/dist/op/steward-turn.d.ts.map +1 -0
  93. package/dist/op/steward.d.ts +22 -1
  94. package/dist/op/steward.d.ts.map +1 -1
  95. package/dist/op/types.d.ts +7 -0
  96. package/dist/op/types.d.ts.map +1 -1
  97. package/dist/op/work-lease-run.d.ts +1 -1
  98. package/dist/op/work-lease-run.d.ts.map +1 -1
  99. package/dist/workspace/__fixtures__/decide-workspace.d.ts +100 -0
  100. package/dist/workspace/__fixtures__/decide-workspace.d.ts.map +1 -0
  101. package/dist/workspace/answers-ledger.d.ts +76 -0
  102. package/dist/workspace/answers-ledger.d.ts.map +1 -0
  103. package/dist/workspace/box-intent.d.ts +85 -0
  104. package/dist/workspace/box-intent.d.ts.map +1 -0
  105. package/dist/workspace/changes-cli.d.ts +16 -0
  106. package/dist/workspace/changes-cli.d.ts.map +1 -0
  107. package/dist/workspace/changes.d.ts +200 -0
  108. package/dist/workspace/changes.d.ts.map +1 -0
  109. package/dist/workspace/chant-migrations/chud-lexicon-exit-fly-site.d.ts +59 -0
  110. package/dist/workspace/chant-migrations/chud-lexicon-exit-fly-site.d.ts.map +1 -0
  111. package/dist/workspace/chant-migrations/chud-lexicon-exit-live-names.d.ts +51 -0
  112. package/dist/workspace/chant-migrations/chud-lexicon-exit-live-names.d.ts.map +1 -0
  113. package/dist/workspace/chant-migrations/chud-lexicon-exit-rollback.d.ts +26 -0
  114. package/dist/workspace/chant-migrations/chud-lexicon-exit-rollback.d.ts.map +1 -0
  115. package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.d.ts +59 -0
  116. package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.d.ts.map +1 -0
  117. package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-policy.d.ts +22 -0
  118. package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-policy.d.ts.map +1 -0
  119. package/dist/workspace/chant-migrations/chud-lexicon-exit.d.ts +71 -0
  120. package/dist/workspace/chant-migrations/chud-lexicon-exit.d.ts.map +1 -0
  121. package/dist/workspace/chant-migrations.d.ts +104 -0
  122. package/dist/workspace/chant-migrations.d.ts.map +1 -0
  123. package/dist/workspace/checks/boxes.d.ts +47 -1
  124. package/dist/workspace/checks/boxes.d.ts.map +1 -1
  125. package/dist/workspace/checks/diagrams.d.ts +31 -0
  126. package/dist/workspace/checks/diagrams.d.ts.map +1 -0
  127. package/dist/workspace/checks/records.d.ts +7 -0
  128. package/dist/workspace/checks/records.d.ts.map +1 -1
  129. package/dist/workspace/checks.d.ts +7 -0
  130. package/dist/workspace/checks.d.ts.map +1 -1
  131. package/dist/workspace/compose-graph.d.ts +2 -0
  132. package/dist/workspace/compose-graph.d.ts.map +1 -1
  133. package/dist/workspace/conformance/index.d.ts.map +1 -1
  134. package/dist/workspace/decide.d.ts +49 -4
  135. package/dist/workspace/decide.d.ts.map +1 -1
  136. package/dist/workspace/decision-points.schema.json +3 -3
  137. package/dist/workspace/declaration.d.ts +58 -0
  138. package/dist/workspace/declaration.d.ts.map +1 -1
  139. package/dist/workspace/declaration.schema.json +119 -1
  140. package/dist/workspace/declared-kinds.d.ts +20 -0
  141. package/dist/workspace/declared-kinds.d.ts.map +1 -1
  142. package/dist/workspace/generated-files.d.ts +7 -0
  143. package/dist/workspace/generated-files.d.ts.map +1 -1
  144. package/dist/workspace/intent-joins.d.ts +5 -5
  145. package/dist/workspace/intent.d.ts.map +1 -1
  146. package/dist/workspace/lineage-check.d.ts +3 -0
  147. package/dist/workspace/lineage-check.d.ts.map +1 -1
  148. package/dist/workspace/lineage-upgrade.d.ts +6 -1
  149. package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
  150. package/dist/workspace/ls.d.ts +39 -1
  151. package/dist/workspace/ls.d.ts.map +1 -1
  152. package/dist/workspace/point-answer.schema.json +95 -0
  153. package/dist/workspace/points-cli.d.ts +2 -0
  154. package/dist/workspace/points-cli.d.ts.map +1 -1
  155. package/dist/workspace/points.d.ts +4 -6
  156. package/dist/workspace/points.d.ts.map +1 -1
  157. package/dist/workspace/reason-codes.d.ts +16 -2
  158. package/dist/workspace/reason-codes.d.ts.map +1 -1
  159. package/dist/workspace/records-cli.d.ts +11 -1
  160. package/dist/workspace/records-cli.d.ts.map +1 -1
  161. package/dist/workspace/records-close.d.ts +1 -1
  162. package/dist/workspace/records-close.d.ts.map +1 -1
  163. package/dist/workspace/records-write.d.ts +11 -5
  164. package/dist/workspace/records-write.d.ts.map +1 -1
  165. package/dist/workspace/records.d.ts +34 -4
  166. package/dist/workspace/records.d.ts.map +1 -1
  167. package/dist/workspace/status-gates.d.ts +7 -2
  168. package/dist/workspace/status-gates.d.ts.map +1 -1
  169. package/dist/workspace/status-stewards.d.ts +65 -1
  170. package/dist/workspace/status-stewards.d.ts.map +1 -1
  171. package/dist/workspace/status.d.ts +24 -0
  172. package/dist/workspace/status.d.ts.map +1 -1
  173. package/dist/workspace/work-cli.d.ts +113 -2
  174. package/dist/workspace/work-cli.d.ts.map +1 -1
  175. package/dist/workspace/work-evidence.d.ts +74 -0
  176. package/dist/workspace/work-evidence.d.ts.map +1 -0
  177. package/dist/workspace/work.d.ts +43 -1
  178. package/dist/workspace/work.d.ts.map +1 -1
  179. package/package.json +1 -1
  180. package/src/cli/commands/audit.test.ts +9 -1
  181. package/src/cli/commands/build.ts +3 -2
  182. package/src/cli/commands/lint.ts +7 -1
  183. package/src/cli/handlers/graph.test.ts +20 -0
  184. package/src/cli/handlers/graph.ts +16 -2
  185. package/src/cli/handlers/operator.ts +34 -2
  186. package/src/cli/handlers/run.test.ts +154 -2
  187. package/src/cli/handlers/run.ts +121 -2
  188. package/src/cli/main.test.ts +10 -0
  189. package/src/cli/main.ts +23 -3
  190. package/src/cli/mcp/workspace-plugins.ts +6 -6
  191. package/src/cli/mcp/workspace-tools.test.ts +35 -19
  192. package/src/cli/mcp/workspace-tools.ts +62 -4
  193. package/src/cli/plugins.ts +11 -3
  194. package/src/cli/registry.ts +12 -2
  195. package/src/cli/serve-mcp-workspace.test.ts +6 -6
  196. package/src/codegen/pinned-upgrade.ts +2 -1
  197. package/src/codegen/release-wiring.test.ts +49 -2
  198. package/src/components/capability-plugin-loader.ts +3 -3
  199. package/src/components/cli-support.ts +2 -2
  200. package/src/config.ts +11 -0
  201. package/src/lexicon-module.ts +56 -1
  202. package/src/lexicon-package.test.ts +96 -0
  203. package/src/lifecycle/converge-ledger.ts +20 -0
  204. package/src/lifecycle/run-ledger.ts +6 -0
  205. package/src/lifecycle/symptoms.ts +95 -0
  206. package/src/lint/rules/index.ts +6 -2
  207. package/src/lint/rules/op/index.ts +17 -4
  208. package/src/lint/rules/op/ops014-converge-rule-refusals.test.ts +23 -1
  209. package/src/lint/rules/op/ops014-converge-rule-refusals.ts +10 -6
  210. package/src/lint/rules/op/sys010-key-over-plain-http.test.ts +52 -0
  211. package/src/lint/rules/op/sys010-key-over-plain-http.ts +100 -0
  212. package/src/lint/rules/sys001-no-literal-key.test.ts +31 -0
  213. package/src/lint/rules/sys001-no-literal-key.ts +67 -0
  214. package/src/op/__fixtures__/decide-stub-backend.ts +143 -0
  215. package/src/op/activities/activity-contracts.ts +190 -1
  216. package/src/op/activities/apply.ts +7 -6
  217. package/src/op/activities/change-coverage.ts +60 -0
  218. package/src/op/activities/converge.test.ts +55 -1
  219. package/src/op/activities/converge.ts +208 -29
  220. package/src/op/activities/decide.live.test.ts +78 -0
  221. package/src/op/activities/decide.test.ts +308 -0
  222. package/src/op/activities/decide.ts +267 -0
  223. package/src/op/activities/index.ts +21 -0
  224. package/src/op/activities/shell.test.ts +91 -1
  225. package/src/op/activities/shell.ts +91 -2
  226. package/src/op/activities/source-release.test.ts +118 -0
  227. package/src/op/activities/source-release.ts +168 -0
  228. package/src/op/activities/source-rollback.test.ts +131 -0
  229. package/src/op/activities/source-rollback.ts +239 -0
  230. package/src/op/activities/work-evidence.ts +62 -0
  231. package/src/op/activity-contract-registry.test.ts +2 -0
  232. package/src/op/activity-contract-registry.ts +2 -2
  233. package/src/op/activity-contract.test.ts +12 -0
  234. package/src/op/activity-contract.ts +6 -1
  235. package/src/op/activity-profiles.ts +29 -0
  236. package/src/op/activity-registry.test.ts +1 -1
  237. package/src/op/activity-registry.ts +2 -2
  238. package/src/op/builders.ts +119 -5
  239. package/src/op/command-gate.test.ts +192 -0
  240. package/src/op/composites/converge-op.test.ts +27 -1
  241. package/src/op/composites/converge-op.ts +69 -15
  242. package/src/op/decide-backend.test.ts +44 -0
  243. package/src/op/decide-backend.ts +212 -0
  244. package/src/op/decide-config.test.ts +52 -0
  245. package/src/op/decide-config.ts +80 -0
  246. package/src/op/decide-read-inputs.ts +117 -0
  247. package/src/op/gate-approval.ts +2 -2
  248. package/src/op/gate-wait.ts +57 -0
  249. package/src/op/gate.ts +8 -3
  250. package/src/op/generate-pipeline.ts +2 -2
  251. package/src/op/index.ts +18 -3
  252. package/src/op/local-executor.ts +151 -15
  253. package/src/op/local-output.test.ts +33 -0
  254. package/src/op/local-output.ts +20 -2
  255. package/src/op/op-ir.test.ts +10 -0
  256. package/src/op/op-ir.ts +4 -0
  257. package/src/op/op-verb-class.ts +1 -0
  258. package/src/op/operator.ts +290 -11
  259. package/src/op/runtime.ts +31 -8
  260. package/src/op/runtimes/local.ts +3 -1
  261. package/src/op/source-archive.ts +161 -0
  262. package/src/op/steward-points.test.ts +364 -0
  263. package/src/op/steward-points.ts +194 -0
  264. package/src/op/steward-turn.ts +79 -0
  265. package/src/op/steward.test.ts +87 -3
  266. package/src/op/steward.ts +25 -1
  267. package/src/op/types.ts +7 -0
  268. package/src/op/work-lease-run.test.ts +20 -1
  269. package/src/op/work-lease-run.ts +2 -2
  270. package/src/workspace/__fixtures__/decide-workspace.ts +129 -0
  271. package/src/workspace/__fixtures__/studio-template-84c8b21.json +67 -0
  272. package/src/workspace/answers-ledger.ts +118 -0
  273. package/src/workspace/box-intent.test.ts +205 -0
  274. package/src/workspace/box-intent.ts +159 -0
  275. package/src/workspace/changes-cli.ts +56 -0
  276. package/src/workspace/changes.schema.json +472 -0
  277. package/src/workspace/changes.test.ts +315 -0
  278. package/src/workspace/changes.ts +512 -0
  279. package/src/workspace/chant-migrations/chud-lexicon-exit-fly-site.ts +234 -0
  280. package/src/workspace/chant-migrations/chud-lexicon-exit-live-names.ts +164 -0
  281. package/src/workspace/chant-migrations/chud-lexicon-exit-rollback.ts +223 -0
  282. package/src/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.ts +195 -0
  283. package/src/workspace/chant-migrations/chud-lexicon-exit-ship-policy.ts +108 -0
  284. package/src/workspace/chant-migrations/chud-lexicon-exit.ts +882 -0
  285. package/src/workspace/chant-migrations.e2e.test.ts +1089 -0
  286. package/src/workspace/chant-migrations.ts +169 -0
  287. package/src/workspace/check-contract.test.ts +4 -2
  288. package/src/workspace/check.schema.json +19 -5
  289. package/src/workspace/checks/boxes.test.ts +82 -2
  290. package/src/workspace/checks/boxes.ts +202 -1
  291. package/src/workspace/checks/diagrams.test.ts +134 -0
  292. package/src/workspace/checks/diagrams.ts +108 -0
  293. package/src/workspace/checks/records.ts +47 -0
  294. package/src/workspace/checks.ts +20 -2
  295. package/src/workspace/compose-graph.ts +2 -0
  296. package/src/workspace/conformance/__fixture__/decisions/decision.kind.mjs +10 -0
  297. package/src/workspace/conformance/__fixture__/decisions/decision.schema.json +26 -0
  298. package/src/workspace/conformance/index.ts +5 -0
  299. package/src/workspace/decide.test.ts +1 -1
  300. package/src/workspace/decide.ts +156 -23
  301. package/src/workspace/decision-points.schema.json +3 -3
  302. package/src/workspace/declaration.schema.json +119 -1
  303. package/src/workspace/declaration.test.ts +58 -0
  304. package/src/workspace/declaration.ts +112 -0
  305. package/src/workspace/declared-kinds.test.ts +31 -5
  306. package/src/workspace/declared-kinds.ts +46 -1
  307. package/src/workspace/generated-files.ts +14 -0
  308. package/src/workspace/graph-cli.ts +1 -1
  309. package/src/workspace/graph.schema.json +9 -1
  310. package/src/workspace/intent-joins.test.ts +5 -5
  311. package/src/workspace/intent-joins.ts +5 -5
  312. package/src/workspace/intent.schema.json +8 -2
  313. package/src/workspace/intent.ts +3 -10
  314. package/src/workspace/lineage-check.ts +16 -1
  315. package/src/workspace/lineage-upgrade-cli.ts +1 -1
  316. package/src/workspace/lineage-upgrade.test.ts +70 -0
  317. package/src/workspace/lineage-upgrade.ts +81 -10
  318. package/src/workspace/ls-contract.test.ts +57 -0
  319. package/src/workspace/ls.schema.json +46 -1
  320. package/src/workspace/ls.ts +62 -3
  321. package/src/workspace/points-cli.ts +17 -3
  322. package/src/workspace/points-write.schema.json +11 -1
  323. package/src/workspace/points.schema.json +51 -0
  324. package/src/workspace/points.test.ts +16 -15
  325. package/src/workspace/points.ts +5 -17
  326. package/src/workspace/read-contract.test.ts +9 -1
  327. package/src/workspace/reason-codes.test.ts +18 -8
  328. package/src/workspace/reason-codes.ts +21 -2
  329. package/src/workspace/records-amend.schema.json +2 -0
  330. package/src/workspace/records-cli.ts +27 -4
  331. package/src/workspace/records-close.schema.json +2 -0
  332. package/src/workspace/records-formats.test.ts +15 -15
  333. package/src/workspace/records-new.schema.json +2 -0
  334. package/src/workspace/records-review.schema.json +2 -0
  335. package/src/workspace/records-sessions-write.e2e.test.ts +77 -0
  336. package/src/workspace/records-sessions-write.test.ts +2 -34
  337. package/src/workspace/records-since.test.ts +8 -7
  338. package/src/workspace/records-write.e2e.test.ts +108 -0
  339. package/src/workspace/records-write.test.ts +32 -41
  340. package/src/workspace/records-write.ts +33 -15
  341. package/src/workspace/records.schema.json +36 -2
  342. package/src/workspace/records.test.ts +49 -4
  343. package/src/workspace/records.ts +109 -3
  344. package/src/workspace/status-contract.test.ts +14 -8
  345. package/src/workspace/status-gates.ts +9 -4
  346. package/src/workspace/status-stewards.ts +155 -5
  347. package/src/workspace/status.schema.json +129 -8
  348. package/src/workspace/status.ts +46 -1
  349. package/src/workspace/work-acceptance.test.ts +274 -0
  350. package/src/workspace/{work-cli.test.ts → work-cli.e2e.test.ts} +1 -1
  351. package/src/workspace/work-cli.ts +193 -10
  352. package/src/workspace/work-evidence.schema.json +212 -0
  353. package/src/workspace/work-evidence.ts +169 -0
  354. package/src/workspace/work-history.schema.json +349 -0
  355. package/src/workspace/work-history.test.ts +156 -0
  356. package/src/workspace/{work-readiness-chud.test.ts → work-readiness.test.ts} +30 -32
  357. package/src/workspace/work.test.ts +20 -2
  358. package/src/workspace/work.ts +100 -1
  359. /package/src/cli/{discovery-skip.test.ts → discovery-skip.e2e.test.ts} +0 -0
  360. /package/src/workspace/conformance/{conformance.test.ts → conformance.e2e.test.ts} +0 -0
  361. /package/src/workspace/{which-chant.test.ts → which-chant.e2e.test.ts} +0 -0
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Migrations chant ships (#2737).
3
+ *
4
+ * A template's own migrations (./lineage-migrations.ts) move a scope between
5
+ * two versions of that template. Some moves are chant's instead: a part a
6
+ * template used goes away, and chant knows where each piece of it went. The
7
+ * first is `chud-lexicon-exit`, which takes a repo made from chud's template
8
+ * (or the studio kit's copy of it) off `@intentius/chant-lexicon-chud` and
9
+ * `@intentius/chud-runtime` (ws-056).
10
+ *
11
+ * `chant workspace upgrade` asks each one for a plan after the template's own
12
+ * chain has run in the staging worktree, and before the per-file merge. A
13
+ * migration plans from what the scope holds, not from a version range, so it
14
+ * works whatever ref the scope is pinned at. Its plan lists every file it
15
+ * writes or deletes and why, what it cannot move and where that went, and any
16
+ * conflict. A plan with a conflict is not applied at all, and the upgrade's
17
+ * checks fail with it, so the dry run shows the whole plan and the tree is
18
+ * never left half moved.
19
+ *
20
+ * An applied migration's id is added to the lineage's `migrations`, so the
21
+ * lock records it and a second upgrade does not plan it again. A migration's
22
+ * plan is also empty once nothing it moves is left, so running it twice
23
+ * changes nothing either way.
24
+ *
25
+ * Files the migration rewrites keep their lineage entry: the lock's hash is
26
+ * still the template's, so to a later upgrade they are the project's own
27
+ * edits, merged per file like any other (ws-005). A deleted file keeps its
28
+ * entry too, so a template version that still has it leaves it deleted.
29
+ *
30
+ * chant itself carries no chud lexicon or chud-shaped code (#2830, once
31
+ * #2715's proof passed): `chud-lexicon-exit` and its follow-ons are the
32
+ * retirement tool, not a remaining dependency. They stay until no known repo
33
+ * still depends on chud, and are removed only in a major version.
34
+ */
35
+
36
+ import { mkdirSync, rmSync, writeFileSync } from "node:fs";
37
+ import { dirname, join } from "node:path";
38
+ import type { Lineage } from "./lineage-lock";
39
+ import { chudLexiconExit } from "./chant-migrations/chud-lexicon-exit";
40
+ import { chudLexiconExitRollback } from "./chant-migrations/chud-lexicon-exit-rollback";
41
+ import { chudLexiconExitShipInputs } from "./chant-migrations/chud-lexicon-exit-ship-inputs";
42
+ import { chudLexiconExitShipPolicy } from "./chant-migrations/chud-lexicon-exit-ship-policy";
43
+ import { chudLexiconExitFlySite } from "./chant-migrations/chud-lexicon-exit-fly-site";
44
+ import { chudLexiconExitLiveNames } from "./chant-migrations/chud-lexicon-exit-live-names";
45
+
46
+ /** One file a plan writes or deletes, relative to the scope. */
47
+ export interface PlannedChange {
48
+ path: string;
49
+ action: "write" | "delete";
50
+ /** Why, in a few words. */
51
+ why: string;
52
+ /** The new content, for a write. */
53
+ data?: Buffer;
54
+ /** True when the file had been edited since the template wrote it; the edit stays in git history. */
55
+ edited?: boolean;
56
+ }
57
+
58
+ /** A part the migration does not move, and where it went instead. */
59
+ export interface NotMoved {
60
+ what: string;
61
+ where: string;
62
+ }
63
+
64
+ /** Why a plan cannot be applied. */
65
+ export interface PlanConflict {
66
+ path: string;
67
+ reason: string;
68
+ }
69
+
70
+ export interface ChantMigrationPlan {
71
+ id: string;
72
+ description: string;
73
+ changes: PlannedChange[];
74
+ notMoved: NotMoved[];
75
+ conflicts: PlanConflict[];
76
+ }
77
+
78
+ export interface ChantMigrationContext {
79
+ /** The scope directory (in the staging worktree). */
80
+ dir: string;
81
+ lineage: Lineage;
82
+ /** The chant version doing the upgrade, for dependency ranges. */
83
+ chantVersion: string;
84
+ }
85
+
86
+ export interface ChantMigration {
87
+ id: string;
88
+ description: string;
89
+ /** The plan for this scope, or null when there is nothing here for this migration to move. */
90
+ plan(ctx: ChantMigrationContext): ChantMigrationPlan | null;
91
+ }
92
+
93
+ /** Every migration chant ships, in the order they are planned. */
94
+ export const CHANT_MIGRATIONS: readonly ChantMigration[] = [
95
+ chudLexiconExit,
96
+ chudLexiconExitRollback,
97
+ chudLexiconExitShipInputs,
98
+ chudLexiconExitShipPolicy,
99
+ chudLexiconExitFlySite,
100
+ chudLexiconExitLiveNames,
101
+ ];
102
+
103
+ /** The plans for a scope: each migration not yet in the lineage that finds something to move. */
104
+ export function planChantMigrations(ctx: ChantMigrationContext, migrations: readonly ChantMigration[] = CHANT_MIGRATIONS): ChantMigrationPlan[] {
105
+ const applied = new Set(ctx.lineage.migrations);
106
+ const plans: ChantMigrationPlan[] = [];
107
+ for (const m of migrations) {
108
+ if (applied.has(m.id)) continue;
109
+ const plan = m.plan(ctx);
110
+ if (plan && (plan.changes.length > 0 || plan.conflicts.length > 0)) plans.push(plan);
111
+ }
112
+ return plans;
113
+ }
114
+
115
+ /**
116
+ * Plan each migration in order and apply each plan with no conflict before
117
+ * the next is planned, so a migration planned after another sees the tree
118
+ * the earlier one left (#2800: `chud-lexicon-exit-rollback` after
119
+ * `chud-lexicon-exit`). A plan with a conflict is not applied, and the ones
120
+ * after it plan from the tree as it is.
121
+ */
122
+ export function runChantMigrations(ctx: ChantMigrationContext, migrations: readonly ChantMigration[] = CHANT_MIGRATIONS): Array<{ plan: ChantMigrationPlan; applied: boolean }> {
123
+ const out: Array<{ plan: ChantMigrationPlan; applied: boolean }> = [];
124
+ for (const m of migrations) {
125
+ if (ctx.lineage.migrations.includes(m.id)) continue;
126
+ const plan = m.plan(ctx);
127
+ if (!plan || (plan.changes.length === 0 && plan.conflicts.length === 0)) continue;
128
+ const applied = plan.conflicts.length === 0;
129
+ if (applied) applyChantMigration(plan, ctx.dir, ctx.lineage);
130
+ out.push({ plan, applied });
131
+ }
132
+ return out;
133
+ }
134
+
135
+ /** Apply a plan with no conflicts, and record its id in the lineage. */
136
+ export function applyChantMigration(plan: ChantMigrationPlan, dir: string, lineage: Lineage): void {
137
+ if (plan.conflicts.length > 0) throw new Error(`${plan.id} has conflicts and cannot be applied`);
138
+ for (const c of plan.changes) {
139
+ const abs = join(dir, c.path);
140
+ if (c.action === "delete") {
141
+ rmSync(abs, { force: true });
142
+ } else {
143
+ mkdirSync(dirname(abs), { recursive: true });
144
+ writeFileSync(abs, c.data!);
145
+ }
146
+ }
147
+ if (!lineage.migrations.includes(plan.id)) lineage.migrations.push(plan.id);
148
+ }
149
+
150
+ /** A plan as the upgrade reports it: no file contents. */
151
+ export interface ChantMigrationReport {
152
+ id: string;
153
+ description: string;
154
+ applied: boolean;
155
+ changes: Array<Omit<PlannedChange, "data">>;
156
+ notMoved: NotMoved[];
157
+ conflicts: PlanConflict[];
158
+ }
159
+
160
+ export function reportPlan(plan: ChantMigrationPlan, applied: boolean): ChantMigrationReport {
161
+ return {
162
+ id: plan.id,
163
+ description: plan.description,
164
+ applied,
165
+ changes: plan.changes.map(({ data: _d, ...rest }) => rest),
166
+ notMoved: plan.notMoved,
167
+ conflicts: plan.conflicts,
168
+ };
169
+ }
@@ -11,6 +11,8 @@ import { afterAll, describe, expect, test } from "vitest";
11
11
  import { cleanScratch, commitAll, contract, declaration, git, REPO, repo, scratchDir, validSchema } from "./__fixtures__/contract-repo";
12
12
  import schema from "./check.schema.json";
13
13
  import { BOX_FINDING_CODES } from "./checks/boxes";
14
+ import { RECORD_FINDING_CODES } from "./checks/records";
15
+ import { DIAGRAM_FINDING_CODES } from "./checks/diagrams";
14
16
  import { WORKSPACE_ERROR_CODES } from "./declaration";
15
17
  import { CHECK_CODES, CHECK_CONTRACT_VERSION, CHECK_ERROR_CODES, CHECK_OUTPUT_SCHEMA_ID, runChecks, type CheckDocument } from "./lineage-check";
16
18
 
@@ -36,8 +38,8 @@ describe("check output schema", () => {
36
38
  test("lists exactly the codes the code can return", () => {
37
39
  expect(schema.$defs.lockFinding.properties.code.enum).toEqual([...CHECK_CODES]);
38
40
  expect(schema.$defs.failure.properties.error.properties.code.enum).toEqual([...CHECK_ERROR_CODES]);
39
- expect(schema.$defs.diagnostic.properties.code.enum).toEqual([...WORKSPACE_ERROR_CODES, ...BOX_FINDING_CODES]);
40
- expect(schema.$defs.suppressed.properties.code.enum).toEqual([...WORKSPACE_ERROR_CODES, ...BOX_FINDING_CODES]);
41
+ expect(schema.$defs.diagnostic.properties.code.enum).toEqual([...WORKSPACE_ERROR_CODES, ...BOX_FINDING_CODES, ...RECORD_FINDING_CODES, ...DIAGRAM_FINDING_CODES]);
42
+ expect(schema.$defs.suppressed.properties.code.enum).toEqual([...WORKSPACE_ERROR_CODES, ...BOX_FINDING_CODES, ...RECORD_FINDING_CODES, ...DIAGRAM_FINDING_CODES]);
41
43
  });
42
44
  });
43
45
 
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "https://intentius.io/chant/schemas/workspace/check/v1/check.schema.json",
4
4
  "title": "chant workspace check output",
5
- "description": "What `chant workspace check --format json` prints, version 1 of the read contract for checking a workspace (#2524 D9, D15, D16, #2535, #2536). The lineage lock findings carry reason codes; the declaration findings carry WSP ids from the check catalog, WSP001, a declaration that can't be read, also carries the declaration's reason code, and the box checks WSP121 to WSP124 carry theirs. Readers ignore fields they do not know; a field is only ever added within a version. The reason and error codes are closed lists: a new code is a new contract version. Contract version 1 is written by chant 0.81.0 and newer; a reader that needs it refuses output whose `contract` it doesn't know. Every code is in the one closed list of `reason-codes.ts`.",
5
+ "description": "What `chant workspace check --format json` prints, version 1 of the read contract for checking a workspace (#2524 D9, D15, D16, #2535, #2536). The lineage lock findings carry reason codes; the declaration findings carry WSP ids from the check catalog, WSP001, a declaration that can't be read, also carries the declaration's reason code, and the box checks WSP121 to WSP127 carry theirs. Readers ignore fields they do not know; a field is only ever added within a version. The reason and error codes are closed lists: a new code is a new contract version. Contract version 1 is written by chant 0.81.0 and newer; a reader that needs it refuses output whose `contract` it doesn't know. Every code is in the one closed list of `reason-codes.ts`.",
6
6
  "oneOf": [
7
7
  {
8
8
  "$ref": "#/$defs/result"
@@ -226,9 +226,16 @@
226
226
  "box-credential-declared",
227
227
  "box-capability-unbrokered",
228
228
  "box-isolation-collision",
229
- "box-isolation-literal"
229
+ "box-isolation-literal",
230
+ "box-fountain-callback-undeclared",
231
+ "box-intent-unknown",
232
+ "box-intent-unconstrained",
233
+ "work-acceptance-unmet",
234
+ "diagram-source-missing",
235
+ "diagram-render-missing",
236
+ "diagram-render-drift"
230
237
  ],
231
- "description": "For WSP001: why the declaration can't be read. For WSP121 to WSP124, the box finding (#2726, #2727)."
238
+ "description": "For WSP001: why the declaration can't be read. For WSP121 to WSP127, the box finding (#2726, #2727, #2780, #2850). For WSP117, work-acceptance-unmet (#2772). For WSP131 to WSP133, the diagram finding (#2764)."
232
239
  }
233
240
  }
234
241
  },
@@ -297,9 +304,16 @@
297
304
  "box-credential-declared",
298
305
  "box-capability-unbrokered",
299
306
  "box-isolation-collision",
300
- "box-isolation-literal"
307
+ "box-isolation-literal",
308
+ "box-fountain-callback-undeclared",
309
+ "box-intent-unknown",
310
+ "box-intent-unconstrained",
311
+ "work-acceptance-unmet",
312
+ "diagram-source-missing",
313
+ "diagram-render-missing",
314
+ "diagram-render-drift"
301
315
  ],
302
- "description": "For WSP001: why the declaration can't be read. For WSP121 to WSP124, the box finding (#2726, #2727)."
316
+ "description": "For WSP001: why the declaration can't be read. For WSP121 to WSP127, the box finding (#2726, #2727, #2780, #2850). For WSP117, work-acceptance-unmet (#2772). For WSP131 to WSP133, the diagram finding (#2764)."
303
317
  },
304
318
  "reason": {
305
319
  "type": "string"
@@ -7,7 +7,7 @@ import { afterAll, describe, expect, test } from "vitest";
7
7
  import { runDeclarationChecks } from "../checks";
8
8
  import { parseDeclaration, WorkspaceReadError } from "../declaration";
9
9
  import { gitTree } from "../tree";
10
- import { isCredentialKey, isSecretReference, literalSecretsInCode, literalSecretsInText } from "./boxes";
10
+ import { fountainBoxCall, isCredentialKey, isSecretReference, literalSecretsInCode, literalSecretsInText } from "./boxes";
11
11
 
12
12
  const scratch: string[] = [];
13
13
  afterAll(() => {
@@ -89,7 +89,7 @@ describe("box-credential-declared (WSP121)", () => {
89
89
  test("an env file and a shell script in the box's directory are read too; prose only for credential shapes", async () => {
90
90
  const root = repo({
91
91
  "chant.workspace.json": declaration(BROKERED),
92
- "spec/.env": `FOUNTAIN_API_KEY=abc123def456\nPORT=8080\nGIT_TOKEN=\${CHUD_GIT_TOKEN}\n`,
92
+ "spec/.env": `FOUNTAIN_API_KEY=abc123def456\nPORT=8080\nGIT_TOKEN=\${ACME_GIT_TOKEN}\n`,
93
93
  "spec/run.sh": `#!/bin/sh\nexport GITHUB_TOKEN=${GITHUB_TOKEN}\necho "password=$GIT_TOKEN"\nTOKEN=$1\ntoken=~/box/llm-token\nSECRET=/run/secrets/x\n`,
94
94
  "spec/README.md": `Token: rotate it monthly.\n`,
95
95
  "spec/node_modules/pkg/index.js": `export const k = ${JSON.stringify(ANTHROPIC_KEY)};\n`,
@@ -156,6 +156,7 @@ describe("the box block in the declaration", () => {
156
156
  expect(d.members[0].box).toEqual({
157
157
  pointer: "/members/0/box",
158
158
  isolation: null,
159
+ intent: null,
159
160
  capabilities: [
160
161
  { name: "fountain", broker: "lobby", scope: ["agent", "vault"], pointer: "/members/0/box/capabilities/0" },
161
162
  { name: "inference", broker: null, scope: [], pointer: "/members/0/box/capabilities/1" },
@@ -195,3 +196,82 @@ describe("what counts as a literal secret", () => {
195
196
  expect(literalSecretsInText(`# nothing\nkey: ${GITHUB_TOKEN}\n`).map((s) => s.line)).toEqual([2]);
196
197
  });
197
198
  });
199
+
200
+ describe("box-fountain-callback-undeclared (WSP125, #2780)", () => {
201
+ /** A studio-style fountain Box declaration. */
202
+ const fountainBox = `import { readFileSync } from "node:fs";
203
+ import { Box } from "@intentius/chant-lexicon-fountain";
204
+
205
+ export const { environment, agent } = Box({
206
+ name: "studio-box",
207
+ setupScript: readFileSync("box/setup.sh", "utf8"),
208
+ model: "anthropic/claude-sonnet-4-6",
209
+ permissionPolicy: { default: "auto_allow" },
210
+ });
211
+ `;
212
+ const CALLBACK = { name: "fountain-callback", broker: "fountain", scope: ["owner"] };
213
+
214
+ test("a box member that builds a fountain Box and does not declare the callback token fails, at the Box call", async () => {
215
+ const root = repo({ "chant.workspace.json": declaration(BROKERED), "spec/box.ts": fountainBox });
216
+ const d = await found(root);
217
+ expect(d.map((x) => [x.ruleId, x.code, x.entity, x.file, x.line])).toEqual([
218
+ ["WSP125", "box-fountain-callback-undeclared", "spec", "spec/box.ts", 4],
219
+ ]);
220
+ expect(d[0].message).toContain("callback token scoped to its owner (FOUNTAIN_TOKEN); the member's box block does not declare it");
221
+ expect(d[0].message).toContain('{ "name": "fountain-callback", "broker": "fountain", "scope": ["owner"] }');
222
+ });
223
+
224
+ test("declaring fountain-callback, brokered by fountain with scope owner, passes, beside a fountain capability a lobby brokers", async () => {
225
+ const root = repo({
226
+ "chant.workspace.json": declaration({
227
+ capabilities: [...BROKERED.capabilities, { name: "fountain", broker: "lobby", scope: ["agent", "conversations"] }, CALLBACK],
228
+ }),
229
+ "spec/box.ts": fountainBox,
230
+ });
231
+ expect(await found(root)).toEqual([]);
232
+ });
233
+
234
+ test("fountain-callback behind another broker, or without owner in its scope, fails", async () => {
235
+ const lobby = repo({
236
+ "chant.workspace.json": declaration({ capabilities: [{ name: "fountain-callback", broker: "lobby", scope: ["conversations"] }] }),
237
+ "spec/box.ts": fountainBox,
238
+ });
239
+ const d = await found(lobby);
240
+ expect(d.map((x) => [x.ruleId, x.file])).toEqual([["WSP125", "spec/box.ts"]]);
241
+ expect(d[0].message).toContain("names the broker lobby, but fountain hands this token to the sandbox itself");
242
+ const narrow = repo({
243
+ "chant.workspace.json": declaration({ capabilities: [{ name: "fountain-callback", broker: "fountain", scope: ["conversations"] }] }),
244
+ "spec/box.ts": fountainBox,
245
+ });
246
+ expect((await found(narrow))[0].message).toContain("has scope [conversations], not owner");
247
+ });
248
+
249
+ test("a box member that builds no fountain Box, and a member with no box block, are not flagged", async () => {
250
+ const other = repo({ "chant.workspace.json": declaration(BROKERED), "spec/box.ts": spec("${ANTHROPIC_API_KEY}") });
251
+ expect(await found(other)).toEqual([]);
252
+ const plain = repo({
253
+ "chant.workspace.json": JSON.stringify({ name: "acme", schema: 1, members: [{ name: "spec", dir: "spec", kind: "other", because: "x" }] }),
254
+ "spec/box.ts": fountainBox,
255
+ });
256
+ expect(await found(plain)).toEqual([]);
257
+ });
258
+
259
+ test("it can be turned down while fountain has no way to drop the token (managoat/fountain#2497)", async () => {
260
+ const root = repo({
261
+ "chant.workspace.json": declaration(BROKERED, { suppress: [{ check: "WSP125", because: "waiting on fountain" }] }),
262
+ "spec/box.ts": fountainBox,
263
+ });
264
+ expect((await found(root)).filter((x) => x.ruleId === "WSP125")).toEqual([]);
265
+ });
266
+
267
+ test("fountainBoxCall finds the composite imported by name, aliased or through a namespace, and nothing else", () => {
268
+ const at = (text: string) => fountainBoxCall(text, "box.ts", ts.ScriptKind.TS);
269
+ expect(at(fountainBox)).toEqual({ line: 4, column: 39 });
270
+ expect(at(`import { Box as FBox } from "@intentius/chant-lexicon-fountain";\nFBox({});\n`)).toEqual({ line: 2, column: 1 });
271
+ expect(at(`import * as fountain from "@intentius/chant-lexicon-fountain";\nfountain.Box({});\n`)).toEqual({ line: 2, column: 1 });
272
+ // Imported but never called, another package's Box, or a Box of one's own.
273
+ expect(at(`import { Box } from "@intentius/chant-lexicon-fountain";\nexport { Box };\n`)).toBeUndefined();
274
+ expect(at(`import { Box } from "@intentius/chant-lexicon-chaff";\nBox({});\n`)).toBeUndefined();
275
+ expect(at(`const Box = (o: object) => o;\nBox({}); // @intentius/chant-lexicon-fountain\n`)).toBeUndefined();
276
+ });
277
+ });
@@ -26,20 +26,114 @@
26
26
  *
27
27
  * WSP123 and WSP124, box isolation (#2727), are in `./box-isolation.ts` and
28
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
+ * WSP126 (`box-intent-unknown`) and WSP127 (`box-intent-unconstrained`), #2850,
42
+ * read the decision record a box block names as its intent (`box-intent.ts`).
43
+ * WSP126 fails when no record of a declared kind named decision has the id.
44
+ * WSP127 warns when the record's constrains names no member or path of this
45
+ * workspace at all: no `member:` entry for a declared member and no `path:`
46
+ * entry at, above or inside one's directory. It need not reach the box's own
47
+ * member: the decision an intent names can constrain the member whose app the
48
+ * box runs, on a workspace where the box block sits on a different member,
49
+ * the box's steward (studio's template, #2857). Both read the working
50
+ * tree's records, so neither runs under `--at`.
29
51
  */
30
52
 
31
53
  import * as ts from "typescript";
32
54
  import type { WorkspaceCheck, WorkspaceCheckContext, WorkspaceDiagnostic } from "../checks";
33
55
  import type { Member } from "../declaration";
56
+ import { constrainsWorkspace } from "../box-intent";
34
57
  import type { ReasonCode } from "../reason-codes";
35
58
  import { joinPath, skippedDir, type WorkspaceTree } from "../tree";
36
59
  import { BOX_ISOLATION_CHECKS } from "./box-isolation";
37
60
 
38
61
  /** The read contract's codes for the box findings, carried as `code` on each. */
39
- export const BOX_FINDING_CODES = ["box-credential-declared", "box-capability-unbrokered", "box-isolation-collision", "box-isolation-literal"] as const satisfies readonly ReasonCode[];
62
+ export const BOX_FINDING_CODES = [
63
+ "box-credential-declared",
64
+ "box-capability-unbrokered",
65
+ "box-isolation-collision",
66
+ "box-isolation-literal",
67
+ "box-fountain-callback-undeclared",
68
+ "box-intent-unknown",
69
+ "box-intent-unconstrained",
70
+ ] as const satisfies readonly ReasonCode[];
40
71
 
41
72
  export const WSP_BOX_CREDENTIAL = "WSP121";
42
73
  export const WSP_BOX_UNBROKERED = "WSP122";
74
+ export const WSP_BOX_FOUNTAIN_CALLBACK = "WSP125";
75
+ export const WSP_BOX_INTENT_UNKNOWN = "WSP126";
76
+ export const WSP_BOX_INTENT_UNCONSTRAINED = "WSP127";
77
+
78
+ /** The fountain lexicon's package, whose `Box` composite runs a persistent sandbox. */
79
+ const FOUNTAIN_LEXICON = "@intentius/chant-lexicon-fountain";
80
+
81
+ /**
82
+ * The capability a box block declares for fountain's callback token (#2780).
83
+ * The fountain lexicon exports the same value as `BOX_FOUNTAIN_CALLBACK_CAPABILITY`.
84
+ */
85
+ export const FOUNTAIN_CALLBACK_CAPABILITY = { name: "fountain-callback", broker: "fountain", scope: ["owner"] } as const;
86
+
87
+ /** Where a file calls the fountain lexicon's `Box`, 1-based. */
88
+ export interface FountainBoxCall {
89
+ line: number;
90
+ column: number;
91
+ }
92
+
93
+ /**
94
+ * The first call of the fountain lexicon's `Box` in a TypeScript or
95
+ * JavaScript file, read as syntax and never run: `Box` imported by name
96
+ * (aliased or not) and called, or called through a namespace import. Any
97
+ * other `Box` is not the composite.
98
+ */
99
+ export function fountainBoxCall(text: string, file: string, kind: ts.ScriptKind): FountainBoxCall | undefined {
100
+ if (!text.includes(FOUNTAIN_LEXICON)) return undefined;
101
+ const source = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, kind);
102
+ const names = new Set<string>();
103
+ const namespaces = new Set<string>();
104
+ for (const stmt of source.statements) {
105
+ if (!ts.isImportDeclaration(stmt) || !ts.isStringLiteral(stmt.moduleSpecifier)) continue;
106
+ const from = stmt.moduleSpecifier.text;
107
+ if (from !== FOUNTAIN_LEXICON && !from.startsWith(`${FOUNTAIN_LEXICON}/`)) continue;
108
+ const bindings = stmt.importClause?.namedBindings;
109
+ if (!bindings) continue;
110
+ if (ts.isNamespaceImport(bindings)) namespaces.add(bindings.name.text);
111
+ else for (const el of bindings.elements) if ((el.propertyName ?? el.name).text === "Box") names.add(el.name.text);
112
+ }
113
+ if (names.size === 0 && namespaces.size === 0) return undefined;
114
+ let found: ts.Node | undefined;
115
+ const visit = (node: ts.Node) => {
116
+ if (found) return;
117
+ if (ts.isCallExpression(node)) {
118
+ const callee = node.expression;
119
+ if (
120
+ (ts.isIdentifier(callee) && names.has(callee.text)) ||
121
+ (ts.isPropertyAccessExpression(callee) &&
122
+ callee.name.text === "Box" &&
123
+ ts.isIdentifier(callee.expression) &&
124
+ namespaces.has(callee.expression.text))
125
+ ) {
126
+ found = node;
127
+ return;
128
+ }
129
+ }
130
+ ts.forEachChild(node, visit);
131
+ };
132
+ visit(source);
133
+ if (!found) return undefined;
134
+ const { line, character } = source.getLineAndCharacterOfPosition(found.getStart(source));
135
+ return { line: line + 1, column: character + 1 };
136
+ }
43
137
 
44
138
  /** Values whose shape alone says they are a credential. The list FTN001 uses, plus Anthropic keys. */
45
139
  export const CREDENTIAL_SHAPES: readonly { pattern: RegExp; label: string }[] = [
@@ -304,4 +398,111 @@ export const BOX_CHECKS: readonly WorkspaceCheck[] = [
304
398
  },
305
399
  // Box isolation (#2727), WSP123 and WSP124.
306
400
  ...BOX_ISOLATION_CHECKS,
401
+ {
402
+ id: WSP_BOX_FOUNTAIN_CALLBACK,
403
+ name: "box-fountain-callback-undeclared",
404
+ description:
405
+ "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.",
406
+ severity: "error",
407
+ configurable: true,
408
+ check(ctx) {
409
+ const out: WorkspaceDiagnostic[] = [];
410
+ const want = FOUNTAIN_CALLBACK_CAPABILITY;
411
+ const expected = `{ "name": "${want.name}", "broker": "${want.broker}", "scope": ["${want.scope[0]}"] }`;
412
+ for (const m of boxMembers(ctx)) {
413
+ const dir = m.dir === "." ? "" : m.dir;
414
+ if (ctx.tree.stat(dir) !== "dir") continue;
415
+ let at: { path: string; call: FountainBoxCall } | undefined;
416
+ for (const path of filesUnder(ctx.tree, dir)) {
417
+ const kind = CODE_EXTENSIONS[extension(path)];
418
+ if (kind === undefined || kind === ts.ScriptKind.JSON) continue;
419
+ let text: string;
420
+ try {
421
+ text = ctx.tree.read(path);
422
+ } catch {
423
+ continue;
424
+ }
425
+ if (text.length > MAX_FILE_BYTES) continue;
426
+ const call = fountainBoxCall(text, path, kind);
427
+ if (call) {
428
+ at = { path, call };
429
+ break;
430
+ }
431
+ }
432
+ if (!at) continue;
433
+ const declared = m.box!.capabilities.find((c) => c.name === want.name);
434
+ if (declared && declared.broker === want.broker && declared.scope.includes(want.scope[0])) continue;
435
+ const why = !declared
436
+ ? "the member's box block does not declare it"
437
+ : declared.broker !== want.broker
438
+ ? `the box block's ${want.name} capability names the broker ${declared.broker ?? "(none)"}, but fountain hands this token to the sandbox itself`
439
+ : `the box block's ${want.name} capability has scope [${declared.scope.join(", ")}], not ${want.scope[0]}`;
440
+ out.push({
441
+ checkId: this.id,
442
+ severity: this.severity,
443
+ code: "box-fountain-callback-undeclared",
444
+ message:
445
+ `box-fountain-callback-undeclared: member ${m.name} builds a fountain Box (${at.path}), and fountain v0.21.0 gives a persistent box's sandbox ` +
446
+ `a callback token scoped to its owner (FOUNTAIN_TOKEN); ${why}. Declare ${expected} in the box block ` +
447
+ `(managoat/fountain#2497 tracks running without it)`,
448
+ entity: m.name,
449
+ pointer: declared?.pointer ?? m.box!.pointer,
450
+ treeFile: at.path,
451
+ line: at.call.line,
452
+ column: at.call.column,
453
+ });
454
+ }
455
+ return out;
456
+ },
457
+ },
458
+ {
459
+ id: WSP_BOX_INTENT_UNKNOWN,
460
+ name: "box-intent-unknown",
461
+ description: "The intent a box block names is the id of a decision record: a record of a declared kind named decision.",
462
+ severity: "error",
463
+ configurable: true,
464
+ check(ctx) {
465
+ const out: WorkspaceDiagnostic[] = [];
466
+ for (const i of ctx.facts?.boxIntents ?? []) {
467
+ if (i.record) continue;
468
+ out.push({
469
+ checkId: this.id,
470
+ severity: this.severity,
471
+ code: "box-intent-unknown",
472
+ message: `box-intent-unknown: member ${i.member}'s box names the intent ${i.id}, and ${i.why}; propose the decision with chant workspace records new, or fix the id`,
473
+ entity: i.member,
474
+ pointer: i.pointer,
475
+ });
476
+ }
477
+ return out;
478
+ },
479
+ },
480
+ {
481
+ id: WSP_BOX_INTENT_UNCONSTRAINED,
482
+ name: "box-intent-unconstrained",
483
+ description:
484
+ "The decision record a box names as its intent constrains a member or path of this workspace: member:<a declared member's name>, or a path: entry at, above or inside a declared member's directory. It need not be the box's own member: a box one member runs can be what a decision about another member constrains.",
485
+ severity: "warning",
486
+ configurable: true,
487
+ check(ctx) {
488
+ const out: WorkspaceDiagnostic[] = [];
489
+ const members = ctx.declaration.members;
490
+ for (const i of ctx.facts?.boxIntents ?? []) {
491
+ if (!i.record) continue;
492
+ if (i.record.constrains.some((c) => constrainsWorkspace(c, members))) continue;
493
+ out.push({
494
+ checkId: this.id,
495
+ severity: this.severity,
496
+ code: "box-intent-unconstrained",
497
+ message:
498
+ `box-intent-unconstrained: member ${i.member}'s box names the intent ${i.id} (${i.record.path}), whose constrains ` +
499
+ (i.record.constrains.length === 0 ? "is empty" : `names ${i.record.constrains.join(", ")}`) +
500
+ `, and none of it is a member or path of this workspace; add member:<name> for the member the intent is about, or a path: entry at, above or inside a member's directory`,
501
+ entity: i.member,
502
+ pointer: i.pointer,
503
+ });
504
+ }
505
+ return out;
506
+ },
507
+ },
307
508
  ];