@intentius/chant 0.80.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 (414) hide show
  1. package/dist/audit/discover.d.ts +8 -16
  2. package/dist/audit/discover.d.ts.map +1 -1
  3. package/dist/audit/report-html.d.ts +5 -0
  4. package/dist/audit/report-html.d.ts.map +1 -1
  5. package/dist/audit/report-model.d.ts +13 -0
  6. package/dist/audit/report-model.d.ts.map +1 -1
  7. package/dist/audit/terraform-state.d.ts +4 -5
  8. package/dist/audit/terraform-state.d.ts.map +1 -1
  9. package/dist/cli/commands/audit.d.ts +25 -9
  10. package/dist/cli/commands/audit.d.ts.map +1 -1
  11. package/dist/cli/commands/check-lexicon.d.ts +8 -0
  12. package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
  13. package/dist/cli/commands/doctor.d.ts.map +1 -1
  14. package/dist/cli/commands/import-agents.d.ts.map +1 -1
  15. package/dist/cli/commands/init.d.ts.map +1 -1
  16. package/dist/cli/commands/lint.d.ts.map +1 -1
  17. package/dist/cli/commands/update.d.ts +6 -0
  18. package/dist/cli/commands/update.d.ts.map +1 -1
  19. package/dist/cli/commands/vendor.d.ts +5 -0
  20. package/dist/cli/commands/vendor.d.ts.map +1 -1
  21. package/dist/cli/handlers/build.d.ts.map +1 -1
  22. package/dist/cli/handlers/components.d.ts.map +1 -1
  23. package/dist/cli/handlers/dev.d.ts.map +1 -1
  24. package/dist/cli/handlers/fan-out.d.ts.map +1 -1
  25. package/dist/cli/handlers/init.d.ts.map +1 -1
  26. package/dist/cli/handlers/operator.d.ts +7 -0
  27. package/dist/cli/handlers/operator.d.ts.map +1 -1
  28. package/dist/cli/handlers/promote.d.ts +7 -4
  29. package/dist/cli/handlers/promote.d.ts.map +1 -1
  30. package/dist/cli/handlers/run-generate.d.ts.map +1 -1
  31. package/dist/cli/handlers/run.d.ts +4 -1
  32. package/dist/cli/handlers/run.d.ts.map +1 -1
  33. package/dist/cli/handlers/vendor.d.ts +8 -3
  34. package/dist/cli/handlers/vendor.d.ts.map +1 -1
  35. package/dist/cli/main.d.ts +12 -0
  36. package/dist/cli/main.d.ts.map +1 -1
  37. package/dist/cli/plugins.d.ts +12 -7
  38. package/dist/cli/plugins.d.ts.map +1 -1
  39. package/dist/cli/registry.d.ts +32 -0
  40. package/dist/cli/registry.d.ts.map +1 -1
  41. package/dist/codegen/fetch.d.ts +10 -0
  42. package/dist/codegen/fetch.d.ts.map +1 -1
  43. package/dist/components/cli-support.d.ts +3 -1
  44. package/dist/components/cli-support.d.ts.map +1 -1
  45. package/dist/components/discover.d.ts.map +1 -1
  46. package/dist/components/driver.d.ts +16 -2
  47. package/dist/components/driver.d.ts.map +1 -1
  48. package/dist/components/gate-plan.d.ts +40 -0
  49. package/dist/components/gate-plan.d.ts.map +1 -0
  50. package/dist/components/promote.d.ts +60 -7
  51. package/dist/components/promote.d.ts.map +1 -1
  52. package/dist/components/sandbox/driver.d.ts.map +1 -1
  53. package/dist/components/sandbox/run.d.ts.map +1 -1
  54. package/dist/components/verbs/build-archive.d.ts +22 -11
  55. package/dist/components/verbs/build-archive.d.ts.map +1 -1
  56. package/dist/components/verbs/run-agent.d.ts.map +1 -1
  57. package/dist/config-static.d.ts +59 -0
  58. package/dist/config-static.d.ts.map +1 -0
  59. package/dist/config.d.ts +32 -10
  60. package/dist/config.d.ts.map +1 -1
  61. package/dist/content-digest.d.ts +17 -0
  62. package/dist/content-digest.d.ts.map +1 -0
  63. package/dist/discovery/files.d.ts +10 -5
  64. package/dist/discovery/files.d.ts.map +1 -1
  65. package/dist/discovery/fold-import.d.ts +20 -4
  66. package/dist/discovery/fold-import.d.ts.map +1 -1
  67. package/dist/discovery/index.d.ts.map +1 -1
  68. package/dist/discovery/sandbox/driver.d.ts +20 -0
  69. package/dist/discovery/sandbox/driver.d.ts.map +1 -1
  70. package/dist/discovery/sandbox/fork.d.ts +20 -0
  71. package/dist/discovery/sandbox/fork.d.ts.map +1 -1
  72. package/dist/discovery/walk.d.ts +119 -0
  73. package/dist/discovery/walk.d.ts.map +1 -0
  74. package/dist/graph-ir.d.ts +4 -1
  75. package/dist/graph-ir.d.ts.map +1 -1
  76. package/dist/join-key.d.ts +31 -0
  77. package/dist/join-key.d.ts.map +1 -0
  78. package/dist/lexicon-module.d.ts +51 -6
  79. package/dist/lexicon-module.d.ts.map +1 -1
  80. package/dist/lexicon.d.ts +58 -1
  81. package/dist/lexicon.d.ts.map +1 -1
  82. package/dist/lifecycle/build-ledger-store.d.ts.map +1 -1
  83. package/dist/lifecycle/digest.d.ts +5 -1
  84. package/dist/lifecycle/digest.d.ts.map +1 -1
  85. package/dist/lifecycle/gate-ledger.d.ts +20 -5
  86. package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
  87. package/dist/lifecycle/git.d.ts +64 -0
  88. package/dist/lifecycle/git.d.ts.map +1 -1
  89. package/dist/lifecycle/lease.d.ts +8 -1
  90. package/dist/lifecycle/lease.d.ts.map +1 -1
  91. package/dist/lifecycle/legacy-digest.d.ts +41 -8
  92. package/dist/lifecycle/legacy-digest.d.ts.map +1 -1
  93. package/dist/lifecycle/member-ledger.d.ts +67 -0
  94. package/dist/lifecycle/member-ledger.d.ts.map +1 -0
  95. package/dist/lifecycle/plan-digest.d.ts +4 -3
  96. package/dist/lifecycle/plan-digest.d.ts.map +1 -1
  97. package/dist/lifecycle/release-ledger.d.ts +18 -2
  98. package/dist/lifecycle/release-ledger.d.ts.map +1 -1
  99. package/dist/lifecycle/status.d.ts +90 -5
  100. package/dist/lifecycle/status.d.ts.map +1 -1
  101. package/dist/op/activities/activity-contracts.d.ts +32 -0
  102. package/dist/op/activities/activity-contracts.d.ts.map +1 -1
  103. package/dist/op/activities/index.d.ts +2 -0
  104. package/dist/op/activities/index.d.ts.map +1 -1
  105. package/dist/op/activities/propose-upgrade.d.ts +75 -0
  106. package/dist/op/activities/propose-upgrade.d.ts.map +1 -0
  107. package/dist/op/discover.d.ts.map +1 -1
  108. package/dist/op/gate-name.d.ts +7 -0
  109. package/dist/op/gate-name.d.ts.map +1 -1
  110. package/dist/op/gate-summary.d.ts +16 -0
  111. package/dist/op/gate-summary.d.ts.map +1 -1
  112. package/dist/op/gate.d.ts +16 -2
  113. package/dist/op/gate.d.ts.map +1 -1
  114. package/dist/project-root.d.ts +26 -0
  115. package/dist/project-root.d.ts.map +1 -1
  116. package/dist/workspace/__fixtures__/contract-repo.d.ts +37 -0
  117. package/dist/workspace/__fixtures__/contract-repo.d.ts.map +1 -0
  118. package/dist/workspace/__fixtures__/two-member-workspace.d.ts +11 -0
  119. package/dist/workspace/__fixtures__/two-member-workspace.d.ts.map +1 -0
  120. package/dist/workspace/checks/generated.d.ts +156 -0
  121. package/dist/workspace/checks/generated.d.ts.map +1 -0
  122. package/dist/workspace/checks/ledgers.d.ts +78 -0
  123. package/dist/workspace/checks/ledgers.d.ts.map +1 -0
  124. package/dist/workspace/checks/links.d.ts +25 -0
  125. package/dist/workspace/checks/links.d.ts.map +1 -0
  126. package/dist/workspace/checks/pipelines.d.ts +76 -0
  127. package/dist/workspace/checks/pipelines.d.ts.map +1 -0
  128. package/dist/workspace/checks.d.ts +161 -0
  129. package/dist/workspace/checks.d.ts.map +1 -0
  130. package/dist/workspace/compose-graph.d.ts +115 -0
  131. package/dist/workspace/compose-graph.d.ts.map +1 -0
  132. package/dist/workspace/compose-reports.d.ts +68 -0
  133. package/dist/workspace/compose-reports.d.ts.map +1 -0
  134. package/dist/workspace/declaration.d.ts +220 -0
  135. package/dist/workspace/declaration.d.ts.map +1 -0
  136. package/dist/workspace/declaration.schema.json +434 -0
  137. package/dist/workspace/generated-files.d.ts +65 -0
  138. package/dist/workspace/generated-files.d.ts.map +1 -0
  139. package/dist/workspace/glob.d.ts +13 -0
  140. package/dist/workspace/glob.d.ts.map +1 -0
  141. package/dist/workspace/graph-cli.d.ts +75 -0
  142. package/dist/workspace/graph-cli.d.ts.map +1 -0
  143. package/dist/workspace/init.d.ts +96 -0
  144. package/dist/workspace/init.d.ts.map +1 -0
  145. package/dist/workspace/jsonc.d.ts +38 -0
  146. package/dist/workspace/jsonc.d.ts.map +1 -0
  147. package/dist/workspace/kinds.d.ts +192 -0
  148. package/dist/workspace/kinds.d.ts.map +1 -0
  149. package/dist/workspace/lineage-check.d.ts +84 -0
  150. package/dist/workspace/lineage-check.d.ts.map +1 -0
  151. package/dist/workspace/lineage-cli.d.ts +31 -0
  152. package/dist/workspace/lineage-cli.d.ts.map +1 -0
  153. package/dist/workspace/lineage-init.d.ts +101 -0
  154. package/dist/workspace/lineage-init.d.ts.map +1 -0
  155. package/dist/workspace/lineage-lock.d.ts +258 -0
  156. package/dist/workspace/lineage-lock.d.ts.map +1 -0
  157. package/dist/workspace/lineage-merge.d.ts +14 -0
  158. package/dist/workspace/lineage-merge.d.ts.map +1 -0
  159. package/dist/workspace/lineage-migrations.d.ts +178 -0
  160. package/dist/workspace/lineage-migrations.d.ts.map +1 -0
  161. package/dist/workspace/lineage-update.d.ts +59 -0
  162. package/dist/workspace/lineage-update.d.ts.map +1 -0
  163. package/dist/workspace/lineage-upgrade-cli.d.ts +40 -0
  164. package/dist/workspace/lineage-upgrade-cli.d.ts.map +1 -0
  165. package/dist/workspace/lineage-upgrade.d.ts +146 -0
  166. package/dist/workspace/lineage-upgrade.d.ts.map +1 -0
  167. package/dist/workspace/lineage-vendor.d.ts +63 -0
  168. package/dist/workspace/lineage-vendor.d.ts.map +1 -0
  169. package/dist/workspace/lineage-version.d.ts +32 -0
  170. package/dist/workspace/lineage-version.d.ts.map +1 -0
  171. package/dist/workspace/links.d.ts +150 -0
  172. package/dist/workspace/links.d.ts.map +1 -0
  173. package/dist/workspace/ls.d.ts +99 -0
  174. package/dist/workspace/ls.d.ts.map +1 -0
  175. package/dist/workspace/member-commands.d.ts +139 -0
  176. package/dist/workspace/member-commands.d.ts.map +1 -0
  177. package/dist/workspace/member-handles.d.ts +54 -0
  178. package/dist/workspace/member-handles.d.ts.map +1 -0
  179. package/dist/workspace/member-pipeline.d.ts +143 -0
  180. package/dist/workspace/member-pipeline.d.ts.map +1 -0
  181. package/dist/workspace/member-run.d.ts +67 -0
  182. package/dist/workspace/member-run.d.ts.map +1 -0
  183. package/dist/workspace/reason-codes.d.ts +65 -0
  184. package/dist/workspace/reason-codes.d.ts.map +1 -0
  185. package/dist/workspace/records-cli.d.ts +28 -4
  186. package/dist/workspace/records-cli.d.ts.map +1 -1
  187. package/dist/workspace/records.d.ts.map +1 -1
  188. package/dist/workspace/root-refusal.d.ts +53 -0
  189. package/dist/workspace/root-refusal.d.ts.map +1 -0
  190. package/dist/workspace/status.d.ts +150 -0
  191. package/dist/workspace/status.d.ts.map +1 -0
  192. package/dist/workspace/template-manifest.d.ts +90 -0
  193. package/dist/workspace/template-manifest.d.ts.map +1 -0
  194. package/dist/workspace/tree.d.ts +36 -0
  195. package/dist/workspace/tree.d.ts.map +1 -0
  196. package/dist/workspace/trust/attestor.d.ts +67 -0
  197. package/dist/workspace/trust/attestor.d.ts.map +1 -0
  198. package/dist/workspace/trust/policy.d.ts +111 -0
  199. package/dist/workspace/trust/policy.d.ts.map +1 -0
  200. package/dist/workspace/trust/provenance.d.ts +67 -0
  201. package/dist/workspace/trust/provenance.d.ts.map +1 -0
  202. package/dist/workspace/trust/ssh-commit.d.ts +43 -0
  203. package/dist/workspace/trust/ssh-commit.d.ts.map +1 -0
  204. package/dist/workspace/trust/test-repo.d.ts +42 -0
  205. package/dist/workspace/trust/test-repo.d.ts.map +1 -0
  206. package/dist/workspace/trust/verify-cli.d.ts +9 -0
  207. package/dist/workspace/trust/verify-cli.d.ts.map +1 -0
  208. package/dist/workspace/trust/verify.d.ts +66 -0
  209. package/dist/workspace/trust/verify.d.ts.map +1 -0
  210. package/dist/workspace/which-chant.d.ts +58 -0
  211. package/dist/workspace/which-chant.d.ts.map +1 -0
  212. package/dist/workspace/workspace-kinds.schema.json +133 -0
  213. package/package.json +2 -1
  214. package/src/audit/discover.ts +66 -78
  215. package/src/audit/report-html.ts +3 -1
  216. package/src/audit/report-model.ts +14 -1
  217. package/src/audit/terraform-state.test.ts +4 -9
  218. package/src/audit/terraform-state.ts +4 -5
  219. package/src/cli/commands/audit-truncation.test.ts +167 -0
  220. package/src/cli/commands/audit.ts +89 -60
  221. package/src/cli/commands/check-lexicon.ts +24 -0
  222. package/src/cli/commands/doctor.test.ts +30 -0
  223. package/src/cli/commands/doctor.ts +18 -0
  224. package/src/cli/commands/import-agents.ts +6 -2
  225. package/src/cli/commands/init.ts +22 -3
  226. package/src/cli/commands/lint.test.ts +17 -23
  227. package/src/cli/commands/lint.ts +24 -84
  228. package/src/cli/commands/update.ts +13 -4
  229. package/src/cli/commands/vendor.ts +11 -6
  230. package/src/cli/handlers/__snapshots__/build-member.test.ts.snap +38 -0
  231. package/src/cli/handlers/build-member.test.ts +127 -0
  232. package/src/cli/handlers/build.ts +35 -1
  233. package/src/cli/handlers/components.test.ts +127 -0
  234. package/src/cli/handlers/components.ts +70 -8
  235. package/src/cli/handlers/dev.ts +5 -3
  236. package/src/cli/handlers/fan-out.ts +9 -2
  237. package/src/cli/handlers/init.ts +54 -1
  238. package/src/cli/handlers/operator.test.ts +68 -0
  239. package/src/cli/handlers/operator.ts +37 -9
  240. package/src/cli/handlers/promote.test.ts +118 -2
  241. package/src/cli/handlers/promote.ts +82 -16
  242. package/src/cli/handlers/run-generate-member.test.ts +83 -0
  243. package/src/cli/handlers/run-generate.test.ts +6 -8
  244. package/src/cli/handlers/run-generate.ts +36 -1
  245. package/src/cli/handlers/run.test.ts +47 -0
  246. package/src/cli/handlers/run.ts +28 -5
  247. package/src/cli/handlers/vendor.ts +147 -43
  248. package/src/cli/main.test.ts +105 -1
  249. package/src/cli/main.ts +230 -17
  250. package/src/cli/plugins.ts +17 -17
  251. package/src/cli/registry.ts +32 -0
  252. package/src/cli/static-config-read.test.ts +208 -0
  253. package/src/codegen/fetch.test.ts +63 -1
  254. package/src/codegen/fetch.ts +30 -1
  255. package/src/components/cli-support.ts +6 -1
  256. package/src/components/discover.test.ts +27 -57
  257. package/src/components/discover.ts +13 -68
  258. package/src/components/driver.test.ts +78 -1
  259. package/src/components/driver.ts +43 -7
  260. package/src/components/gate-plan.ts +53 -0
  261. package/src/components/promote.test.ts +276 -2
  262. package/src/components/promote.ts +119 -15
  263. package/src/components/sandbox/driver.ts +8 -8
  264. package/src/components/sandbox/run.ts +4 -1
  265. package/src/components/verbs/build-archive.ts +27 -21
  266. package/src/components/verbs/run-agent.ts +3 -5
  267. package/src/config-static.test.ts +136 -0
  268. package/src/config-static.ts +232 -0
  269. package/src/config.test.ts +2 -2
  270. package/src/config.ts +48 -15
  271. package/src/content-digest.test.ts +120 -0
  272. package/src/content-digest.ts +21 -0
  273. package/src/discovery/files.test.ts +10 -10
  274. package/src/discovery/files.ts +25 -81
  275. package/src/discovery/fold-import.ts +49 -5
  276. package/src/discovery/index.ts +1 -0
  277. package/src/discovery/sandbox/driver.ts +73 -35
  278. package/src/discovery/sandbox/fork-diagnostic.test.ts +136 -13
  279. package/src/discovery/sandbox/fork.ts +83 -43
  280. package/src/discovery/sandbox/path-lexicon-multifile.test.ts +285 -0
  281. package/src/discovery/walk.test.ts +263 -0
  282. package/src/discovery/walk.ts +334 -0
  283. package/src/effect-receipt.ts +2 -2
  284. package/src/graph-ir.ts +4 -1
  285. package/src/join-key.test.ts +25 -0
  286. package/src/join-key.ts +39 -0
  287. package/src/lexicon-module.ts +108 -10
  288. package/src/lexicon.ts +79 -1
  289. package/src/lifecycle/build-ledger-store.ts +11 -10
  290. package/src/lifecycle/digest.ts +9 -5
  291. package/src/lifecycle/gate-ledger.test.ts +20 -0
  292. package/src/lifecycle/gate-ledger.ts +28 -6
  293. package/src/lifecycle/git.test.ts +13 -1
  294. package/src/lifecycle/git.ts +221 -176
  295. package/src/lifecycle/lease.ts +25 -10
  296. package/src/lifecycle/legacy-digest.test.ts +115 -23
  297. package/src/lifecycle/legacy-digest.ts +60 -19
  298. package/src/lifecycle/member-ledger.test.ts +319 -0
  299. package/src/lifecycle/member-ledger.ts +145 -0
  300. package/src/lifecycle/plan-digest.ts +7 -7
  301. package/src/lifecycle/release-ledger.test.ts +20 -0
  302. package/src/lifecycle/release-ledger.ts +23 -7
  303. package/src/lifecycle/status.test.ts +151 -1
  304. package/src/lifecycle/status.ts +228 -7
  305. package/src/op/activities/activity-contracts.ts +37 -0
  306. package/src/op/activities/index.ts +10 -0
  307. package/src/op/activities/propose-upgrade.ts +206 -0
  308. package/src/op/discover.test.ts +14 -4
  309. package/src/op/discover.ts +19 -27
  310. package/src/op/gate-name.ts +8 -0
  311. package/src/op/gate-summary.test.ts +14 -0
  312. package/src/op/gate-summary.ts +25 -2
  313. package/src/op/gate.ts +36 -7
  314. package/src/op/generate-pipeline.test.ts +14 -5
  315. package/src/project-root.test.ts +66 -1
  316. package/src/project-root.ts +48 -0
  317. package/src/workspace/__fixtures__/contract-repo.ts +100 -0
  318. package/src/workspace/__fixtures__/two-member-workspace.ts +35 -0
  319. package/src/workspace/behold-kinds.test.ts +162 -0
  320. package/src/workspace/check-contract.test.ts +127 -0
  321. package/src/workspace/check.schema.json +347 -0
  322. package/src/workspace/checks/generated.test.ts +287 -0
  323. package/src/workspace/checks/generated.ts +475 -0
  324. package/src/workspace/checks/ledgers.test.ts +139 -0
  325. package/src/workspace/checks/ledgers.ts +246 -0
  326. package/src/workspace/checks/links.ts +183 -0
  327. package/src/workspace/checks/pipelines.test.ts +133 -0
  328. package/src/workspace/checks/pipelines.ts +225 -0
  329. package/src/workspace/checks-repo.test.ts +71 -0
  330. package/src/workspace/checks.test.ts +450 -0
  331. package/src/workspace/checks.ts +461 -0
  332. package/src/workspace/compose-graph.test.ts +146 -0
  333. package/src/workspace/compose-graph.ts +228 -0
  334. package/src/workspace/compose-reports.test.ts +109 -0
  335. package/src/workspace/compose-reports.ts +186 -0
  336. package/src/workspace/declaration.schema.json +434 -0
  337. package/src/workspace/declaration.test.ts +347 -0
  338. package/src/workspace/declaration.ts +623 -0
  339. package/src/workspace/generated-files.test.ts +114 -0
  340. package/src/workspace/generated-files.ts +94 -0
  341. package/src/workspace/glob.ts +49 -0
  342. package/src/workspace/graph-cli.ts +220 -0
  343. package/src/workspace/graph-contract.test.ts +204 -0
  344. package/src/workspace/graph.schema.json +662 -0
  345. package/src/workspace/init.test.ts +223 -0
  346. package/src/workspace/init.ts +585 -0
  347. package/src/workspace/jsonc.test.ts +65 -0
  348. package/src/workspace/jsonc.ts +231 -0
  349. package/src/workspace/kinds.test.ts +288 -0
  350. package/src/workspace/kinds.ts +506 -0
  351. package/src/workspace/lineage-check.ts +264 -0
  352. package/src/workspace/lineage-cli.ts +114 -0
  353. package/src/workspace/lineage-init.test.ts +231 -0
  354. package/src/workspace/lineage-init.ts +365 -0
  355. package/src/workspace/lineage-lock.test.ts +224 -0
  356. package/src/workspace/lineage-lock.ts +357 -0
  357. package/src/workspace/lineage-merge.ts +44 -0
  358. package/src/workspace/lineage-migrations.test.ts +178 -0
  359. package/src/workspace/lineage-migrations.ts +343 -0
  360. package/src/workspace/lineage-update.ts +160 -0
  361. package/src/workspace/lineage-upgrade-cli.ts +151 -0
  362. package/src/workspace/lineage-upgrade.test.ts +474 -0
  363. package/src/workspace/lineage-upgrade.ts +624 -0
  364. package/src/workspace/lineage-vendor.test.ts +126 -0
  365. package/src/workspace/lineage-vendor.ts +215 -0
  366. package/src/workspace/lineage-version.ts +127 -0
  367. package/src/workspace/links.test.ts +344 -0
  368. package/src/workspace/links.ts +331 -0
  369. package/src/workspace/ls-contract.test.ts +200 -0
  370. package/src/workspace/ls.schema.json +190 -0
  371. package/src/workspace/ls.ts +254 -0
  372. package/src/workspace/member-commands.e2e.test.ts +244 -0
  373. package/src/workspace/member-commands.test.ts +217 -0
  374. package/src/workspace/member-commands.ts +540 -0
  375. package/src/workspace/member-handles.test.ts +76 -0
  376. package/src/workspace/member-handles.ts +228 -0
  377. package/src/workspace/member-pipeline.test.ts +135 -0
  378. package/src/workspace/member-pipeline.ts +317 -0
  379. package/src/workspace/member-run.ts +146 -0
  380. package/src/workspace/read-contract.test.ts +129 -0
  381. package/src/workspace/reason-codes.test.ts +113 -0
  382. package/src/workspace/reason-codes.ts +81 -0
  383. package/src/workspace/records-cli.ts +86 -14
  384. package/src/workspace/records-contract.test.ts +5 -0
  385. package/src/workspace/records.schema.json +32 -2
  386. package/src/workspace/records.ts +3 -2
  387. package/src/workspace/root-refusal.ts +97 -0
  388. package/src/workspace/status-contract.test.ts +308 -0
  389. package/src/workspace/status.schema.json +210 -0
  390. package/src/workspace/status.ts +412 -0
  391. package/src/workspace/template-manifest.test.ts +79 -0
  392. package/src/workspace/template-manifest.ts +221 -0
  393. package/src/workspace/tree.ts +132 -0
  394. package/src/workspace/trust/attestor.ts +93 -0
  395. package/src/workspace/trust/policy.ts +306 -0
  396. package/src/workspace/trust/provenance.ts +181 -0
  397. package/src/workspace/trust/ssh-commit.ts +138 -0
  398. package/src/workspace/trust/test-repo.ts +138 -0
  399. package/src/workspace/trust/trust.test.ts +397 -0
  400. package/src/workspace/trust/verify-cli.ts +56 -0
  401. package/src/workspace/trust/verify.ts +194 -0
  402. package/src/workspace/which-chant.test.ts +117 -0
  403. package/src/workspace/which-chant.ts +151 -0
  404. package/src/workspace/workspace-kinds.schema.json +133 -0
  405. package/dist/components/unbound-gate-approval.d.ts +0 -24
  406. package/dist/components/unbound-gate-approval.d.ts.map +0 -1
  407. package/dist/discovery/convergence.d.ts +0 -97
  408. package/dist/discovery/convergence.d.ts.map +0 -1
  409. package/src/cli/commands/audit-walk-warnings.test.ts +0 -156
  410. package/src/components/unbound-gate-approval.test.ts +0 -82
  411. package/src/components/unbound-gate-approval.ts +0 -51
  412. package/src/discovery/convergence.test.ts +0 -231
  413. package/src/discovery/convergence.ts +0 -399
  414. package/src/op/discover-convergence.test.ts +0 -70
@@ -0,0 +1,232 @@
1
+ /**
2
+ * chant#2591, chant#2589 — read the `lexicons` a project's config declares
3
+ * without running the config.
4
+ *
5
+ * `chant.config.ts` is project code. Most commands evaluate it (see
6
+ * `./config-sandbox.ts`), but a few only need to know which lexicons it
7
+ * declares by path (#2520) so their messages can name the path instead of a
8
+ * package: `init --force`, `dev onboard`, `import --agents` and `audit`.
9
+ * Those read the config here, statically, and never evaluate it.
10
+ *
11
+ * The reader parses the file and reduces the `lexicons` value with the fold
12
+ * machinery (`./fold/fold.ts`'s {@link fold}), given the file's own top-level
13
+ * `const`s and nothing else. It never imports anything, so an imported
14
+ * binding, a function call or `process.env` is not readable. When the value
15
+ * cannot be read, the result says why and callers treat path lexicons as
16
+ * unknown. They never fall back to evaluating the config.
17
+ *
18
+ * What the config's export looks like follows `selectConfigExport` in
19
+ * `./config-sandbox.ts`: `export default`, else `export const config`, else
20
+ * the module's named exports (`export const lexicons`).
21
+ *
22
+ * `chant.config.json` is data and is simply parsed.
23
+ *
24
+ * {@link readConfigFieldsStatically} reads other top-level fields the same
25
+ * way. `chant workspace check` reads a member's `ownership` and
26
+ * `environments` with it for the ledger checks (#2641).
27
+ */
28
+ import { existsSync, readFileSync } from "node:fs";
29
+ import { dirname, join } from "node:path";
30
+ import * as ts from "typescript";
31
+ import { ChantConfigSchema, findProjectConfigPastFragments } from "./config";
32
+ import { collectConsts, fold, propName, FoldError } from "./fold/fold";
33
+ import type { LexiconDeclaration } from "./lexicon-module";
34
+
35
+ /** What {@link readLexiconDeclarationsStatically} found. */
36
+ export type StaticLexiconRead =
37
+ /** No `chant.config.ts` or `chant.config.json` for the directory. */
38
+ | { status: "no-config" }
39
+ /** The declarations, in order. Empty when the config declares no `lexicons`. */
40
+ | { status: "read"; configPath: string; entries: LexiconDeclaration[] }
41
+ /** The config exists, but its `lexicons` could not be read without running it. */
42
+ | { status: "unknown"; configPath: string; reason: string };
43
+
44
+ /** Signals a config shape the reader does not follow. The message is the reason reported. */
45
+ class Unreadable extends Error {}
46
+
47
+ /**
48
+ * Find the project config for `startDir` (the walk `loadChantConfigUpward`
49
+ * makes) and read its `lexicons` without evaluating it.
50
+ */
51
+ export function readLexiconDeclarationsStatically(startDir: string): StaticLexiconRead {
52
+ const { configPath } = findProjectConfigPastFragments(startDir);
53
+ if (configPath === undefined) return { status: "no-config" };
54
+ try {
55
+ const value = readFields(configPath, ["lexicons"]).lexicons;
56
+ if (value === undefined) return { status: "read", configPath, entries: [] };
57
+ const parsed = ChantConfigSchema.shape.lexicons.safeParse(value);
58
+ if (!parsed.success) {
59
+ return { status: "unknown", configPath, reason: "`lexicons` is not a list of lexicon entries" };
60
+ }
61
+ return { status: "read", configPath, entries: (parsed.data ?? []) as LexiconDeclaration[] };
62
+ } catch (err) {
63
+ return { status: "unknown", configPath, reason: unreadableReason(err) };
64
+ }
65
+ }
66
+
67
+ function unreadableReason(err: unknown): string {
68
+ return err instanceof FoldError
69
+ ? `line ${err.line}: ${err.message}`
70
+ : err instanceof Unreadable
71
+ ? err.message
72
+ : `the file could not be read (${err instanceof Error ? err.message : String(err)})`;
73
+ }
74
+
75
+ /** What {@link readConfigFieldsStatically} found. */
76
+ export type StaticFieldsRead =
77
+ /** No `chant.config.ts` or `chant.config.json` in the directory. */
78
+ | { status: "no-config" }
79
+ /** Each asked-for field the config sets, folded to a value. A field it doesn't set is absent. */
80
+ | { status: "read"; configPath: string; fields: Record<string, unknown> }
81
+ /** The config exists, but a field could not be read without running it. */
82
+ | { status: "unknown"; configPath: string; reason: string };
83
+
84
+ /**
85
+ * Read top-level `fields` of the config in `dir` itself, as
86
+ * `loadChantConfig(dir)` finds it (`chant.config.ts`, else
87
+ * `chant.config.json`), without evaluating it. The values are not checked
88
+ * against the config schema; the caller does that for the fields it reads.
89
+ */
90
+ export function readConfigFieldsStatically(dir: string, fields: readonly string[]): StaticFieldsRead {
91
+ const configPath = ["chant.config.ts", "chant.config.json"].map((f) => join(dir, f)).find((f) => existsSync(f));
92
+ if (configPath === undefined) return { status: "no-config" };
93
+ try {
94
+ return { status: "read", configPath, fields: readFields(configPath, fields) };
95
+ } catch (err) {
96
+ return { status: "unknown", configPath, reason: unreadableReason(err) };
97
+ }
98
+ }
99
+
100
+ function readFields(configPath: string, fields: readonly string[]): Record<string, unknown> {
101
+ const values = configPath.endsWith(".json") ? readJsonFields(configPath, fields) : readTsFields(configPath, fields);
102
+ // Absent fields are left out rather than set to undefined.
103
+ return Object.fromEntries(Object.entries(values).filter(([, v]) => v !== undefined));
104
+ }
105
+
106
+ /** The directory a read config's relative module paths resolve against. */
107
+ export function staticConfigBaseDir(read: { configPath: string }): string {
108
+ return dirname(read.configPath);
109
+ }
110
+
111
+ /**
112
+ * The line a command prints when {@link readLexiconDeclarationsStatically}
113
+ * could not read the config, or `undefined` when it could.
114
+ */
115
+ export function unknownPathLexiconsNotice(read: StaticLexiconRead): string | undefined {
116
+ if (read.status !== "unknown") return undefined;
117
+ return (
118
+ `note: could not read the lexicons in ${read.configPath} without running it (${read.reason}). ` +
119
+ "chant does not run it for this command, so any lexicon it declares by path is unknown here and is treated as a package."
120
+ );
121
+ }
122
+
123
+ function readJsonFields(configPath: string, fields: readonly string[]): Record<string, unknown> {
124
+ const parsed = JSON.parse(readFileSync(configPath, "utf-8")) as unknown;
125
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
126
+ throw new Unreadable("the config is not a JSON object");
127
+ }
128
+ return Object.fromEntries(fields.map((f) => [f, (parsed as Record<string, unknown>)[f]]));
129
+ }
130
+
131
+ function readTsFields(configPath: string, fields: readonly string[]): Record<string, unknown> {
132
+ const source = ts.createSourceFile(configPath, readFileSync(configPath, "utf-8"), ts.ScriptTarget.Latest, true);
133
+ const consts = collectConsts(source);
134
+
135
+ let defaultExport: ts.Expression | undefined;
136
+ const named = new Map<string, ts.Expression>();
137
+ for (const statement of source.statements) {
138
+ if (ts.isExportAssignment(statement)) {
139
+ if (statement.isExportEquals) throw new Unreadable("`export =` is not read");
140
+ defaultExport = statement.expression;
141
+ } else if (ts.isExportDeclaration(statement)) {
142
+ // `export { x as default }`, `export * from "./other"`: the export's
143
+ // value lives elsewhere, possibly in another module.
144
+ throw new Unreadable("an `export { … }` or `export … from` declaration is not read");
145
+ } else if (ts.isVariableStatement(statement) && hasExportModifier(statement)) {
146
+ if ((statement.declarationList.flags & ts.NodeFlags.Const) === 0) {
147
+ throw new Unreadable("an exported `let` or `var` is not read");
148
+ }
149
+ for (const decl of statement.declarationList.declarations) {
150
+ if (ts.isIdentifier(decl.name) && decl.initializer) named.set(decl.name.text, decl.initializer);
151
+ else throw new Unreadable("a destructured export is not read");
152
+ }
153
+ } else if (hasExportModifier(statement) && hasDefaultModifier(statement)) {
154
+ throw new Unreadable("the default export is not an object");
155
+ }
156
+ }
157
+
158
+ const configExpr = defaultExport ?? named.get("config");
159
+ if (configExpr === undefined) {
160
+ // The module's named exports are the config.
161
+ return Object.fromEntries(
162
+ fields.map((f) => {
163
+ const expr = named.get(f);
164
+ return [f, expr === undefined ? undefined : fold(expr, consts)];
165
+ }),
166
+ );
167
+ }
168
+ const obj = configObject(configExpr, consts, new Set());
169
+ return Object.fromEntries(fields.map((f) => [f, fieldOfObject(obj, f, consts)]));
170
+ }
171
+
172
+ /** The object literal a config export reduces to, following `const` aliases. */
173
+ function configObject(
174
+ node: ts.Expression,
175
+ consts: Map<string, ts.Expression>,
176
+ seen: Set<string>,
177
+ ): ts.ObjectLiteralExpression {
178
+ const inner = unwrap(node);
179
+ if (ts.isObjectLiteralExpression(inner)) return inner;
180
+ if (ts.isIdentifier(inner)) {
181
+ const init = consts.get(inner.text);
182
+ if (init === undefined || seen.has(inner.text)) {
183
+ throw new Unreadable(`the exported config \`${inner.text}\` is not a top-level const of this file`);
184
+ }
185
+ seen.add(inner.text);
186
+ return configObject(init, consts, seen);
187
+ }
188
+ throw new Unreadable("the exported config is not an object literal");
189
+ }
190
+
191
+ /** The value `field` takes in a config object literal, or `undefined` when it sets none. */
192
+ function fieldOfObject(obj: ts.ObjectLiteralExpression, field: string, consts: Map<string, ts.Expression>): unknown {
193
+ let value: unknown = undefined;
194
+ for (const prop of obj.properties) {
195
+ if (ts.isSpreadAssignment(prop)) {
196
+ const spread = fold(prop.expression, consts);
197
+ if (spread !== null && typeof spread === "object" && Object.prototype.hasOwnProperty.call(spread, field)) {
198
+ value = (spread as Record<string, unknown>)[field];
199
+ }
200
+ continue;
201
+ }
202
+ if (prop.name === undefined) continue;
203
+ const name = propName(prop.name);
204
+ if (name !== field) continue;
205
+ if (ts.isPropertyAssignment(prop)) value = fold(prop.initializer, consts);
206
+ else if (ts.isShorthandPropertyAssignment(prop)) value = fold(prop.name, consts);
207
+ else throw new Unreadable(`\`${field}\` is a method or accessor`);
208
+ }
209
+ return value;
210
+ }
211
+
212
+ function unwrap(node: ts.Expression): ts.Expression {
213
+ let current = node;
214
+ while (
215
+ ts.isParenthesizedExpression(current) ||
216
+ ts.isAsExpression(current) ||
217
+ ts.isSatisfiesExpression(current) ||
218
+ ts.isNonNullExpression(current) ||
219
+ ts.isTypeAssertionExpression(current)
220
+ ) {
221
+ current = current.expression;
222
+ }
223
+ return current;
224
+ }
225
+
226
+ function hasExportModifier(node: ts.Node): boolean {
227
+ return (ts.canHaveModifiers(node) && ts.getModifiers(node)?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) ?? false;
228
+ }
229
+
230
+ function hasDefaultModifier(node: ts.Node): boolean {
231
+ return (ts.canHaveModifiers(node) && ts.getModifiers(node)?.some((m) => m.kind === ts.SyntaxKind.DefaultKeyword)) ?? false;
232
+ }
@@ -468,10 +468,10 @@ describe("exclude / include discovery globs (#2519)", () => {
468
468
  await expect(resolveDiscoveryGlobs(TEST_DIR)).rejects.toThrow(message);
469
469
  });
470
470
 
471
- test("resolveDiscoveryGlobs is undefined without exclude, so include alone changes nothing", async () => {
471
+ test("resolveDiscoveryGlobs returns include alone, since it re-admits the walk's default skips (#2527)", async () => {
472
472
  expect(await resolveDiscoveryGlobs(TEST_DIR)).toBeUndefined();
473
473
  writeFileSync(join(TEST_DIR, "chant.config.json"), JSON.stringify({ include: ["ops/**"] }));
474
- expect(await resolveDiscoveryGlobs(TEST_DIR)).toBeUndefined();
474
+ expect(await resolveDiscoveryGlobs(TEST_DIR)).toEqual({ root: TEST_DIR, exclude: [], include: ["ops/**"] });
475
475
  });
476
476
 
477
477
  test("resolveDiscoveryGlobs anchors the globs at the project config's directory, past a lint fragment", async () => {
package/src/config.ts CHANGED
@@ -10,7 +10,7 @@ import type { BuildParamsConfig } from "./build-params";
10
10
  import type { BuildParamProvenance } from "./provenance";
11
11
  import { findProjectConfig } from "./project-root";
12
12
  import { evaluateProjectConfig } from "./config-sandbox";
13
- import { lexiconNames, registerLexiconDeclarations, type LexiconDeclaration } from "./lexicon-module";
13
+ import { lexiconNames, pathLexiconRoot, registerLexiconDeclarations, type LexiconDeclaration } from "./lexicon-module";
14
14
 
15
15
  /**
16
16
  * One project-declared environment (chant #1166). Historically always a bare
@@ -131,6 +131,7 @@ const LexiconEntrySchema = z.union([
131
131
  z.object({
132
132
  name: z.string().min(1),
133
133
  module: z.string().min(1),
134
+ root: z.string().min(1).optional(),
134
135
  }).strict(),
135
136
  ]);
136
137
 
@@ -195,6 +196,7 @@ export const ChantConfigSchema = z.object({
195
196
  }).optional(),
196
197
  exclude: z.array(z.string().min(1)).optional(),
197
198
  include: z.array(z.string().min(1)).optional(),
199
+ rootOnly: z.boolean().optional(),
198
200
  }).passthrough();
199
201
 
200
202
  /**
@@ -207,7 +209,8 @@ export interface ChantConfig {
207
209
  * Lexicons to load. A bare name (`"aws"`) loads the package
208
210
  * `@intentius/chant-lexicon-aws`. `{ name, module }` (#2520) loads a
209
211
  * project-local lexicon from a module path, resolved against the directory
210
- * holding this config; see `./lexicon-module.ts`.
212
+ * holding this config; see `./lexicon-module.ts`. An optional `root`
213
+ * (chant#2590) names the directory holding the lexicon's other files.
211
214
  *
212
215
  * Once loaded, this holds names only: the loader replaces each
213
216
  * `{ name, module }` entry with its name and records the path in
@@ -453,19 +456,33 @@ export interface ChantConfig {
453
456
  * import or lint a matching file. A pattern matches a file when it matches
454
457
  * the file's path or the path of any directory above it, so `"ops"` and
455
458
  * `"ops/**"` both skip everything under `ops/`. Matched with picomatch,
456
- * dot files included. See `./discovery/files.ts`'s `compileDiscoveryFilter`.
459
+ * dot files included. Op discovery and component discovery honour it too
460
+ * (#2527). See `./discovery/walk.ts`.
457
461
  */
458
462
  exclude?: string[];
459
463
 
460
464
  /**
461
- * Globs that re-admit files {@link exclude} would skip. `include` always
462
- * wins over `exclude`, and it never narrows discovery on its own: a file
463
- * no `exclude` pattern matches is discovered whether or not it matches an
464
- * `include`. It does not re-admit what discovery skips unconditionally
465
- * (`node_modules`, test files, child projects, the generated and skip
466
- * markers).
465
+ * Globs that re-admit files {@link exclude} would skip, and, since #2527,
466
+ * files the discovery walk skips by default: dot-directories, `dist`,
467
+ * git-ignored paths and child projects. `include` always wins over
468
+ * `exclude`, and it never narrows discovery on its own: a file nothing
469
+ * skips is discovered whether or not it matches an `include`. It never
470
+ * re-admits `node_modules`, `.git`, test files, workspace members, or
471
+ * files carrying the generated or skip marker.
467
472
  */
468
473
  include?: string[];
474
+
475
+ /**
476
+ * Lets `chant build` and `chant lint` run on this project when it is the
477
+ * root of a declared workspace (#2537, #2524 D0). Without it, or the
478
+ * `--root-only` flag, both commands refuse there with `WSP000` and point to
479
+ * `chant workspace build` and `lint`. With it they act on the root project
480
+ * alone. Member directories and example-group matches are left out of
481
+ * discovery either way, as they are for every walk under a declaration
482
+ * (#2527, `workspaceMemberDirs` in `./discovery/walk.ts`). It means nothing
483
+ * in a project with no `chant.workspace.json`.
484
+ */
485
+ rootOnly?: boolean;
469
486
  }
470
487
 
471
488
  /**
@@ -561,7 +578,7 @@ export async function loadChantConfigUpward(startDir: string): Promise<ResolvedC
561
578
  }
562
579
 
563
580
  /** The walk {@link loadChantConfigUpward} makes, lint-only fragments skipped, without its warning. */
564
- function findProjectConfigPastFragments(startDir: string): { dir: string; configPath?: string } {
581
+ export function findProjectConfigPastFragments(startDir: string): { dir: string; configPath?: string } {
565
582
  let { dir, configPath } = findProjectConfig(startDir);
566
583
  while (configPath && isLintOnlyFragment(configPath)) {
567
584
  const parent = dirname(dir);
@@ -582,8 +599,9 @@ export interface DiscoveryGlobs {
582
599
  /**
583
600
  * The `exclude`/`include` globs of the project `startDir` belongs to, found by
584
601
  * the same upward walk as {@link loadChantConfigUpward}. `undefined` when the
585
- * project declares no `exclude`, which leaves discovery exactly as it was
586
- * before the keys existed: `include` only ever re-admits what `exclude` skips.
602
+ * project declares neither. `include` re-admits what `exclude` skips, and
603
+ * also what the discovery walk skips by default (#2527), so it means
604
+ * something on its own.
587
605
  *
588
606
  * A config that fails to load is left to the command's own config load to
589
607
  * report, so discovery keeps working where it did before, unless the failure
@@ -600,8 +618,10 @@ export async function resolveDiscoveryGlobs(startDir: string): Promise<Discovery
600
618
  if (err instanceof InvalidChantConfigError && (err.key === "exclude" || err.key === "include")) throw err;
601
619
  return undefined;
602
620
  }
603
- if (!config.exclude || config.exclude.length === 0) return undefined;
604
- return { root: dir, exclude: config.exclude, include: config.include ?? [] };
621
+ const exclude = config.exclude ?? [];
622
+ const include = config.include ?? [];
623
+ if (exclude.length === 0 && include.length === 0) return undefined;
624
+ return { root: dir, exclude, include };
605
625
  }
606
626
 
607
627
  /**
@@ -941,7 +961,20 @@ function normalizeConfig(raw: Record<string, unknown>, source?: string): LoadedC
941
961
  // the loaders and the entry becomes its name, so every reader of `lexicons`
942
962
  // keeps seeing names. A config of plain names is returned untouched.
943
963
  const lexicons = (raw as ChantConfig).lexicons;
944
- const recorded = registerLexiconDeclarations(lexicons, source !== undefined ? dirname(source) : process.cwd());
964
+ const baseDir = source !== undefined ? dirname(source) : process.cwd();
965
+ const sourceDir = (raw as ChantConfig).sourceDir;
966
+ // chant#2590 — a declared `root` must stay inside the project and must not
967
+ // contain its source directory. Refused here rather than narrowed, since
968
+ // the project asked for it by name.
969
+ for (const [i, entry] of (lexicons ?? []).entries()) {
970
+ if (typeof entry === "string" || entry.root === undefined) continue;
971
+ const { problem } = pathLexiconRoot(entry, baseDir, sourceDir);
972
+ if (problem !== undefined) {
973
+ const loc = source ? ` in ${source}` : "";
974
+ throw new InvalidChantConfigError(`Invalid chant config${loc}: lexicons.${i}.root: ${problem}`, "lexicons");
975
+ }
976
+ }
977
+ const recorded = registerLexiconDeclarations(lexicons, baseDir, sourceDir);
945
978
  if (Object.keys(recorded).length > 0) {
946
979
  return { ...(raw as ChantConfig), lexicons: lexiconNames(lexicons), lexiconModules: recorded };
947
980
  }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * chant #2514 — `contentDigest` is real SHA-256 over UTF-8, and the manifest
3
+ * and SBOM digests built on it separate inputs one byte apart.
4
+ */
5
+ import { describe, expect, test } from "vitest";
6
+ import { createHash } from "node:crypto";
7
+ import { contentDigest } from "./content-digest";
8
+ import { canonicalJson } from "./effect-receipt";
9
+ import {
10
+ addArchiveEntry,
11
+ computeManifestDigest,
12
+ contentDigest as archiveContentDigest,
13
+ createBuildArchiveManifest,
14
+ } from "./components/verbs/build-archive";
15
+ import { createGenerateSbomCapability } from "./components/verbs/sbom";
16
+ import type { SbomDocument, SbomGenerator } from "./components/verbs/sbom-generator";
17
+ import { computePlanDigest } from "./lifecycle/plan-digest";
18
+ import { hashProps } from "./lifecycle/digest";
19
+ import { isLegacyContentDigest } from "./lifecycle/legacy-digest";
20
+
21
+ describe("contentDigest (#2514)", () => {
22
+ test("matches the FIPS 180-2 test vectors", () => {
23
+ expect(contentDigest("abc")).toBe("sha256:ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad");
24
+ expect(contentDigest("")).toBe("sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
25
+ });
26
+
27
+ test("hashes the UTF-8 bytes, not UTF-16 code units", () => {
28
+ const text = "héllo, 世界 🌍";
29
+ const expected = createHash("sha256").update(Buffer.from(text, "utf8")).digest("hex");
30
+ expect(contentDigest(text)).toBe(`sha256:${expected}`);
31
+ });
32
+
33
+ test("the build archive exports the same function", () => {
34
+ expect(archiveContentDigest).toBe(contentDigest);
35
+ });
36
+
37
+ test("its output is never mistaken for the old form", () => {
38
+ for (const input of ["", "x", "hello", "{}"]) {
39
+ expect(isLegacyContentDigest(contentDigest(input))).toBe(false);
40
+ }
41
+ });
42
+
43
+ test("inputs one byte apart get different digests", () => {
44
+ expect(contentDigest("template-a")).not.toBe(contentDigest("template-b"));
45
+ expect(contentDigest("abc")).not.toBe(contentDigest("abc "));
46
+ });
47
+ });
48
+
49
+ describe("manifest digest (#2514)", () => {
50
+ const entry = (digest: string, path = "web.json") => ({ kind: "template" as const, path, digest });
51
+
52
+ test("is contentDigest over the canonical JSON of the sorted entries", () => {
53
+ const a = entry(contentDigest("{}"), "b.json");
54
+ const b = entry(contentDigest("[]"), "a.json");
55
+ const expected = contentDigest(
56
+ canonicalJson([
57
+ { kind: b.kind, path: b.path, digest: b.digest },
58
+ { kind: a.kind, path: a.path, digest: a.digest },
59
+ ]),
60
+ );
61
+ expect(computeManifestDigest([a, b])).toBe(expected);
62
+ expect(computeManifestDigest([b, a])).toBe(expected);
63
+ });
64
+
65
+ test("templates one byte apart give different entry and manifest digests", () => {
66
+ const base = createBuildArchiveManifest("web", { now: () => new Date(0) });
67
+ const one = addArchiveEntry(base, entry(contentDigest('{"Resources":{"A":1}}')));
68
+ const two = addArchiveEntry(base, entry(contentDigest('{"Resources":{"A":2}}')));
69
+ expect(one.contents[0]!.digest).not.toBe(two.contents[0]!.digest);
70
+ expect(one.manifestDigest).not.toBe(two.manifestDigest);
71
+ });
72
+
73
+ test("a path cannot be read as a separator", () => {
74
+ // Under the old `kind:path:digest` join these two could be confused.
75
+ const x = computeManifestDigest([entry("sha256:1", "a:template:b")]);
76
+ const y = computeManifestDigest([entry("template:b:sha256:1", "a")]);
77
+ expect(x).not.toBe(y);
78
+ });
79
+ });
80
+
81
+ describe("SBOM digest (#2514)", () => {
82
+ function fixedGenerator(bytes: string): SbomGenerator {
83
+ const doc = async (): Promise<SbomDocument> => ({
84
+ format: "spdx",
85
+ mediaType: "application/spdx+json",
86
+ bytes,
87
+ generator: "test",
88
+ });
89
+ return { forImage: doc, forJar: doc, forZip: doc, forDir: doc };
90
+ }
91
+
92
+ test("SBOMs one byte apart get different digests, each real SHA-256", async () => {
93
+ const ctx = { env: "dev", component: "web" };
94
+ const input = { artifactType: "image" as const, path: "archive/web.tar", digest: contentDigest("image") };
95
+ const a = await createGenerateSbomCapability(fixedGenerator('{"packages":[1]}')).run(ctx, input);
96
+ const b = await createGenerateSbomCapability(fixedGenerator('{"packages":[2]}')).run(ctx, input);
97
+ expect(a.digest).toBe(contentDigest('{"packages":[1]}'));
98
+ expect(a.digest).not.toBe(b.digest);
99
+ expect(a.manifest.manifestDigest).not.toBe(b.manifest.manifestDigest);
100
+ });
101
+ });
102
+
103
+ describe("other hashes use contentDigest over canonicalJson (#2514)", () => {
104
+ test("a plan digest", () => {
105
+ const subject = { b: 1, a: [true, null] };
106
+ expect(computePlanDigest("lifecycle-diff", subject)).toBe(
107
+ contentDigest(canonicalJson({ kind: "lifecycle-diff", subject })),
108
+ );
109
+ });
110
+
111
+ test("a plan digest orders keys by code unit, not by locale", () => {
112
+ // localeCompare puts "a" before "B"; canonicalJson puts "B" first.
113
+ expect(computePlanDigest("k", { a: 1, B: 2 })).toBe(contentDigest('{"kind":"k","subject":{"B":2,"a":1}}'));
114
+ });
115
+
116
+ test("a props hash, which drops what JSON cannot hold instead of throwing", () => {
117
+ const props = { Name: "x", fn: () => 1, nested: { z: 1, a: undefined } };
118
+ expect(hashProps(props)).toBe(contentDigest('{"Name":"x","nested":{"z":1}}').slice("sha256:".length));
119
+ });
120
+ });
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The one content digest chant computes itself (chant #2514).
3
+ *
4
+ * `contentDigest(x)` is `"sha256:"` followed by the lowercase hex SHA-256 of
5
+ * the UTF-8 bytes of `x`. Anything core hashes goes through it, and anything
6
+ * structured is first put in canonical form with `canonicalJson`
7
+ * (./effect-receipt.ts), so the same value always hashes the same way. The
8
+ * build archive re-exports it from ./components/verbs/build-archive.ts, where
9
+ * it used to live.
10
+ *
11
+ * Before #2514 this function computed a 32-bit string hash and repeated its
12
+ * eight hex digits eight times. Values recorded then are recognised and
13
+ * flagged by ./lifecycle/legacy-digest.ts.
14
+ */
15
+
16
+ import { createHash } from "node:crypto";
17
+
18
+ /** `sha256:<64 hex>` over the UTF-8 bytes of `input`. */
19
+ export function contentDigest(input: string): string {
20
+ return `sha256:${createHash("sha256").update(input, "utf8").digest("hex")}`;
21
+ }
@@ -189,11 +189,11 @@ describe("findInfraFiles — project exclude/include globs (#2519)", () => {
189
189
  ".stage/copy.ts": "export const copy = {};",
190
190
  };
191
191
 
192
- test("with neither key configured, discovery returns exactly what it did before", async () => {
192
+ test("with neither key configured, every file but the dot directory's is discovered", async () => {
193
193
  await withTestDir(async (testDir) => {
194
194
  await project(testDir, { lexicons: ["aws"] }, LAYOUT);
195
195
  expect(rel(testDir, await findInfraFiles(testDir))).toEqual([
196
- ".stage/copy.ts", "ops/keep.ts", "ops/run.ts", "src/app.ts", "src/lib/util.ts",
196
+ "ops/keep.ts", "ops/run.ts", "src/app.ts", "src/lib/util.ts",
197
197
  ]);
198
198
  });
199
199
  });
@@ -201,14 +201,14 @@ describe("findInfraFiles — project exclude/include globs (#2519)", () => {
201
201
  test.each([["ops"], ["ops/**"], ["ops/*.ts"]])("exclude %s skips everything under ops/", async (pattern) => {
202
202
  await withTestDir(async (testDir) => {
203
203
  await project(testDir, { exclude: [pattern] }, LAYOUT);
204
- expect(rel(testDir, await findInfraFiles(testDir))).toEqual([".stage/copy.ts", "src/app.ts", "src/lib/util.ts"]);
204
+ expect(rel(testDir, await findInfraFiles(testDir))).toEqual(["src/app.ts", "src/lib/util.ts"]);
205
205
  });
206
206
  });
207
207
 
208
- test("patterns match dot directories", async () => {
208
+ test("include re-admits a dot directory the walk skips by default (#2527)", async () => {
209
209
  await withTestDir(async (testDir) => {
210
- await project(testDir, { exclude: [".stage"] }, LAYOUT);
211
- expect(rel(testDir, await findInfraFiles(testDir))).not.toContain(".stage/copy.ts");
210
+ await project(testDir, { include: [".stage"] }, LAYOUT);
211
+ expect(rel(testDir, await findInfraFiles(testDir))).toContain(".stage/copy.ts");
212
212
  });
213
213
  });
214
214
 
@@ -216,7 +216,7 @@ describe("findInfraFiles — project exclude/include globs (#2519)", () => {
216
216
  await withTestDir(async (testDir) => {
217
217
  await project(testDir, { exclude: ["ops"], include: ["ops/keep.ts"] }, LAYOUT);
218
218
  expect(rel(testDir, await findInfraFiles(testDir))).toEqual([
219
- ".stage/copy.ts", "ops/keep.ts", "src/app.ts", "src/lib/util.ts",
219
+ "ops/keep.ts", "src/app.ts", "src/lib/util.ts",
220
220
  ]);
221
221
  });
222
222
  });
@@ -231,7 +231,7 @@ describe("findInfraFiles — project exclude/include globs (#2519)", () => {
231
231
  test("include alone changes nothing", async () => {
232
232
  await withTestDir(async (testDir) => {
233
233
  await project(testDir, { include: ["src"] }, LAYOUT);
234
- expect(await findInfraFiles(testDir)).toHaveLength(5);
234
+ expect(await findInfraFiles(testDir)).toHaveLength(4);
235
235
  });
236
236
  });
237
237
 
@@ -252,9 +252,9 @@ describe("findInfraFiles — project exclude/include globs (#2519)", () => {
252
252
  test("explicit globs override the config; null applies none", async () => {
253
253
  await withTestDir(async (testDir) => {
254
254
  await project(testDir, { exclude: ["ops"] }, LAYOUT);
255
- expect(await findInfraFiles(testDir, { globs: null })).toHaveLength(5);
255
+ expect(await findInfraFiles(testDir, { globs: null })).toHaveLength(4);
256
256
  const only = await findInfraFiles(testDir, { globs: { root: testDir, exclude: ["src"], include: [] } });
257
- expect(rel(testDir, only)).toEqual([".stage/copy.ts", "ops/keep.ts", "ops/run.ts"]);
257
+ expect(rel(testDir, only)).toEqual(["ops/keep.ts", "ops/run.ts"]);
258
258
  });
259
259
  });
260
260
  });