@intentius/chant 0.79.0 → 0.81.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 (462) hide show
  1. package/dist/audit/core.d.ts.map +1 -1
  2. package/dist/audit/discover.d.ts +30 -0
  3. package/dist/audit/discover.d.ts.map +1 -1
  4. package/dist/audit/report-html.d.ts +5 -0
  5. package/dist/audit/report-html.d.ts.map +1 -1
  6. package/dist/audit/report-model.d.ts +13 -0
  7. package/dist/audit/report-model.d.ts.map +1 -1
  8. package/dist/audit/terraform-state.d.ts +13 -0
  9. package/dist/audit/terraform-state.d.ts.map +1 -1
  10. package/dist/cli/command-group.d.ts +6 -0
  11. package/dist/cli/command-group.d.ts.map +1 -1
  12. package/dist/cli/commands/audit.d.ts +44 -3
  13. package/dist/cli/commands/audit.d.ts.map +1 -1
  14. package/dist/cli/commands/check-lexicon-examples.d.ts.map +1 -1
  15. package/dist/cli/commands/check-lexicon.d.ts +8 -0
  16. package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
  17. package/dist/cli/commands/doctor.d.ts.map +1 -1
  18. package/dist/cli/commands/import-agents.d.ts.map +1 -1
  19. package/dist/cli/commands/init.d.ts +6 -0
  20. package/dist/cli/commands/init.d.ts.map +1 -1
  21. package/dist/cli/commands/lint.d.ts.map +1 -1
  22. package/dist/cli/commands/onboard.d.ts.map +1 -1
  23. package/dist/cli/commands/update.d.ts +6 -0
  24. package/dist/cli/commands/update.d.ts.map +1 -1
  25. package/dist/cli/commands/vendor.d.ts +5 -0
  26. package/dist/cli/commands/vendor.d.ts.map +1 -1
  27. package/dist/cli/conflict-check.d.ts +6 -0
  28. package/dist/cli/conflict-check.d.ts.map +1 -1
  29. package/dist/cli/handlers/build.d.ts.map +1 -1
  30. package/dist/cli/handlers/components.d.ts.map +1 -1
  31. package/dist/cli/handlers/dev.d.ts.map +1 -1
  32. package/dist/cli/handlers/fan-out.d.ts.map +1 -1
  33. package/dist/cli/handlers/init.d.ts.map +1 -1
  34. package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
  35. package/dist/cli/handlers/misc.d.ts.map +1 -1
  36. package/dist/cli/handlers/operator.d.ts +7 -0
  37. package/dist/cli/handlers/operator.d.ts.map +1 -1
  38. package/dist/cli/handlers/promote.d.ts +20 -0
  39. package/dist/cli/handlers/promote.d.ts.map +1 -0
  40. package/dist/cli/handlers/run-generate.d.ts +39 -0
  41. package/dist/cli/handlers/run-generate.d.ts.map +1 -0
  42. package/dist/cli/handlers/run.d.ts +4 -1
  43. package/dist/cli/handlers/run.d.ts.map +1 -1
  44. package/dist/cli/handlers/vendor.d.ts +8 -3
  45. package/dist/cli/handlers/vendor.d.ts.map +1 -1
  46. package/dist/cli/main.d.ts +12 -0
  47. package/dist/cli/main.d.ts.map +1 -1
  48. package/dist/cli/mcp/op-tools.d.ts.map +1 -1
  49. package/dist/cli/plugins.d.ts +14 -0
  50. package/dist/cli/plugins.d.ts.map +1 -1
  51. package/dist/cli/registry.d.ts +46 -1
  52. package/dist/cli/registry.d.ts.map +1 -1
  53. package/dist/codegen/fetch.d.ts +10 -0
  54. package/dist/codegen/fetch.d.ts.map +1 -1
  55. package/dist/components/capability-plugin-loader.d.ts.map +1 -1
  56. package/dist/components/cli-support.d.ts +3 -1
  57. package/dist/components/cli-support.d.ts.map +1 -1
  58. package/dist/components/discover.d.ts.map +1 -1
  59. package/dist/components/driver.d.ts +16 -2
  60. package/dist/components/driver.d.ts.map +1 -1
  61. package/dist/components/fan-out-support.d.ts.map +1 -1
  62. package/dist/components/gate-plan.d.ts +40 -0
  63. package/dist/components/gate-plan.d.ts.map +1 -0
  64. package/dist/components/pilots/alb-ecs.pilot.d.ts +7 -1
  65. package/dist/components/pilots/alb-ecs.pilot.d.ts.map +1 -1
  66. package/dist/components/presets/ecs-fargate.d.ts +10 -4
  67. package/dist/components/presets/ecs-fargate.d.ts.map +1 -1
  68. package/dist/components/promote.d.ts +264 -0
  69. package/dist/components/promote.d.ts.map +1 -0
  70. package/dist/components/sandbox/driver.d.ts.map +1 -1
  71. package/dist/components/sandbox/run.d.ts.map +1 -1
  72. package/dist/components/verbs/build-archive.d.ts +22 -11
  73. package/dist/components/verbs/build-archive.d.ts.map +1 -1
  74. package/dist/components/verbs/run-agent.d.ts.map +1 -1
  75. package/dist/config-static.d.ts +59 -0
  76. package/dist/config-static.d.ts.map +1 -0
  77. package/dist/config.d.ts +104 -5
  78. package/dist/config.d.ts.map +1 -1
  79. package/dist/content-digest.d.ts +17 -0
  80. package/dist/content-digest.d.ts.map +1 -0
  81. package/dist/discovery/files.d.ts +48 -4
  82. package/dist/discovery/files.d.ts.map +1 -1
  83. package/dist/discovery/fold-import.d.ts +32 -1
  84. package/dist/discovery/fold-import.d.ts.map +1 -1
  85. package/dist/discovery/index.d.ts.map +1 -1
  86. package/dist/discovery/sandbox/driver.d.ts +20 -0
  87. package/dist/discovery/sandbox/driver.d.ts.map +1 -1
  88. package/dist/discovery/sandbox/fork.d.ts +20 -0
  89. package/dist/discovery/sandbox/fork.d.ts.map +1 -1
  90. package/dist/discovery/walk.d.ts +119 -0
  91. package/dist/discovery/walk.d.ts.map +1 -0
  92. package/dist/graph-ir.d.ts +17 -1
  93. package/dist/graph-ir.d.ts.map +1 -1
  94. package/dist/join-key.d.ts +31 -0
  95. package/dist/join-key.d.ts.map +1 -0
  96. package/dist/lexicon-module.d.ts +110 -0
  97. package/dist/lexicon-module.d.ts.map +1 -0
  98. package/dist/lexicon.d.ts +82 -1
  99. package/dist/lexicon.d.ts.map +1 -1
  100. package/dist/lifecycle/build-ledger-store.d.ts.map +1 -1
  101. package/dist/lifecycle/digest.d.ts +5 -1
  102. package/dist/lifecycle/digest.d.ts.map +1 -1
  103. package/dist/lifecycle/gate-ledger.d.ts +20 -5
  104. package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
  105. package/dist/lifecycle/git.d.ts +64 -0
  106. package/dist/lifecycle/git.d.ts.map +1 -1
  107. package/dist/lifecycle/lease.d.ts +8 -1
  108. package/dist/lifecycle/lease.d.ts.map +1 -1
  109. package/dist/lifecycle/legacy-digest.d.ts +59 -0
  110. package/dist/lifecycle/legacy-digest.d.ts.map +1 -0
  111. package/dist/lifecycle/member-ledger.d.ts +67 -0
  112. package/dist/lifecycle/member-ledger.d.ts.map +1 -0
  113. package/dist/lifecycle/plan-digest.d.ts +4 -3
  114. package/dist/lifecycle/plan-digest.d.ts.map +1 -1
  115. package/dist/lifecycle/release-ledger.d.ts +42 -1
  116. package/dist/lifecycle/release-ledger.d.ts.map +1 -1
  117. package/dist/lifecycle/status.d.ts +90 -5
  118. package/dist/lifecycle/status.d.ts.map +1 -1
  119. package/dist/lint/rules/comp/comp003-mutating-no-rollback.d.ts +2 -3
  120. package/dist/lint/rules/comp/comp003-mutating-no-rollback.d.ts.map +1 -1
  121. package/dist/op/activities/activity-contracts.d.ts +32 -0
  122. package/dist/op/activities/activity-contracts.d.ts.map +1 -1
  123. package/dist/op/activities/index.d.ts +2 -0
  124. package/dist/op/activities/index.d.ts.map +1 -1
  125. package/dist/op/activities/propose-upgrade.d.ts +75 -0
  126. package/dist/op/activities/propose-upgrade.d.ts.map +1 -0
  127. package/dist/op/activity-contract-registry.d.ts.map +1 -1
  128. package/dist/op/activity-registry.d.ts.map +1 -1
  129. package/dist/op/discover.d.ts.map +1 -1
  130. package/dist/op/gate-approval.d.ts.map +1 -1
  131. package/dist/op/gate-name.d.ts +7 -0
  132. package/dist/op/gate-name.d.ts.map +1 -1
  133. package/dist/op/gate-summary.d.ts +16 -0
  134. package/dist/op/gate-summary.d.ts.map +1 -1
  135. package/dist/op/gate.d.ts +16 -2
  136. package/dist/op/gate.d.ts.map +1 -1
  137. package/dist/op/generate-pipeline.d.ts.map +1 -1
  138. package/dist/op/runtimes/local.d.ts.map +1 -1
  139. package/dist/project-root.d.ts +26 -0
  140. package/dist/project-root.d.ts.map +1 -1
  141. package/dist/workspace/__fixtures__/contract-repo.d.ts +37 -0
  142. package/dist/workspace/__fixtures__/contract-repo.d.ts.map +1 -0
  143. package/dist/workspace/__fixtures__/two-member-workspace.d.ts +11 -0
  144. package/dist/workspace/__fixtures__/two-member-workspace.d.ts.map +1 -0
  145. package/dist/workspace/checks/generated.d.ts +156 -0
  146. package/dist/workspace/checks/generated.d.ts.map +1 -0
  147. package/dist/workspace/checks/ledgers.d.ts +78 -0
  148. package/dist/workspace/checks/ledgers.d.ts.map +1 -0
  149. package/dist/workspace/checks/links.d.ts +25 -0
  150. package/dist/workspace/checks/links.d.ts.map +1 -0
  151. package/dist/workspace/checks/pipelines.d.ts +76 -0
  152. package/dist/workspace/checks/pipelines.d.ts.map +1 -0
  153. package/dist/workspace/checks.d.ts +161 -0
  154. package/dist/workspace/checks.d.ts.map +1 -0
  155. package/dist/workspace/compose-graph.d.ts +115 -0
  156. package/dist/workspace/compose-graph.d.ts.map +1 -0
  157. package/dist/workspace/compose-reports.d.ts +68 -0
  158. package/dist/workspace/compose-reports.d.ts.map +1 -0
  159. package/dist/workspace/declaration.d.ts +220 -0
  160. package/dist/workspace/declaration.d.ts.map +1 -0
  161. package/dist/workspace/declaration.schema.json +434 -0
  162. package/dist/workspace/generated-files.d.ts +65 -0
  163. package/dist/workspace/generated-files.d.ts.map +1 -0
  164. package/dist/workspace/glob.d.ts +13 -0
  165. package/dist/workspace/glob.d.ts.map +1 -0
  166. package/dist/workspace/graph-cli.d.ts +75 -0
  167. package/dist/workspace/graph-cli.d.ts.map +1 -0
  168. package/dist/workspace/init.d.ts +96 -0
  169. package/dist/workspace/init.d.ts.map +1 -0
  170. package/dist/workspace/jsonc.d.ts +38 -0
  171. package/dist/workspace/jsonc.d.ts.map +1 -0
  172. package/dist/workspace/kinds.d.ts +192 -0
  173. package/dist/workspace/kinds.d.ts.map +1 -0
  174. package/dist/workspace/lineage-check.d.ts +84 -0
  175. package/dist/workspace/lineage-check.d.ts.map +1 -0
  176. package/dist/workspace/lineage-cli.d.ts +31 -0
  177. package/dist/workspace/lineage-cli.d.ts.map +1 -0
  178. package/dist/workspace/lineage-init.d.ts +101 -0
  179. package/dist/workspace/lineage-init.d.ts.map +1 -0
  180. package/dist/workspace/lineage-lock.d.ts +258 -0
  181. package/dist/workspace/lineage-lock.d.ts.map +1 -0
  182. package/dist/workspace/lineage-merge.d.ts +14 -0
  183. package/dist/workspace/lineage-merge.d.ts.map +1 -0
  184. package/dist/workspace/lineage-migrations.d.ts +178 -0
  185. package/dist/workspace/lineage-migrations.d.ts.map +1 -0
  186. package/dist/workspace/lineage-update.d.ts +59 -0
  187. package/dist/workspace/lineage-update.d.ts.map +1 -0
  188. package/dist/workspace/lineage-upgrade-cli.d.ts +40 -0
  189. package/dist/workspace/lineage-upgrade-cli.d.ts.map +1 -0
  190. package/dist/workspace/lineage-upgrade.d.ts +146 -0
  191. package/dist/workspace/lineage-upgrade.d.ts.map +1 -0
  192. package/dist/workspace/lineage-vendor.d.ts +63 -0
  193. package/dist/workspace/lineage-vendor.d.ts.map +1 -0
  194. package/dist/workspace/lineage-version.d.ts +32 -0
  195. package/dist/workspace/lineage-version.d.ts.map +1 -0
  196. package/dist/workspace/links.d.ts +150 -0
  197. package/dist/workspace/links.d.ts.map +1 -0
  198. package/dist/workspace/ls.d.ts +99 -0
  199. package/dist/workspace/ls.d.ts.map +1 -0
  200. package/dist/workspace/member-commands.d.ts +139 -0
  201. package/dist/workspace/member-commands.d.ts.map +1 -0
  202. package/dist/workspace/member-handles.d.ts +54 -0
  203. package/dist/workspace/member-handles.d.ts.map +1 -0
  204. package/dist/workspace/member-pipeline.d.ts +143 -0
  205. package/dist/workspace/member-pipeline.d.ts.map +1 -0
  206. package/dist/workspace/member-run.d.ts +67 -0
  207. package/dist/workspace/member-run.d.ts.map +1 -0
  208. package/dist/workspace/reason-codes.d.ts +65 -0
  209. package/dist/workspace/reason-codes.d.ts.map +1 -0
  210. package/dist/workspace/record-source.d.ts +22 -0
  211. package/dist/workspace/record-source.d.ts.map +1 -0
  212. package/dist/workspace/records-cli.d.ts +81 -0
  213. package/dist/workspace/records-cli.d.ts.map +1 -0
  214. package/dist/workspace/records.d.ts +118 -0
  215. package/dist/workspace/records.d.ts.map +1 -0
  216. package/dist/workspace/root-refusal.d.ts +53 -0
  217. package/dist/workspace/root-refusal.d.ts.map +1 -0
  218. package/dist/workspace/status.d.ts +150 -0
  219. package/dist/workspace/status.d.ts.map +1 -0
  220. package/dist/workspace/template-manifest.d.ts +90 -0
  221. package/dist/workspace/template-manifest.d.ts.map +1 -0
  222. package/dist/workspace/tree.d.ts +36 -0
  223. package/dist/workspace/tree.d.ts.map +1 -0
  224. package/dist/workspace/trust/attestor.d.ts +67 -0
  225. package/dist/workspace/trust/attestor.d.ts.map +1 -0
  226. package/dist/workspace/trust/policy.d.ts +111 -0
  227. package/dist/workspace/trust/policy.d.ts.map +1 -0
  228. package/dist/workspace/trust/provenance.d.ts +67 -0
  229. package/dist/workspace/trust/provenance.d.ts.map +1 -0
  230. package/dist/workspace/trust/ssh-commit.d.ts +43 -0
  231. package/dist/workspace/trust/ssh-commit.d.ts.map +1 -0
  232. package/dist/workspace/trust/test-repo.d.ts +42 -0
  233. package/dist/workspace/trust/test-repo.d.ts.map +1 -0
  234. package/dist/workspace/trust/verify-cli.d.ts +9 -0
  235. package/dist/workspace/trust/verify-cli.d.ts.map +1 -0
  236. package/dist/workspace/trust/verify.d.ts +66 -0
  237. package/dist/workspace/trust/verify.d.ts.map +1 -0
  238. package/dist/workspace/which-chant.d.ts +58 -0
  239. package/dist/workspace/which-chant.d.ts.map +1 -0
  240. package/dist/workspace/workspace-kinds.schema.json +133 -0
  241. package/package.json +4 -3
  242. package/src/audit/core.ts +13 -2
  243. package/src/audit/discover.ts +102 -36
  244. package/src/audit/report-html.ts +3 -1
  245. package/src/audit/report-model.ts +14 -1
  246. package/src/audit/terraform-state.test.ts +81 -1
  247. package/src/audit/terraform-state.ts +25 -0
  248. package/src/cli/command-group.ts +7 -0
  249. package/src/cli/commands/audit-truncation.test.ts +167 -0
  250. package/src/cli/commands/audit.ts +132 -29
  251. package/src/cli/commands/check-lexicon-examples.ts +2 -1
  252. package/src/cli/commands/check-lexicon.ts +24 -0
  253. package/src/cli/commands/doctor.test.ts +30 -0
  254. package/src/cli/commands/doctor.ts +21 -0
  255. package/src/cli/commands/import-agents.ts +15 -4
  256. package/src/cli/commands/init.ts +76 -25
  257. package/src/cli/commands/lint.test.ts +52 -1
  258. package/src/cli/commands/lint.ts +41 -71
  259. package/src/cli/commands/onboard.ts +15 -0
  260. package/src/cli/commands/update.ts +18 -5
  261. package/src/cli/commands/vendor.ts +11 -6
  262. package/src/cli/conflict-check.test.ts +18 -1
  263. package/src/cli/conflict-check.ts +13 -0
  264. package/src/cli/discovery-skip.test.ts +168 -0
  265. package/src/cli/handlers/__snapshots__/build-member.test.ts.snap +38 -0
  266. package/src/cli/handlers/build-member.test.ts +127 -0
  267. package/src/cli/handlers/build.test.ts +11 -0
  268. package/src/cli/handlers/build.ts +35 -1
  269. package/src/cli/handlers/components.test.ts +127 -0
  270. package/src/cli/handlers/components.ts +70 -8
  271. package/src/cli/handlers/dev.ts +6 -1
  272. package/src/cli/handlers/fan-out.ts +9 -2
  273. package/src/cli/handlers/graph.test.ts +20 -0
  274. package/src/cli/handlers/graph.ts +4 -2
  275. package/src/cli/handlers/init.ts +54 -1
  276. package/src/cli/handlers/lifecycle.ts +2 -1
  277. package/src/cli/handlers/misc.ts +16 -0
  278. package/src/cli/handlers/operator.test.ts +68 -0
  279. package/src/cli/handlers/operator.ts +39 -10
  280. package/src/cli/handlers/promote.test.ts +377 -0
  281. package/src/cli/handlers/promote.ts +391 -0
  282. package/src/cli/handlers/run-generate-member.test.ts +83 -0
  283. package/src/cli/handlers/run-generate.test.ts +158 -0
  284. package/src/cli/handlers/run-generate.ts +168 -0
  285. package/src/cli/handlers/run.test.ts +47 -0
  286. package/src/cli/handlers/run.ts +41 -6
  287. package/src/cli/handlers/vendor.ts +147 -43
  288. package/src/cli/main.test.ts +137 -1
  289. package/src/cli/main.ts +290 -24
  290. package/src/cli/mcp/op-tools.ts +2 -1
  291. package/src/cli/path-lexicon-messages.test.ts +152 -0
  292. package/src/cli/plugins.ts +43 -5
  293. package/src/cli/registry.ts +46 -1
  294. package/src/cli/static-config-read.test.ts +208 -0
  295. package/src/codegen/fetch.test.ts +63 -1
  296. package/src/codegen/fetch.ts +30 -1
  297. package/src/components/__fixtures__/alb-ecs-service.json +0 -8
  298. package/src/components/capability-plugin-loader.ts +3 -1
  299. package/src/components/cli-support.ts +9 -3
  300. package/src/components/discover.test.ts +27 -57
  301. package/src/components/discover.ts +20 -51
  302. package/src/components/driver.test.ts +90 -12
  303. package/src/components/driver.ts +43 -5
  304. package/src/components/fan-out-support.ts +2 -1
  305. package/src/components/gate-plan.ts +53 -0
  306. package/src/components/pilots/README.md +1 -1
  307. package/src/components/pilots/alb-ecs.pilot.ts +7 -7
  308. package/src/components/presets/ecs-fargate.ts +10 -5
  309. package/src/components/promote.test.ts +645 -0
  310. package/src/components/promote.ts +641 -0
  311. package/src/components/sandbox/driver.ts +8 -8
  312. package/src/components/sandbox/run.ts +4 -1
  313. package/src/components/verbs/build-archive.ts +27 -21
  314. package/src/components/verbs/run-agent.ts +3 -5
  315. package/src/config-static.test.ts +136 -0
  316. package/src/config-static.ts +232 -0
  317. package/src/config.test.ts +40 -0
  318. package/src/config.ts +161 -11
  319. package/src/content-digest.test.ts +120 -0
  320. package/src/content-digest.ts +21 -0
  321. package/src/discovery/files.test.ts +127 -1
  322. package/src/discovery/files.ts +91 -59
  323. package/src/discovery/fold-import.ts +127 -9
  324. package/src/discovery/index.ts +1 -0
  325. package/src/discovery/sandbox/driver.ts +73 -35
  326. package/src/discovery/sandbox/fork-diagnostic.test.ts +136 -13
  327. package/src/discovery/sandbox/fork.ts +83 -43
  328. package/src/discovery/sandbox/path-lexicon-multifile.test.ts +285 -0
  329. package/src/discovery/sandbox/path-lexicon-parity.test.ts +162 -0
  330. package/src/discovery/walk.test.ts +263 -0
  331. package/src/discovery/walk.ts +334 -0
  332. package/src/effect-receipt.ts +2 -2
  333. package/src/graph-ir.ts +18 -1
  334. package/src/join-key.test.ts +25 -0
  335. package/src/join-key.ts +39 -0
  336. package/src/lexicon-module.test.ts +203 -0
  337. package/src/lexicon-module.ts +215 -0
  338. package/src/lexicon.ts +105 -1
  339. package/src/lifecycle/build-ledger-store.ts +13 -2
  340. package/src/lifecycle/digest.ts +9 -5
  341. package/src/lifecycle/gate-ledger.test.ts +20 -0
  342. package/src/lifecycle/gate-ledger.ts +28 -6
  343. package/src/lifecycle/git.test.ts +13 -1
  344. package/src/lifecycle/git.ts +221 -176
  345. package/src/lifecycle/lease.ts +25 -10
  346. package/src/lifecycle/legacy-digest.test.ts +206 -0
  347. package/src/lifecycle/legacy-digest.ts +103 -0
  348. package/src/lifecycle/member-ledger.test.ts +319 -0
  349. package/src/lifecycle/member-ledger.ts +145 -0
  350. package/src/lifecycle/plan-digest.ts +7 -7
  351. package/src/lifecycle/release-ledger.test.ts +20 -0
  352. package/src/lifecycle/release-ledger.ts +53 -3
  353. package/src/lifecycle/status.test.ts +151 -1
  354. package/src/lifecycle/status.ts +228 -7
  355. package/src/lint/rules/comp/comp003-mutating-no-rollback.ts +2 -3
  356. package/src/op/activities/activity-contracts.ts +37 -0
  357. package/src/op/activities/index.ts +10 -0
  358. package/src/op/activities/propose-upgrade.ts +206 -0
  359. package/src/op/activity-contract-registry.ts +18 -4
  360. package/src/op/activity-registry.ts +26 -0
  361. package/src/op/discover.test.ts +14 -4
  362. package/src/op/discover.ts +19 -23
  363. package/src/op/gate-approval.ts +3 -1
  364. package/src/op/gate-name.ts +8 -0
  365. package/src/op/gate-summary.test.ts +14 -0
  366. package/src/op/gate-summary.ts +25 -2
  367. package/src/op/gate.ts +36 -7
  368. package/src/op/generate-pipeline.test.ts +14 -5
  369. package/src/op/generate-pipeline.ts +2 -1
  370. package/src/op/runtimes/local.ts +2 -1
  371. package/src/project-root.test.ts +66 -1
  372. package/src/project-root.ts +48 -0
  373. package/src/workspace/__fixtures__/contract-repo.ts +100 -0
  374. package/src/workspace/__fixtures__/two-member-workspace.ts +35 -0
  375. package/src/workspace/behold-kinds.test.ts +162 -0
  376. package/src/workspace/check-contract.test.ts +127 -0
  377. package/src/workspace/check.schema.json +347 -0
  378. package/src/workspace/checks/generated.test.ts +287 -0
  379. package/src/workspace/checks/generated.ts +475 -0
  380. package/src/workspace/checks/ledgers.test.ts +139 -0
  381. package/src/workspace/checks/ledgers.ts +246 -0
  382. package/src/workspace/checks/links.ts +183 -0
  383. package/src/workspace/checks/pipelines.test.ts +133 -0
  384. package/src/workspace/checks/pipelines.ts +225 -0
  385. package/src/workspace/checks-repo.test.ts +71 -0
  386. package/src/workspace/checks.test.ts +450 -0
  387. package/src/workspace/checks.ts +461 -0
  388. package/src/workspace/compose-graph.test.ts +146 -0
  389. package/src/workspace/compose-graph.ts +228 -0
  390. package/src/workspace/compose-reports.test.ts +109 -0
  391. package/src/workspace/compose-reports.ts +186 -0
  392. package/src/workspace/declaration.schema.json +434 -0
  393. package/src/workspace/declaration.test.ts +347 -0
  394. package/src/workspace/declaration.ts +623 -0
  395. package/src/workspace/generated-files.test.ts +114 -0
  396. package/src/workspace/generated-files.ts +94 -0
  397. package/src/workspace/glob.ts +49 -0
  398. package/src/workspace/graph-cli.ts +220 -0
  399. package/src/workspace/graph-contract.test.ts +204 -0
  400. package/src/workspace/graph.schema.json +662 -0
  401. package/src/workspace/init.test.ts +223 -0
  402. package/src/workspace/init.ts +585 -0
  403. package/src/workspace/jsonc.test.ts +65 -0
  404. package/src/workspace/jsonc.ts +231 -0
  405. package/src/workspace/kinds.test.ts +288 -0
  406. package/src/workspace/kinds.ts +506 -0
  407. package/src/workspace/lineage-check.ts +264 -0
  408. package/src/workspace/lineage-cli.ts +114 -0
  409. package/src/workspace/lineage-init.test.ts +231 -0
  410. package/src/workspace/lineage-init.ts +365 -0
  411. package/src/workspace/lineage-lock.test.ts +224 -0
  412. package/src/workspace/lineage-lock.ts +357 -0
  413. package/src/workspace/lineage-merge.ts +44 -0
  414. package/src/workspace/lineage-migrations.test.ts +178 -0
  415. package/src/workspace/lineage-migrations.ts +343 -0
  416. package/src/workspace/lineage-update.ts +160 -0
  417. package/src/workspace/lineage-upgrade-cli.ts +151 -0
  418. package/src/workspace/lineage-upgrade.test.ts +474 -0
  419. package/src/workspace/lineage-upgrade.ts +624 -0
  420. package/src/workspace/lineage-vendor.test.ts +126 -0
  421. package/src/workspace/lineage-vendor.ts +215 -0
  422. package/src/workspace/lineage-version.ts +127 -0
  423. package/src/workspace/links.test.ts +344 -0
  424. package/src/workspace/links.ts +331 -0
  425. package/src/workspace/ls-contract.test.ts +200 -0
  426. package/src/workspace/ls.schema.json +190 -0
  427. package/src/workspace/ls.ts +254 -0
  428. package/src/workspace/member-commands.e2e.test.ts +244 -0
  429. package/src/workspace/member-commands.test.ts +217 -0
  430. package/src/workspace/member-commands.ts +540 -0
  431. package/src/workspace/member-handles.test.ts +76 -0
  432. package/src/workspace/member-handles.ts +228 -0
  433. package/src/workspace/member-pipeline.test.ts +135 -0
  434. package/src/workspace/member-pipeline.ts +317 -0
  435. package/src/workspace/member-run.ts +146 -0
  436. package/src/workspace/read-contract.test.ts +129 -0
  437. package/src/workspace/reason-codes.test.ts +113 -0
  438. package/src/workspace/reason-codes.ts +81 -0
  439. package/src/workspace/record-source.ts +117 -0
  440. package/src/workspace/records-cli.ts +198 -0
  441. package/src/workspace/records-contract.test.ts +93 -0
  442. package/src/workspace/records.schema.json +150 -0
  443. package/src/workspace/records.test.ts +243 -0
  444. package/src/workspace/records.ts +384 -0
  445. package/src/workspace/root-refusal.ts +97 -0
  446. package/src/workspace/status-contract.test.ts +308 -0
  447. package/src/workspace/status.schema.json +210 -0
  448. package/src/workspace/status.ts +412 -0
  449. package/src/workspace/template-manifest.test.ts +79 -0
  450. package/src/workspace/template-manifest.ts +221 -0
  451. package/src/workspace/tree.ts +132 -0
  452. package/src/workspace/trust/attestor.ts +93 -0
  453. package/src/workspace/trust/policy.ts +306 -0
  454. package/src/workspace/trust/provenance.ts +181 -0
  455. package/src/workspace/trust/ssh-commit.ts +138 -0
  456. package/src/workspace/trust/test-repo.ts +138 -0
  457. package/src/workspace/trust/trust.test.ts +397 -0
  458. package/src/workspace/trust/verify-cli.ts +56 -0
  459. package/src/workspace/trust/verify.ts +194 -0
  460. package/src/workspace/which-chant.test.ts +117 -0
  461. package/src/workspace/which-chant.ts +151 -0
  462. package/src/workspace/workspace-kinds.schema.json +133 -0
@@ -1,9 +1,12 @@
1
- import { describe, test, expect } from "vitest";
2
- import { fork } from "node:child_process";
3
- import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
1
+ import { describe, test, expect, vi } from "vitest";
2
+ import { fork, type ChildProcess } from "node:child_process";
3
+ import { EventEmitter } from "node:events";
4
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync, realpathSync } from "node:fs";
4
5
  import { tmpdir } from "node:os";
5
6
  import { join } from "node:path";
6
7
  import { runFallbackFilesSandboxed } from "./run";
8
+ import { forkSandboxed, settleOnExitAfterChannelDrains } from "./fork";
9
+ import { generateConfigDriverSource, generateDriverSource, generatePolicyDriverSource, sendFunctionSource } from "./driver";
7
10
 
8
11
  /**
9
12
  * chant#2461 — a child that ends without a usable result says which way it did.
@@ -22,13 +25,13 @@ import { runFallbackFilesSandboxed } from "./run";
22
25
  * - a payload the parent's `isResponse` refuses, which used to be dropped in
23
26
  * silence and then reported as though nothing had been sent
24
27
  *
25
- * A third mechanism was proposed in the issue and is NOT covered, because it
26
- * was measured and does not happen: `process.send` racing the child's reap so
27
- * that `exit` is dispatched before an already-queued `message`. With the parent
28
- * blocked so both were certainly pending, `message` won 60 times out of 60. The
29
- * live IPC channel keeps the child alive until the payload flushes, and the
30
- * driver has no `process.exit()` to cut that short. The last test here records
31
- * that, so the disproof is not lost with the transcript.
28
+ * A third mechanism, the one the issue first proposed, was ruled out early on
29
+ * a test that blocked the parent (`message` won 60 of 60) and turned out to be
30
+ * the real one: under concurrent load the parent can handle a child's `exit`
31
+ * before the `message` it had already written. A stress run of the real
32
+ * `evaluateConfigSandboxed` lost 3 of 800 that way. The parent now waits for
33
+ * the IPC channel to drain before it reads an exit, and the drivers await
34
+ * their `process.send` callback; both are pinned below.
32
35
  */
33
36
  function runChild(source: string): Promise<{ code: number | null; message: unknown; stderr: string }> {
34
37
  const dir = mkdtempSync(join(tmpdir(), "chant-fork-diag-"));
@@ -62,9 +65,9 @@ describe("why a sandboxed child ends without a result (chant#2461)", () => {
62
65
  expect(r.stderr.trim()).toBe("");
63
66
  });
64
67
 
65
- test("a child that sends and falls off the end always gets its message through", async () => {
66
- // The disproof, kept as a test. If this ever fails, the race the issue
67
- // proposed is real after all and the diagnostic's wording needs revisiting.
68
+ test("a child that sends and falls off the end gets its message through", async () => {
69
+ // Usually true even on a bare `exit` handler; the drain wait is what makes
70
+ // it true every time. See the settleOnExitAfterChannelDrains tests below.
68
71
  const results = await Promise.all(
69
72
  Array.from({ length: 12 }, () => runChild("process.send({ ok: true });\n")),
70
73
  );
@@ -123,3 +126,123 @@ describe("why a sandboxed child ends without a result (chant#2461)", () => {
123
126
  }
124
127
  }, 120_000);
125
128
  });
129
+
130
+ /** A stand-in for a ChildProcess: just the events and the `connected` flag the helper reads. */
131
+ function fakeChild(connected: boolean): ChildProcess & EventEmitter {
132
+ const child = new EventEmitter() as ChildProcess & EventEmitter;
133
+ (child as { connected: boolean }).connected = connected;
134
+ return child;
135
+ }
136
+
137
+ describe("settleOnExitAfterChannelDrains (chant#2461)", () => {
138
+ test("an exit handled before the last message waits for the channel, so the message wins", () => {
139
+ // The observed ordering: exit first, the already-written message after,
140
+ // then the channel's EOF. Settling on exit alone dropped the message.
141
+ const child = fakeChild(true);
142
+ const order: string[] = [];
143
+ child.on("message", () => order.push("message"));
144
+ settleOnExitAfterChannelDrains(child, (code) => order.push(`exit:${code}`));
145
+
146
+ child.emit("exit", 0, null);
147
+ child.emit("message", { ok: true });
148
+ expect(order).toEqual(["message"]);
149
+
150
+ child.emit("disconnect");
151
+ expect(order).toEqual(["message", "exit:0"]);
152
+ });
153
+
154
+ test("a channel that has already closed settles on exit straight away", () => {
155
+ const child = fakeChild(false);
156
+ const onExit = vi.fn();
157
+ settleOnExitAfterChannelDrains(child, onExit);
158
+ child.emit("exit", 3, null);
159
+ expect(onExit).toHaveBeenCalledWith(3, null);
160
+ });
161
+
162
+ test("a channel that never reports closed is bounded by the grace period", () => {
163
+ vi.useFakeTimers();
164
+ try {
165
+ const child = fakeChild(true);
166
+ const onExit = vi.fn();
167
+ settleOnExitAfterChannelDrains(child, onExit, 50);
168
+ child.emit("exit", 0, null);
169
+ vi.advanceTimersByTime(49);
170
+ expect(onExit).not.toHaveBeenCalled();
171
+ vi.advanceTimersByTime(1);
172
+ expect(onExit).toHaveBeenCalledTimes(1);
173
+ child.emit("disconnect");
174
+ expect(onExit).toHaveBeenCalledTimes(1);
175
+ } finally {
176
+ vi.useRealTimers();
177
+ }
178
+ });
179
+
180
+ test("real sandboxed children that send and return all resolve, many at once", async () => {
181
+ // The shape of the stress harness, small enough for CI: a --permission
182
+ // child that sends and returns, through forkSandboxed's settle logic.
183
+ const dir = realpathSync(mkdtempSync(join(tmpdir(), "chant-2461-many-")));
184
+ try {
185
+ const file = join(dir, "child.mjs");
186
+ writeFileSync(file, 'process.send({ kind: "chant-config", ok: true, config: { pad: "x".repeat(65536) } });\n');
187
+ const isResponse = (v: unknown): v is { kind: string } =>
188
+ typeof v === "object" && v !== null && (v as { kind?: unknown }).kind === "chant-config";
189
+ const results = await Promise.all(
190
+ Array.from({ length: 32 }, () =>
191
+ forkSandboxed(
192
+ { bundlePath: file, bundleDir: dir, projectRealpath: dir, externalReadPaths: [], env: { PATH: process.env.PATH ?? "" }, timeoutMs: 60_000, label: "many", outputPrefix: "[t]" },
193
+ isResponse,
194
+ ),
195
+ ),
196
+ );
197
+ expect(results.every((r) => r.kind === "chant-config")).toBe(true);
198
+ } finally {
199
+ rmSync(dir, { recursive: true, force: true });
200
+ }
201
+ }, 120_000);
202
+ });
203
+
204
+ describe("driver send awaits delivery and fails loudly (chant#2461)", () => {
205
+ const harness = (body: string): string =>
206
+ ['import { writeSync } from "node:fs";', ...sendFunctionSource(), body].join("\n");
207
+
208
+ test("a send that succeeds resolves, and the code after it runs", async () => {
209
+ const r = await runChild(harness('await send({ ok: 1 }); writeSync(2, "after\\n");'));
210
+ expect(r.code).toBe(0);
211
+ expect(r.message).toEqual({ ok: 1 });
212
+ expect(r.stderr).toBe("after\n");
213
+ });
214
+
215
+ test("a send on a closed channel exits non-zero with the reason on stderr", async () => {
216
+ const r = await runChild(harness('process.disconnect(); await send({ ok: 1 }); writeSync(2, "unreachable\\n");'));
217
+ expect(r.code).toBe(1);
218
+ expect(r.message).toBeUndefined();
219
+ expect(r.stderr).toContain("could not hand its result to the parent");
220
+ expect(r.stderr).toContain("chant#2461");
221
+ expect(r.stderr).not.toContain("unreachable");
222
+ });
223
+
224
+ test("a payload that cannot be serialized exits non-zero with the reason on stderr", async () => {
225
+ const r = await runChild(harness("await send({ big: 1n });"));
226
+ expect(r.code).toBe(1);
227
+ expect(r.message).toBeUndefined();
228
+ expect(r.stderr).toContain("could not hand its result to the parent");
229
+ });
230
+
231
+ test("every driver awaits or returns every send", () => {
232
+ // A bare `send(...)` statement would let main() return before the write
233
+ // completes. The only unawaited call allowed is main().catch's, whose
234
+ // promise is the last thing the module does.
235
+ const sources = {
236
+ config: generateConfigDriverSource("/tmp/project/chant.config.ts"),
237
+ run: generateDriverSource({ files: ["/tmp/project/a.ts"], buildRoot: "/tmp/project" }),
238
+ policy: generatePolicyDriverSource(["/tmp/project/policy.ts"]),
239
+ };
240
+ for (const [name, source] of Object.entries(sources)) {
241
+ const bare = source
242
+ .split("\n")
243
+ .filter((line) => /^\s*(send|fail)\(/.test(line))
244
+ .filter((line) => !/^\s*send\(\{[^\n]*fatal: true|^\s*send\(\{ kind: "chant-config", ok: false, error: classifyChildError\([^)]*, err\)\.toJSON\(\) \}\),$/.test(line));
245
+ expect({ name, bare }).toEqual({ name, bare: [] });
246
+ }
247
+ });
248
+ });
@@ -1,4 +1,4 @@
1
- import { fork } from "node:child_process";
1
+ import { fork, type ChildProcess } from "node:child_process";
2
2
 
3
3
  /**
4
4
  * The one place chant starts a sandboxed child process.
@@ -131,38 +131,33 @@ function lineBuffered(emit: (line: string) => void) {
131
131
  * chant#2461 — this used to be one sentence, `child exited before reporting
132
132
  * results (code N, signal S)`, for three unrelated situations. The one that
133
133
  * was observed in the wild is the hardest to read: exit code 0, empty stderr,
134
- * no message. That reads like the child was cut off, and it was not — it ran
135
- * to completion and sent nothing.
134
+ * no message. That reads like the child was cut off, and it was not: it ran
135
+ * to completion.
136
136
  *
137
- * Two mechanisms produce it, and the driver decides which is possible.
137
+ * The observed case turned out to be the parent's fault. A config child that
138
+ * sent its result and exited could have its `exit` dispatched here BEFORE its
139
+ * already-written `message`, and the old `exit` handler settled on the spot.
140
+ * The payload arrived a moment later and was ignored. Stress runs of the real
141
+ * `evaluateConfigSandboxed`, 24 at a time under 16 CPU hogs, lost 3 of 800 and
142
+ * 13 of 1500 that way. An earlier test that blocked the parent had seen
143
+ * `message` win 60 of 60, which is why this was ruled out for a while.
144
+ * {@link settleOnExitAfterChannelDrains} closes it by not reading the exit
145
+ * until the IPC channel has drained. Every driver also awaits its
146
+ * `process.send` callback and exits 1 with a stderr line if the send fails
147
+ * (see `./driver.ts`'s `sendFunctionSource`), so a result that never left the
148
+ * child cannot look like exit 0 either.
138
149
  *
139
- * The first is an `await` that never settles. `main().catch(...)` catches a
140
- * REJECTION, and a promise that never settles is not one, so if nothing keeps
141
- * the loop alive Node drains it and exits 0 having sent nothing and written
142
- * nothing. Nothing else in the process reports that.
143
- *
144
- * On the RUN path that is not hypothetical, and it is reproducible: the driver
145
- * does `await import(<project file>)` for each run-fallback file, module scope
146
- * is arbitrary project source, and a file whose top level awaits something that
147
- * never settles makes that import never complete. A fixture doing exactly that
148
- * yields this error, which is how the wording here was checked rather than
149
- * guessed.
150
- *
151
- * The second is the payload being lost between `process.send` and exit.
152
- *
153
- * For the CONFIG driver the first is impossible, which is worth stating because
154
- * it was the working hypothesis until the bundle was read. Its `await
155
- * import(configPath)` bundles to `await Promise.resolve().then(() =>
156
- * (init_chant_config(), chant_config_exports))` — one microtask over
157
- * synchronous code — and the bundle has no runtime imports, no dynamic
158
- * `import(`, and exactly one `process.send`. There is nothing there to hang on.
159
- * So a config child that exits 0 with nothing sent DID send, and the payload
160
- * did not arrive.
161
- *
162
- * The exit/message race originally proposed in chant#2461 is a third thing and
163
- * is not it: with the parent blocked so a queued payload and the reap were both
164
- * pending, `message` was dispatched first 60 times out of 60, because the live
165
- * IPC channel keeps the child alive until the payload flushes.
150
+ * With both of those in place, exit 0 with nothing on stderr and nothing
151
+ * received has one known cause left: an `await` that never settles.
152
+ * `main().catch(...)` catches a REJECTION, and a promise that never settles is
153
+ * not one, so if nothing keeps the loop alive Node drains it and exits 0
154
+ * having sent nothing. On the RUN path that is reproducible: the driver does
155
+ * `await import(<project file>)` per run-fallback file, module scope is
156
+ * arbitrary project source, and a file whose top level never settles makes
157
+ * that import never complete (the run driver names the file on stderr in that
158
+ * case). The CONFIG driver bundles to one microtask over synchronous code with
159
+ * no runtime imports, so on that path the cause is not known, and the message
160
+ * says so rather than guessing.
166
161
  */
167
162
  function describeSilentExit(
168
163
  label: string,
@@ -190,21 +185,64 @@ function describeSilentExit(
190
185
  if (stderr) return `${head}: ${stderr}`;
191
186
  if (code !== 0 || signal !== null) return head;
192
187
 
193
- // Exit 0, nothing on stderr, nothing sent. The child finished normally and
194
- // the parent has nothing. Two mechanisms produce exactly this, and which one
195
- // it is depends on the driver — see this function's doc.
188
+ // Exit 0, nothing on stderr, and nothing received even after the IPC
189
+ // channel drained. See this function's doc for what is and is not left.
196
190
  return (
197
- `${head}. It exited cleanly with nothing on stderr and sent no message, so the child drained ` +
198
- `its loop without sending: something it awaited never settled. On the run path the usual ` +
199
- `cause is a project file with a top-level \`await\` that does not settle — module scope is ` +
200
- `arbitrary project source, and an import of such a file never completes. A promise that ` +
201
- `never settles is not a rejection, so the driver's own \`main().catch\` does not see it ` +
202
- `either, which is why nothing is written anywhere. The config driver bundles to one ` +
203
- `microtask over synchronous code with no runtime I/O, so on THAT path nothing can hang and ` +
204
- `the payload was lost between \`process.send\` and exit instead (chant#2461).`
191
+ `${head}. It exited cleanly with nothing on stderr, and no message arrived even after its ` +
192
+ `IPC channel closed, so it never sent one: something it awaited never settled. On the run ` +
193
+ `path the usual cause is a project file with a top-level \`await\` that does not settle. ` +
194
+ `Module scope is arbitrary project source, an import of such a file never completes, and a ` +
195
+ `promise that never settles is not a rejection, so the driver's own \`main().catch\` does ` +
196
+ `not see it either. The config driver has nothing to await that can hang, and a failed ` +
197
+ `send exits non-zero with a reason on stderr, so on the config path this has no known ` +
198
+ `cause: please report it with the command that produced it (chant#2461).`
205
199
  );
206
200
  }
207
201
 
202
+ /** How long {@link settleOnExitAfterChannelDrains} waits, after exit, for the IPC channel to report closed. Normally it takes well under a millisecond; this only bounds a channel that never does. */
203
+ const EXIT_DRAIN_GRACE_MS = 2_000;
204
+
205
+ /**
206
+ * chant#2461 — call `onExit` once `child` has exited AND its IPC channel has
207
+ * drained, so a message the child wrote before exiting is always dispatched
208
+ * first.
209
+ *
210
+ * Node does not order `exit` after `message`. The child's exit and the last
211
+ * bytes on the IPC socket reach the parent's event loop as separate events,
212
+ * and under load the exit can be handled first even though the child wrote
213
+ * the message before it exited. Settling on `exit` alone then drops a payload
214
+ * that is already sitting in the socket. The channel reports `disconnect`
215
+ * only after it has read EOF, which comes after every message the child
216
+ * wrote, so waiting for it is exact.
217
+ *
218
+ * This is deliberately not `close`. `close` also waits for stdout and stderr
219
+ * to end, and a grandchild holding those pipes would turn a fast failure into
220
+ * the full timeout. The wait here is on the IPC channel alone, and is bounded
221
+ * by `graceMs` in case that channel never reports closed.
222
+ */
223
+ export function settleOnExitAfterChannelDrains(
224
+ child: ChildProcess,
225
+ onExit: (code: number | null, signal: NodeJS.Signals | null) => void,
226
+ graceMs = EXIT_DRAIN_GRACE_MS,
227
+ ): void {
228
+ child.on("exit", (code, signal) => {
229
+ let done = false;
230
+ let grace: NodeJS.Timeout | undefined;
231
+ const finish = (): void => {
232
+ if (done) return;
233
+ done = true;
234
+ if (grace !== undefined) clearTimeout(grace);
235
+ onExit(code, signal);
236
+ };
237
+ if (!child.connected) {
238
+ finish();
239
+ return;
240
+ }
241
+ child.once("disconnect", finish);
242
+ grace = setTimeout(finish, graceMs);
243
+ });
244
+ }
245
+
208
246
  export function forkSandboxed<T>(
209
247
  options: SandboxForkOptions,
210
248
  isResponse: (value: unknown) => value is T,
@@ -298,7 +336,9 @@ export function forkSandboxed<T>(
298
336
  reject(err);
299
337
  });
300
338
 
301
- child.on("exit", (code, signal) => {
339
+ // chant#2461 — not `child.on("exit")`: a payload can still be in the IPC
340
+ // socket when the exit is handled. See settleOnExitAfterChannelDrains.
341
+ settleOnExitAfterChannelDrains(child, (code, signal) => {
302
342
  if (settled) return;
303
343
  settled = true;
304
344
  clearTimeout(timeout);
@@ -0,0 +1,285 @@
1
+ /**
2
+ * chant#2590 — every file of a multi-file lexicon declared by path gets the
3
+ * fold trust a subpath of the same lexicon installed as a package gets, with
4
+ * and without `--sandbox`.
5
+ *
6
+ * The lexicon is three files: `resources.ts`, `composites.ts` (which imports
7
+ * `./resources`) and `index.ts` (which re-exports both and holds the plugin).
8
+ * It is written once as the package `@intentius/chant-lexicon-demo` and once
9
+ * as `./lexicon/` named by path. One project file imports the lexicon's entry
10
+ * and one imports a constructor from its `resources` file directly, by
11
+ * package subpath or by path. Outputs and fold verdicts are compared.
12
+ *
13
+ * The rest of the file covers the bounds of the trusted directory.
14
+ */
15
+ import { describe, test, expect, beforeAll, afterAll, afterEach } from "vitest";
16
+ import { mkdir, mkdtemp, realpath, rm, writeFile } from "node:fs/promises";
17
+ import { join, resolve } from "node:path";
18
+ import { tmpdir } from "node:os";
19
+ import { build } from "../../build";
20
+ import { loadChantConfig } from "../../config";
21
+ import { lexiconModuleRoot, pathLexiconRoot, resetLexiconModules } from "../../lexicon-module";
22
+ import { createFoldSession } from "../fold-import";
23
+ import type { LexiconPlugin } from "../../lexicon";
24
+
25
+ const runtimePath = resolve(import.meta.dirname, "../../runtime");
26
+ const compositePath = resolve(import.meta.dirname, "../../composite");
27
+
28
+ const RESOURCES = `
29
+ import { createResource } from ${JSON.stringify(runtimePath)};
30
+
31
+ export const Bucket = createResource("Demo::Bucket", "demo", { arn: "Arn" });
32
+ export const Role = createResource("Demo::Role", "demo", {});
33
+ `;
34
+
35
+ const COMPOSITES = `
36
+ import { Composite } from ${JSON.stringify(compositePath)};
37
+ import { Bucket, Role } from "./resources";
38
+
39
+ export const WebApp = Composite((props) => {
40
+ const bucket = new Bucket({ bucketName: props.name });
41
+ const role = new Role({ resource: bucket.arn });
42
+ return { bucket, role };
43
+ }, "WebApp");
44
+ `;
45
+
46
+ const INDEX = `
47
+ export * from "./resources";
48
+ export * from "./composites";
49
+
50
+ export const Regions = { primary: "us-east-1" };
51
+
52
+ function plain(value) {
53
+ if (value && typeof value === "object" && typeof value.getLogicalName === "function") {
54
+ return { ref: value.getLogicalName(), attr: value.attribute };
55
+ }
56
+ if (Array.isArray(value)) return value.map(plain);
57
+ if (value && typeof value === "object") {
58
+ return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, plain(v)]));
59
+ }
60
+ return value;
61
+ }
62
+
63
+ export const demoPlugin = {
64
+ name: "demo",
65
+ serializer: {
66
+ name: "demo",
67
+ rulePrefix: "DEMO",
68
+ serialize: (entities) =>
69
+ JSON.stringify(
70
+ Object.fromEntries(
71
+ [...entities.keys()].sort().map((name) => {
72
+ const entity = entities.get(name);
73
+ return [name, { type: entity.entityType, props: plain(entity.props) }];
74
+ }),
75
+ ),
76
+ ),
77
+ },
78
+ generate: async () => {},
79
+ validate: async () => {},
80
+ coverage: async () => {},
81
+ package: async () => {},
82
+ };
83
+ `;
84
+
85
+ function mainSource(entry: string): string {
86
+ return `
87
+ import { Bucket, Regions, WebApp } from ${JSON.stringify(entry)};
88
+
89
+ export const logs = new Bucket({ bucketName: Regions.primary });
90
+ export const web = WebApp({ name: "data" });
91
+ `;
92
+ }
93
+
94
+ function rolesSource(resources: string): string {
95
+ return `
96
+ import { Role } from ${JSON.stringify(resources)};
97
+
98
+ export const deployer = new Role({ resource: "deploy" });
99
+ `;
100
+ }
101
+
102
+ async function writeLexicon(dir: string): Promise<void> {
103
+ await mkdir(dir, { recursive: true });
104
+ await writeFile(join(dir, "resources.ts"), RESOURCES);
105
+ await writeFile(join(dir, "composites.ts"), COMPOSITES);
106
+ await writeFile(join(dir, "index.ts"), INDEX);
107
+ }
108
+
109
+ let root: string;
110
+
111
+ beforeAll(async () => {
112
+ root = await realpath(await mkdtemp(join(tmpdir(), "chant-2590-")));
113
+
114
+ const pkg = join(root, "pkg");
115
+ const pkgDir = join(pkg, "node_modules", "@intentius", "chant-lexicon-demo");
116
+ await writeLexicon(pkgDir);
117
+ await writeFile(
118
+ join(pkgDir, "package.json"),
119
+ JSON.stringify({
120
+ name: "@intentius/chant-lexicon-demo",
121
+ type: "module",
122
+ exports: { ".": "./index.ts", "./resources": "./resources.ts" },
123
+ }),
124
+ );
125
+ await mkdir(join(pkg, "src"));
126
+ await writeFile(join(pkg, "src", "main.ts"), mainSource("@intentius/chant-lexicon-demo"));
127
+ await writeFile(join(pkg, "src", "roles.ts"), rolesSource("@intentius/chant-lexicon-demo/resources"));
128
+ await writeFile(join(pkg, "chant.config.ts"), `export default { lexicons: ["demo"] };\n`);
129
+
130
+ const path = join(root, "path");
131
+ await writeLexicon(join(path, "lexicon"));
132
+ await mkdir(join(path, "src"));
133
+ await writeFile(join(path, "src", "main.ts"), mainSource("../lexicon"));
134
+ await writeFile(join(path, "src", "roles.ts"), rolesSource("../lexicon/resources"));
135
+ await writeFile(
136
+ join(path, "chant.config.ts"),
137
+ `export default { lexicons: [{ name: "demo", module: "./lexicon/index.ts" }] };\n`,
138
+ );
139
+
140
+ // The same lexicon with its module at the project root: only the module
141
+ // file is trusted, so its sibling stays a project file.
142
+ const flat = join(root, "flat");
143
+ await mkdir(join(flat, "src"), { recursive: true });
144
+ await writeFile(join(flat, "resources.ts"), RESOURCES);
145
+ await writeFile(join(flat, "composites.ts"), COMPOSITES);
146
+ await writeFile(join(flat, "demo.ts"), INDEX);
147
+ await writeFile(join(flat, "src", "roles.ts"), rolesSource("../resources"));
148
+ await writeFile(join(flat, "chant.config.ts"), `export default { lexicons: [{ name: "demo", module: "./demo.ts" }] };\n`);
149
+ });
150
+
151
+ afterAll(async () => {
152
+ await rm(root, { recursive: true, force: true });
153
+ });
154
+
155
+ afterEach(() => {
156
+ resetLexiconModules();
157
+ });
158
+
159
+ async function buildProject(project: "pkg" | "path" | "flat", modes: { fold: boolean; sandbox: boolean }) {
160
+ const dir = join(root, project);
161
+ const lexiconFile = {
162
+ pkg: join(dir, "node_modules", "@intentius", "chant-lexicon-demo", "index.ts"),
163
+ path: join(dir, "lexicon", "index.ts"),
164
+ flat: join(dir, "demo.ts"),
165
+ }[project];
166
+ await loadChantConfig(dir);
167
+ const plugin = (await import(lexiconFile)).demoPlugin as LexiconPlugin;
168
+ const result = await build(join(dir, "src"), [plugin.serializer], undefined, {
169
+ ...modes,
170
+ lexicons: [plugin.name],
171
+ });
172
+ return {
173
+ errors: result.errors,
174
+ output: result.outputs.get("demo") as string,
175
+ decisions: result.foldDecisions
176
+ .map((d) => ({ file: d.file.slice(dir.length), mode: d.mode, reason: d.reason }))
177
+ .sort((a, b) => a.file.localeCompare(b.file)),
178
+ };
179
+ }
180
+
181
+ describe("a multi-file lexicon declared by path folds like the same lexicon installed as a package (chant#2590)", () => {
182
+ for (const modes of [
183
+ { fold: true, sandbox: true },
184
+ { fold: true, sandbox: false },
185
+ ]) {
186
+ test(
187
+ `--fold${modes.sandbox ? " --sandbox" : ""}: same output, same fold verdicts`,
188
+ async () => {
189
+ const asPackage = await buildProject("pkg", modes);
190
+ const byPath = await buildProject("path", modes);
191
+
192
+ expect(asPackage.errors).toEqual([]);
193
+ expect(byPath.errors).toEqual([]);
194
+ expect(byPath.output).toBe(asPackage.output);
195
+ expect(JSON.parse(byPath.output)).toEqual({
196
+ deployer: { type: "Demo::Role", props: { resource: "deploy" } },
197
+ logs: { type: "Demo::Bucket", props: { bucketName: "us-east-1" } },
198
+ webBucket: { type: "Demo::Bucket", props: { bucketName: "data" } },
199
+ webRole: { type: "Demo::Role", props: { resource: { ref: "webBucket", attr: "Arn" } } },
200
+ });
201
+ expect(byPath.decisions).toEqual(asPackage.decisions);
202
+ expect(byPath.decisions).toEqual([
203
+ { file: "/src/main.ts", mode: "fold", reason: undefined },
204
+ { file: "/src/roles.ts", mode: "fold", reason: undefined },
205
+ ]);
206
+ },
207
+ 60_000,
208
+ );
209
+ }
210
+
211
+ test("a module at the project root trusts only itself: under --sandbox its sibling runs in the child", async () => {
212
+ const flat = await buildProject("flat", { fold: true, sandbox: true });
213
+ expect(flat.errors).toEqual([]);
214
+ expect(JSON.parse(flat.output)).toEqual({ deployer: { type: "Demo::Role", props: { resource: "deploy" } } });
215
+ expect(flat.decisions).toHaveLength(1);
216
+ expect(flat.decisions[0]).toMatchObject({ file: "/src/roles.ts", mode: "run" });
217
+ expect(flat.decisions[0].reason).toContain("neither chant's own nor an active lexicon");
218
+ }, 60_000);
219
+ });
220
+
221
+ describe("the trusted directory's bounds (chant#2590)", () => {
222
+ const project = "/p";
223
+
224
+ test("defaults to the module's own directory", () => {
225
+ expect(pathLexiconRoot({ module: "./lexicon/index.ts" }, project)).toEqual({ root: "/p/lexicon" });
226
+ });
227
+
228
+ test("a module at the project root gets no directory", () => {
229
+ expect(pathLexiconRoot({ module: "./demo.ts" }, project)).toEqual({});
230
+ });
231
+
232
+ test("a module outside the project gets no directory", () => {
233
+ expect(pathLexiconRoot({ module: "/elsewhere/demo.ts" }, project)).toEqual({});
234
+ });
235
+
236
+ test("the module's directory is dropped when it contains the source directory", () => {
237
+ expect(pathLexiconRoot({ module: "./infra/lexicon.ts" }, project, "infra/src")).toEqual({});
238
+ expect(pathLexiconRoot({ module: "./infra/lexicon.ts" }, project, "infra")).toEqual({});
239
+ });
240
+
241
+ test("a declared root is used when it holds the module", () => {
242
+ expect(pathLexiconRoot({ module: "./lexicon/src/index.ts", root: "./lexicon" }, project)).toEqual({
243
+ root: "/p/lexicon",
244
+ });
245
+ });
246
+
247
+ test("a declared root outside the project, over the source, or not holding the module is a problem", () => {
248
+ expect(pathLexiconRoot({ module: "./lexicon/index.ts", root: ".." }, project).problem).toContain("outside the project");
249
+ expect(pathLexiconRoot({ module: "./lexicon/index.ts", root: "." }, project).problem).toContain(
250
+ "contains the project's source directory",
251
+ );
252
+ expect(pathLexiconRoot({ module: "./lexicon/index.ts", root: "./other" }, project).problem).toContain(
253
+ "is not inside",
254
+ );
255
+ });
256
+
257
+ test("the config loader refuses a declared root that breaks a rule", async () => {
258
+ const dir = join(root, "bad-root");
259
+ await mkdir(join(dir, "lexicon"), { recursive: true });
260
+ await writeFile(
261
+ join(dir, "chant.config.json"),
262
+ JSON.stringify({ lexicons: [{ name: "demo", module: "./lexicon/index.ts", root: "." }] }),
263
+ );
264
+ await expect(loadChantConfig(dir)).rejects.toThrow(/lexicons\.0\.root: .*contains the project's source directory/);
265
+ });
266
+
267
+ test("the config loader records a declared root", async () => {
268
+ const dir = join(root, "good-root");
269
+ await mkdir(join(dir, "lexicon", "src"), { recursive: true });
270
+ await writeFile(
271
+ join(dir, "chant.config.json"),
272
+ JSON.stringify({ lexicons: [{ name: "demo", module: "./lexicon/src/index.ts", root: "./lexicon" }] }),
273
+ );
274
+ await loadChantConfig(dir);
275
+ expect(lexiconModuleRoot("demo")).toBe(join(dir, "lexicon"));
276
+ });
277
+
278
+ test("a fold session drops a directory holding any of the build's own source files", async () => {
279
+ const dir = join(root, "path");
280
+ await loadChantConfig(dir);
281
+ expect(createFoldSession([], undefined, ["demo"]).lexiconRoots).toEqual([join(dir, "lexicon")]);
282
+ const session = createFoldSession([], undefined, ["demo"], true, [], false, [join(dir, "lexicon", "extra.ts")]);
283
+ expect(session.lexiconRoots).toEqual([]);
284
+ });
285
+ });