@intentius/chant 0.13.0 → 0.15.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 (453) hide show
  1. package/dist/audit/catalog.d.ts +45 -5
  2. package/dist/audit/catalog.d.ts.map +1 -1
  3. package/dist/audit/fetch.d.ts.map +1 -1
  4. package/dist/audit/proof.d.ts +3 -0
  5. package/dist/audit/proof.d.ts.map +1 -1
  6. package/dist/audit/report-model.d.ts +4 -1
  7. package/dist/audit/report-model.d.ts.map +1 -1
  8. package/dist/audit/report.d.ts +3 -0
  9. package/dist/audit/report.d.ts.map +1 -1
  10. package/dist/audit/rules-doc.d.ts +8 -2
  11. package/dist/audit/rules-doc.d.ts.map +1 -1
  12. package/dist/cli/commands/audit.d.ts.map +1 -1
  13. package/dist/cli/commands/doctor.d.ts.map +1 -1
  14. package/dist/cli/commands/lexicon-rolling-upgrade.d.ts +37 -0
  15. package/dist/cli/commands/lexicon-rolling-upgrade.d.ts.map +1 -0
  16. package/dist/cli/commands/lexicon-surface-diff.d.ts +48 -0
  17. package/dist/cli/commands/lexicon-surface-diff.d.ts.map +1 -0
  18. package/dist/cli/commands/lint.d.ts.map +1 -1
  19. package/dist/cli/commands/pinned-upgrade.d.ts +41 -0
  20. package/dist/cli/commands/pinned-upgrade.d.ts.map +1 -0
  21. package/dist/cli/handlers/build.d.ts.map +1 -1
  22. package/dist/cli/handlers/components.d.ts +39 -0
  23. package/dist/cli/handlers/components.d.ts.map +1 -0
  24. package/dist/cli/handlers/dev.d.ts +3 -0
  25. package/dist/cli/handlers/dev.d.ts.map +1 -1
  26. package/dist/cli/handlers/graph.d.ts +4 -2
  27. package/dist/cli/handlers/graph.d.ts.map +1 -1
  28. package/dist/cli/handlers/misc.d.ts.map +1 -1
  29. package/dist/cli/handlers/run.d.ts +49 -0
  30. package/dist/cli/handlers/run.d.ts.map +1 -1
  31. package/dist/cli/main.d.ts.map +1 -1
  32. package/dist/cli/registry.d.ts +35 -0
  33. package/dist/cli/registry.d.ts.map +1 -1
  34. package/dist/codegen/fetch.d.ts +22 -0
  35. package/dist/codegen/fetch.d.ts.map +1 -1
  36. package/dist/codegen/lexicon-regen.d.ts +110 -0
  37. package/dist/codegen/lexicon-regen.d.ts.map +1 -0
  38. package/dist/codegen/pinned-upgrade.d.ts +153 -0
  39. package/dist/codegen/pinned-upgrade.d.ts.map +1 -0
  40. package/dist/codegen/rolling-upgrade.d.ts +138 -0
  41. package/dist/codegen/rolling-upgrade.d.ts.map +1 -0
  42. package/dist/codegen/surface-snapshot.d.ts +173 -0
  43. package/dist/codegen/surface-snapshot.d.ts.map +1 -0
  44. package/dist/codegen/version-bump.d.ts +21 -0
  45. package/dist/codegen/version-bump.d.ts.map +1 -0
  46. package/dist/components/auto-release.d.ts +93 -0
  47. package/dist/components/auto-release.d.ts.map +1 -0
  48. package/dist/components/builders.d.ts +37 -0
  49. package/dist/components/builders.d.ts.map +1 -0
  50. package/dist/components/capability-plugin-loader.d.ts +111 -0
  51. package/dist/components/capability-plugin-loader.d.ts.map +1 -0
  52. package/dist/components/capability-plugin-schema.d.ts +24 -0
  53. package/dist/components/capability-plugin-schema.d.ts.map +1 -0
  54. package/dist/components/capability-plugin.d.ts +85 -0
  55. package/dist/components/capability-plugin.d.ts.map +1 -0
  56. package/dist/components/capability.d.ts +89 -0
  57. package/dist/components/capability.d.ts.map +1 -0
  58. package/dist/components/cli-support.d.ts +204 -0
  59. package/dist/components/cli-support.d.ts.map +1 -0
  60. package/dist/components/component.d.ts +179 -0
  61. package/dist/components/component.d.ts.map +1 -0
  62. package/dist/components/config-defaults.d.ts +41 -0
  63. package/dist/components/config-defaults.d.ts.map +1 -0
  64. package/dist/components/discover.d.ts +60 -0
  65. package/dist/components/discover.d.ts.map +1 -0
  66. package/dist/components/driver-output.d.ts +20 -0
  67. package/dist/components/driver-output.d.ts.map +1 -0
  68. package/dist/components/driver.d.ts +209 -0
  69. package/dist/components/driver.d.ts.map +1 -0
  70. package/dist/components/index.d.ts +22 -0
  71. package/dist/components/index.d.ts.map +1 -0
  72. package/dist/components/manifest-persistence.d.ts +79 -0
  73. package/dist/components/manifest-persistence.d.ts.map +1 -0
  74. package/dist/components/pilots/alb-ecs.pilot.d.ts +27 -0
  75. package/dist/components/pilots/alb-ecs.pilot.d.ts.map +1 -0
  76. package/dist/components/pilots/dynamodb.pilot.d.ts +22 -0
  77. package/dist/components/pilots/dynamodb.pilot.d.ts.map +1 -0
  78. package/dist/components/pilots/jar-emr.pilot.d.ts +37 -0
  79. package/dist/components/pilots/jar-emr.pilot.d.ts.map +1 -0
  80. package/dist/components/pilots/lambda.pilot.d.ts +35 -0
  81. package/dist/components/pilots/lambda.pilot.d.ts.map +1 -0
  82. package/dist/components/pilots/neo4j-fanout.pilot.d.ts +17 -0
  83. package/dist/components/pilots/neo4j-fanout.pilot.d.ts.map +1 -0
  84. package/dist/components/pilots/project.d.ts +13 -0
  85. package/dist/components/pilots/project.d.ts.map +1 -0
  86. package/dist/components/presets/ecs-fargate.d.ts +59 -0
  87. package/dist/components/presets/ecs-fargate.d.ts.map +1 -0
  88. package/dist/components/presets/index.d.ts +29 -0
  89. package/dist/components/presets/index.d.ts.map +1 -0
  90. package/dist/components/presets/lambda.d.ts +35 -0
  91. package/dist/components/presets/lambda.d.ts.map +1 -0
  92. package/dist/components/presets/single-host-compose.d.ts +43 -0
  93. package/dist/components/presets/single-host-compose.d.ts.map +1 -0
  94. package/dist/components/registry.d.ts +29 -0
  95. package/dist/components/registry.d.ts.map +1 -0
  96. package/dist/components/starter-plugin.d.ts +51 -0
  97. package/dist/components/starter-plugin.d.ts.map +1 -0
  98. package/dist/components/temporal-codegen-loader.d.ts +27 -0
  99. package/dist/components/temporal-codegen-loader.d.ts.map +1 -0
  100. package/dist/components/verbs/__tests__/mock-cloud-executor.d.ts +35 -0
  101. package/dist/components/verbs/__tests__/mock-cloud-executor.d.ts.map +1 -0
  102. package/dist/components/verbs/__tests__/mock-process-runner.d.ts +45 -0
  103. package/dist/components/verbs/__tests__/mock-process-runner.d.ts.map +1 -0
  104. package/dist/components/verbs/__tests__/mock-sbom-generator.d.ts +33 -0
  105. package/dist/components/verbs/__tests__/mock-sbom-generator.d.ts.map +1 -0
  106. package/dist/components/verbs/bom-writer.d.ts +155 -0
  107. package/dist/components/verbs/bom-writer.d.ts.map +1 -0
  108. package/dist/components/verbs/build-archive.d.ts +205 -0
  109. package/dist/components/verbs/build-archive.d.ts.map +1 -0
  110. package/dist/components/verbs/build.d.ts +150 -0
  111. package/dist/components/verbs/build.d.ts.map +1 -0
  112. package/dist/components/verbs/cloud-executor.d.ts +88 -0
  113. package/dist/components/verbs/cloud-executor.d.ts.map +1 -0
  114. package/dist/components/verbs/component-bom.d.ts +103 -0
  115. package/dist/components/verbs/component-bom.d.ts.map +1 -0
  116. package/dist/components/verbs/index.d.ts +49 -0
  117. package/dist/components/verbs/index.d.ts.map +1 -0
  118. package/dist/components/verbs/license-policy.d.ts +56 -0
  119. package/dist/components/verbs/license-policy.d.ts.map +1 -0
  120. package/dist/components/verbs/lockfile-sbom-generator.d.ts +81 -0
  121. package/dist/components/verbs/lockfile-sbom-generator.d.ts.map +1 -0
  122. package/dist/components/verbs/process-runner.d.ts +64 -0
  123. package/dist/components/verbs/process-runner.d.ts.map +1 -0
  124. package/dist/components/verbs/reproducibility.d.ts +80 -0
  125. package/dist/components/verbs/reproducibility.d.ts.map +1 -0
  126. package/dist/components/verbs/sbom-generator.d.ts +157 -0
  127. package/dist/components/verbs/sbom-generator.d.ts.map +1 -0
  128. package/dist/components/verbs/sbom.d.ts +79 -0
  129. package/dist/components/verbs/sbom.d.ts.map +1 -0
  130. package/dist/components/verbs/shell.d.ts +35 -0
  131. package/dist/components/verbs/shell.d.ts.map +1 -0
  132. package/dist/components/verbs/sign.d.ts +222 -0
  133. package/dist/components/verbs/sign.d.ts.map +1 -0
  134. package/dist/components/verbs/stub.d.ts +18 -0
  135. package/dist/components/verbs/stub.d.ts.map +1 -0
  136. package/dist/components/verbs/tool-sbom-generator.d.ts +51 -0
  137. package/dist/components/verbs/tool-sbom-generator.d.ts.map +1 -0
  138. package/dist/components/verbs/verify.d.ts +106 -0
  139. package/dist/components/verbs/verify.d.ts.map +1 -0
  140. package/dist/components/verbs/vex.d.ts +65 -0
  141. package/dist/components/verbs/vex.d.ts.map +1 -0
  142. package/dist/components/verbs/vuln-gate.d.ts +91 -0
  143. package/dist/components/verbs/vuln-gate.d.ts.map +1 -0
  144. package/dist/components/verbs/vuln-scan.d.ts +116 -0
  145. package/dist/components/verbs/vuln-scan.d.ts.map +1 -0
  146. package/dist/components/verbs/wait-verify.d.ts +85 -0
  147. package/dist/components/verbs/wait-verify.d.ts.map +1 -0
  148. package/dist/config.d.ts +205 -0
  149. package/dist/config.d.ts.map +1 -1
  150. package/dist/index.d.ts +4 -0
  151. package/dist/index.d.ts.map +1 -1
  152. package/dist/lexicon.d.ts +78 -0
  153. package/dist/lexicon.d.ts.map +1 -1
  154. package/dist/lifecycle/build-ledger-store.d.ts +121 -0
  155. package/dist/lifecycle/build-ledger-store.d.ts.map +1 -0
  156. package/dist/lifecycle/build-ledger.d.ts +225 -0
  157. package/dist/lifecycle/build-ledger.d.ts.map +1 -0
  158. package/dist/lifecycle/git.d.ts +84 -0
  159. package/dist/lifecycle/git.d.ts.map +1 -1
  160. package/dist/lifecycle/index.d.ts +5 -0
  161. package/dist/lifecycle/index.d.ts.map +1 -1
  162. package/dist/lifecycle/oras-referrer-lookup.d.ts +41 -0
  163. package/dist/lifecycle/oras-referrer-lookup.d.ts.map +1 -0
  164. package/dist/lifecycle/release-ledger.d.ts +122 -0
  165. package/dist/lifecycle/release-ledger.d.ts.map +1 -0
  166. package/dist/lifecycle/status.d.ts +150 -0
  167. package/dist/lifecycle/status.d.ts.map +1 -0
  168. package/dist/lint/component-checks.d.ts +95 -0
  169. package/dist/lint/component-checks.d.ts.map +1 -0
  170. package/dist/lint/engine.d.ts +23 -0
  171. package/dist/lint/engine.d.ts.map +1 -1
  172. package/dist/lint/presets/strict.json +8 -1
  173. package/dist/lint/rules/__fixtures__/comp/comp000-discovery-error/broken-a.component.d.ts +4 -0
  174. package/dist/lint/rules/__fixtures__/comp/comp000-discovery-error/broken-a.component.d.ts.map +1 -0
  175. package/dist/lint/rules/__fixtures__/comp/comp000-discovery-error/broken-b.component.d.ts +4 -0
  176. package/dist/lint/rules/__fixtures__/comp/comp000-discovery-error/broken-b.component.d.ts.map +1 -0
  177. package/dist/lint/rules/__fixtures__/comp/comp001/fail/orphan-lib.component.d.ts +4 -0
  178. package/dist/lint/rules/__fixtures__/comp/comp001/fail/orphan-lib.component.d.ts.map +1 -0
  179. package/dist/lint/rules/__fixtures__/comp/comp001/pass/emr-job.component.d.ts +4 -0
  180. package/dist/lint/rules/__fixtures__/comp/comp001/pass/emr-job.component.d.ts.map +1 -0
  181. package/dist/lint/rules/__fixtures__/comp/comp001/pass/jar-lib.component.d.ts +4 -0
  182. package/dist/lint/rules/__fixtures__/comp/comp001/pass/jar-lib.component.d.ts.map +1 -0
  183. package/dist/lint/rules/__fixtures__/comp/comp002/fail/search-service.component.d.ts +10 -0
  184. package/dist/lint/rules/__fixtures__/comp/comp002/fail/search-service.component.d.ts.map +1 -0
  185. package/dist/lint/rules/__fixtures__/comp/comp002/pass/jar-lib.component.d.ts +3 -0
  186. package/dist/lint/rules/__fixtures__/comp/comp002/pass/jar-lib.component.d.ts.map +1 -0
  187. package/dist/lint/rules/__fixtures__/comp/comp002/pass/search-service.component.d.ts +4 -0
  188. package/dist/lint/rules/__fixtures__/comp/comp002/pass/search-service.component.d.ts.map +1 -0
  189. package/dist/lint/rules/__fixtures__/comp/comp003/fail/migrator.component.d.ts +4 -0
  190. package/dist/lint/rules/__fixtures__/comp/comp003/fail/migrator.component.d.ts.map +1 -0
  191. package/dist/lint/rules/__fixtures__/comp/comp003/pass/migrator.component.d.ts +4 -0
  192. package/dist/lint/rules/__fixtures__/comp/comp003/pass/migrator.component.d.ts.map +1 -0
  193. package/dist/lint/rules/__fixtures__/comp/comp003/pass/orders-table.component.d.ts +4 -0
  194. package/dist/lint/rules/__fixtures__/comp/comp003/pass/orders-table.component.d.ts.map +1 -0
  195. package/dist/lint/rules/__fixtures__/comp/comp004/fail/neo4j-cluster.component.d.ts +4 -0
  196. package/dist/lint/rules/__fixtures__/comp/comp004/fail/neo4j-cluster.component.d.ts.map +1 -0
  197. package/dist/lint/rules/__fixtures__/comp/comp004/pass/neo4j-cluster.component.d.ts +4 -0
  198. package/dist/lint/rules/__fixtures__/comp/comp004/pass/neo4j-cluster.component.d.ts.map +1 -0
  199. package/dist/lint/rules/__fixtures__/comp/comp004/pass/orders-table.component.d.ts +4 -0
  200. package/dist/lint/rules/__fixtures__/comp/comp004/pass/orders-table.component.d.ts.map +1 -0
  201. package/dist/lint/rules/__fixtures__/comp/comp005/fail/search-service.component.d.ts +4 -0
  202. package/dist/lint/rules/__fixtures__/comp/comp005/fail/search-service.component.d.ts.map +1 -0
  203. package/dist/lint/rules/__fixtures__/comp/comp005/pass/search-service.component.d.ts +4 -0
  204. package/dist/lint/rules/__fixtures__/comp/comp005/pass/search-service.component.d.ts.map +1 -0
  205. package/dist/lint/rules/__fixtures__/comp/comp006/fail/legacy-tool.component.d.ts +4 -0
  206. package/dist/lint/rules/__fixtures__/comp/comp006/fail/legacy-tool.component.d.ts.map +1 -0
  207. package/dist/lint/rules/__fixtures__/comp/comp006/pass/legacy-tool.component.d.ts +4 -0
  208. package/dist/lint/rules/__fixtures__/comp/comp006/pass/legacy-tool.component.d.ts.map +1 -0
  209. package/dist/lint/rules/__fixtures__/comp/comp007/fail/inventory-table.component.d.ts +4 -0
  210. package/dist/lint/rules/__fixtures__/comp/comp007/fail/inventory-table.component.d.ts.map +1 -0
  211. package/dist/lint/rules/__fixtures__/comp/comp007/fail/orders-table.component.d.ts +4 -0
  212. package/dist/lint/rules/__fixtures__/comp/comp007/fail/orders-table.component.d.ts.map +1 -0
  213. package/dist/lint/rules/__fixtures__/comp/comp007/pass/orders-table.component.d.ts +3 -0
  214. package/dist/lint/rules/__fixtures__/comp/comp007/pass/orders-table.component.d.ts.map +1 -0
  215. package/dist/lint/rules/__fixtures__/comp/comp007/pass/search-service.component.d.ts +4 -0
  216. package/dist/lint/rules/__fixtures__/comp/comp007/pass/search-service.component.d.ts.map +1 -0
  217. package/dist/lint/rules/comp/comp001-publishes-never-applies.d.ts +20 -0
  218. package/dist/lint/rules/comp/comp001-publishes-never-applies.d.ts.map +1 -0
  219. package/dist/lint/rules/comp/comp002-dangling-wiring-ref.d.ts +28 -0
  220. package/dist/lint/rules/comp/comp002-dangling-wiring-ref.d.ts.map +1 -0
  221. package/dist/lint/rules/comp/comp003-mutating-no-rollback.d.ts +39 -0
  222. package/dist/lint/rules/comp/comp003-mutating-no-rollback.d.ts.map +1 -0
  223. package/dist/lint/rules/comp/comp004-gate-needs-temporal.d.ts +37 -0
  224. package/dist/lint/rules/comp/comp004-gate-needs-temporal.d.ts.map +1 -0
  225. package/dist/lint/rules/comp/comp005-capability-kind-is-noun.d.ts +43 -0
  226. package/dist/lint/rules/comp/comp005-capability-kind-is-noun.d.ts.map +1 -0
  227. package/dist/lint/rules/comp/comp006-shell-needs-reason.d.ts +22 -0
  228. package/dist/lint/rules/comp/comp006-shell-needs-reason.d.ts.map +1 -0
  229. package/dist/lint/rules/comp/comp007-composition-sprawl.d.ts +33 -0
  230. package/dist/lint/rules/comp/comp007-composition-sprawl.d.ts.map +1 -0
  231. package/dist/lint/rules/comp/index.d.ts +18 -0
  232. package/dist/lint/rules/comp/index.d.ts.map +1 -0
  233. package/dist/lint/rules/comp/support.d.ts +71 -0
  234. package/dist/lint/rules/comp/support.d.ts.map +1 -0
  235. package/dist/lsp/lexicon-providers.d.ts.map +1 -1
  236. package/dist/op/activity-registry.d.ts +11 -6
  237. package/dist/op/activity-registry.d.ts.map +1 -1
  238. package/dist/op/activity-runtime.d.ts +24 -0
  239. package/dist/op/activity-runtime.d.ts.map +1 -0
  240. package/dist/op/builders.d.ts +56 -0
  241. package/dist/op/builders.d.ts.map +1 -1
  242. package/dist/op/index.d.ts +2 -1
  243. package/dist/op/index.d.ts.map +1 -1
  244. package/dist/ownership.d.ts +20 -14
  245. package/dist/ownership.d.ts.map +1 -1
  246. package/package.json +14 -1
  247. package/src/audit/catalog.test.ts +74 -22
  248. package/src/audit/catalog.ts +74 -471
  249. package/src/audit/fetch.test.ts +112 -8
  250. package/src/audit/fetch.ts +53 -8
  251. package/src/audit/proof.ts +4 -2
  252. package/src/audit/report-html.test.ts +14 -7
  253. package/src/audit/report-model.ts +20 -9
  254. package/src/audit/report.test.ts +36 -13
  255. package/src/audit/report.ts +2 -0
  256. package/src/audit/rules-doc.test.ts +8 -6
  257. package/src/audit/rules-doc.ts +13 -5
  258. package/src/cli/commands/audit.ts +23 -18
  259. package/src/cli/commands/component-lint.test.ts +158 -0
  260. package/src/cli/commands/doctor.test.ts +116 -2
  261. package/src/cli/commands/doctor.ts +46 -0
  262. package/src/cli/commands/lexicon-rolling-upgrade.ts +198 -0
  263. package/src/cli/commands/lexicon-surface-diff.test.ts +104 -0
  264. package/src/cli/commands/lexicon-surface-diff.ts +185 -0
  265. package/src/cli/commands/lint.ts +142 -1
  266. package/src/cli/commands/pinned-upgrade.ts +204 -0
  267. package/src/cli/handlers/build.ts +58 -1
  268. package/src/cli/handlers/components.test.ts +504 -0
  269. package/src/cli/handlers/components.ts +404 -0
  270. package/src/cli/handlers/dev.ts +60 -1
  271. package/src/cli/handlers/graph.ts +47 -2
  272. package/src/cli/handlers/misc.ts +45 -1
  273. package/src/cli/handlers/run.test.ts +967 -3
  274. package/src/cli/handlers/run.ts +618 -11
  275. package/src/cli/main.test.ts +66 -0
  276. package/src/cli/main.ts +105 -2
  277. package/src/cli/registry.ts +35 -0
  278. package/src/codegen/fetch.test.ts +84 -3
  279. package/src/codegen/fetch.ts +76 -2
  280. package/src/codegen/lexicon-regen.test.ts +298 -0
  281. package/src/codegen/lexicon-regen.ts +545 -0
  282. package/src/codegen/pinned-upgrade.test.ts +399 -0
  283. package/src/codegen/pinned-upgrade.ts +423 -0
  284. package/src/codegen/rolling-upgrade.test.ts +333 -0
  285. package/src/codegen/rolling-upgrade.ts +320 -0
  286. package/src/codegen/surface-snapshot.test.ts +461 -0
  287. package/src/codegen/surface-snapshot.ts +546 -0
  288. package/src/codegen/version-bump.test.ts +47 -0
  289. package/src/codegen/version-bump.ts +39 -0
  290. package/src/components/README.md +130 -0
  291. package/src/components/SPRAWL-VALIDATION.md +181 -0
  292. package/src/components/__fixtures__/alb-ecs-service.json +51 -0
  293. package/src/components/__fixtures__/dynamodb-infra.json +32 -0
  294. package/src/components/__fixtures__/emr-job-consumer.json +26 -0
  295. package/src/components/__fixtures__/jar-lib-producer.json +16 -0
  296. package/src/components/__fixtures__/lambda-image-processor.json +28 -0
  297. package/src/components/__fixtures__/neo4j-fanout.json +34 -0
  298. package/src/components/__fixtures__/single-host-compose.json +29 -0
  299. package/src/components/auto-release.test.ts +217 -0
  300. package/src/components/auto-release.ts +216 -0
  301. package/src/components/builders.test.ts +45 -0
  302. package/src/components/builders.ts +71 -0
  303. package/src/components/capability-plugin-loader.ts +218 -0
  304. package/src/components/capability-plugin-schema.ts +56 -0
  305. package/src/components/capability-plugin.test.ts +235 -0
  306. package/src/components/capability-plugin.ts +108 -0
  307. package/src/components/capability.ts +117 -0
  308. package/src/components/cli-support.test.ts +579 -0
  309. package/src/components/cli-support.ts +481 -0
  310. package/src/components/component-schema.test.ts +265 -0
  311. package/src/components/component.schema.json +235 -0
  312. package/src/components/component.test.ts +251 -0
  313. package/src/components/component.ts +255 -0
  314. package/src/components/config-defaults.test.ts +263 -0
  315. package/src/components/config-defaults.ts +145 -0
  316. package/src/components/discover.test.ts +352 -0
  317. package/src/components/discover.ts +167 -0
  318. package/src/components/driver-output.ts +71 -0
  319. package/src/components/driver.test.ts +638 -0
  320. package/src/components/driver.ts +690 -0
  321. package/src/components/index.ts +113 -0
  322. package/src/components/manifest-persistence.test.ts +152 -0
  323. package/src/components/manifest-persistence.ts +164 -0
  324. package/src/components/pilots/README.md +33 -0
  325. package/src/components/pilots/alb-ecs.pilot.ts +63 -0
  326. package/src/components/pilots/dynamodb.pilot.ts +59 -0
  327. package/src/components/pilots/jar-emr.pilot.ts +69 -0
  328. package/src/components/pilots/lambda.pilot.ts +55 -0
  329. package/src/components/pilots/neo4j-fanout.pilot.ts +92 -0
  330. package/src/components/pilots/pilots-lint-clean.test.ts +78 -0
  331. package/src/components/pilots/pilots.test.ts +126 -0
  332. package/src/components/pilots/project.ts +13 -0
  333. package/src/components/presets/comp007-reduction.test.ts +122 -0
  334. package/src/components/presets/ecs-fargate.test.ts +84 -0
  335. package/src/components/presets/ecs-fargate.ts +103 -0
  336. package/src/components/presets/index.ts +32 -0
  337. package/src/components/presets/lambda.test.ts +35 -0
  338. package/src/components/presets/lambda.ts +53 -0
  339. package/src/components/presets/single-host-compose.test.ts +65 -0
  340. package/src/components/presets/single-host-compose.ts +64 -0
  341. package/src/components/registry.test.ts +112 -0
  342. package/src/components/registry.ts +37 -0
  343. package/src/components/starter-plugin.ts +105 -0
  344. package/src/components/temporal-codegen-loader.ts +41 -0
  345. package/src/components/verbs/__fixtures__/package-lock.json +45 -0
  346. package/src/components/verbs/__fixtures__/pom.xml +37 -0
  347. package/src/components/verbs/__fixtures__/schemas/cyclonedx-1.5.schema.json +3800 -0
  348. package/src/components/verbs/__fixtures__/schemas/cyclonedx-spdx-license.schema.json +621 -0
  349. package/src/components/verbs/__fixtures__/schemas/jsf-0.82.schema.json +240 -0
  350. package/src/components/verbs/__fixtures__/schemas/spdx-2.3.schema.json +740 -0
  351. package/src/components/verbs/__tests__/mock-cloud-executor.ts +102 -0
  352. package/src/components/verbs/__tests__/mock-process-runner.ts +79 -0
  353. package/src/components/verbs/__tests__/mock-sbom-generator.ts +85 -0
  354. package/src/components/verbs/bom-writer.test.ts +331 -0
  355. package/src/components/verbs/bom-writer.ts +386 -0
  356. package/src/components/verbs/build-archive.test.ts +251 -0
  357. package/src/components/verbs/build-archive.ts +296 -0
  358. package/src/components/verbs/build.test.ts +222 -0
  359. package/src/components/verbs/build.ts +252 -0
  360. package/src/components/verbs/cloud-executor.ts +181 -0
  361. package/src/components/verbs/component-bom.test.ts +219 -0
  362. package/src/components/verbs/component-bom.ts +189 -0
  363. package/src/components/verbs/index.ts +49 -0
  364. package/src/components/verbs/license-policy.ts +115 -0
  365. package/src/components/verbs/lockfile-sbom-generator.test.ts +195 -0
  366. package/src/components/verbs/lockfile-sbom-generator.ts +236 -0
  367. package/src/components/verbs/process-runner.test.ts +79 -0
  368. package/src/components/verbs/process-runner.ts +113 -0
  369. package/src/components/verbs/reproducibility.test.ts +24 -0
  370. package/src/components/verbs/reproducibility.ts +100 -0
  371. package/src/components/verbs/sbom-generator.test.ts +96 -0
  372. package/src/components/verbs/sbom-generator.ts +209 -0
  373. package/src/components/verbs/sbom.test.ts +184 -0
  374. package/src/components/verbs/sbom.ts +152 -0
  375. package/src/components/verbs/shell.test.ts +36 -0
  376. package/src/components/verbs/shell.ts +51 -0
  377. package/src/components/verbs/sign.test.ts +210 -0
  378. package/src/components/verbs/sign.ts +350 -0
  379. package/src/components/verbs/stub.ts +34 -0
  380. package/src/components/verbs/tool-sbom-generator.test.ts +194 -0
  381. package/src/components/verbs/tool-sbom-generator.ts +192 -0
  382. package/src/components/verbs/verify.test.ts +159 -0
  383. package/src/components/verbs/verify.ts +179 -0
  384. package/src/components/verbs/vex.ts +182 -0
  385. package/src/components/verbs/vuln-gate.test.ts +201 -0
  386. package/src/components/verbs/vuln-gate.ts +170 -0
  387. package/src/components/verbs/vuln-scan.test.ts +145 -0
  388. package/src/components/verbs/vuln-scan.ts +266 -0
  389. package/src/components/verbs/wait-verify.test.ts +111 -0
  390. package/src/components/verbs/wait-verify.ts +171 -0
  391. package/src/config.test.ts +74 -1
  392. package/src/config.ts +219 -0
  393. package/src/index.ts +32 -0
  394. package/src/lexicon.ts +88 -0
  395. package/src/lifecycle/build-ledger-store.test.ts +217 -0
  396. package/src/lifecycle/build-ledger-store.ts +165 -0
  397. package/src/lifecycle/build-ledger.test.ts +349 -0
  398. package/src/lifecycle/build-ledger.ts +305 -0
  399. package/src/lifecycle/git.test.ts +118 -0
  400. package/src/lifecycle/git.ts +149 -14
  401. package/src/lifecycle/index.ts +5 -0
  402. package/src/lifecycle/oras-referrer-lookup.test.ts +116 -0
  403. package/src/lifecycle/oras-referrer-lookup.ts +110 -0
  404. package/src/lifecycle/release-ledger.test.ts +209 -0
  405. package/src/lifecycle/release-ledger.ts +185 -0
  406. package/src/lifecycle/status.test.ts +330 -0
  407. package/src/lifecycle/status.ts +278 -0
  408. package/src/lint/component-checks.ts +148 -0
  409. package/src/lint/config.test.ts +21 -0
  410. package/src/lint/engine.ts +11 -2
  411. package/src/lint/presets/relaxed.json +8 -1
  412. package/src/lint/presets/strict.json +8 -1
  413. package/src/lint/rules/__fixtures__/comp/comp000-discovery-error/broken-a.component.ts +9 -0
  414. package/src/lint/rules/__fixtures__/comp/comp000-discovery-error/broken-b.component.ts +9 -0
  415. package/src/lint/rules/__fixtures__/comp/comp001/fail/orphan-lib.component.ts +11 -0
  416. package/src/lint/rules/__fixtures__/comp/comp001/pass/emr-job.component.ts +13 -0
  417. package/src/lint/rules/__fixtures__/comp/comp001/pass/jar-lib.component.ts +11 -0
  418. package/src/lint/rules/__fixtures__/comp/comp002/fail/search-service.component.ts +27 -0
  419. package/src/lint/rules/__fixtures__/comp/comp002/pass/jar-lib.component.ts +10 -0
  420. package/src/lint/rules/__fixtures__/comp/comp002/pass/search-service.component.ts +23 -0
  421. package/src/lint/rules/__fixtures__/comp/comp003/fail/migrator.component.ts +10 -0
  422. package/src/lint/rules/__fixtures__/comp/comp003/pass/migrator.component.ts +19 -0
  423. package/src/lint/rules/__fixtures__/comp/comp003/pass/orders-table.component.ts +10 -0
  424. package/src/lint/rules/__fixtures__/comp/comp004/fail/neo4j-cluster.component.ts +20 -0
  425. package/src/lint/rules/__fixtures__/comp/comp004/pass/neo4j-cluster.component.ts +23 -0
  426. package/src/lint/rules/__fixtures__/comp/comp004/pass/orders-table.component.ts +10 -0
  427. package/src/lint/rules/__fixtures__/comp/comp005/fail/search-service.component.ts +14 -0
  428. package/src/lint/rules/__fixtures__/comp/comp005/pass/search-service.component.ts +18 -0
  429. package/src/lint/rules/__fixtures__/comp/comp006/fail/legacy-tool.component.ts +10 -0
  430. package/src/lint/rules/__fixtures__/comp/comp006/pass/legacy-tool.component.ts +14 -0
  431. package/src/lint/rules/__fixtures__/comp/comp007/fail/inventory-table.component.ts +13 -0
  432. package/src/lint/rules/__fixtures__/comp/comp007/fail/orders-table.component.ts +13 -0
  433. package/src/lint/rules/__fixtures__/comp/comp007/pass/orders-table.component.ts +12 -0
  434. package/src/lint/rules/__fixtures__/comp/comp007/pass/search-service.component.ts +18 -0
  435. package/src/lint/rules/comp/comp.test.ts +378 -0
  436. package/src/lint/rules/comp/comp001-publishes-never-applies.ts +78 -0
  437. package/src/lint/rules/comp/comp002-dangling-wiring-ref.ts +94 -0
  438. package/src/lint/rules/comp/comp003-mutating-no-rollback.ts +115 -0
  439. package/src/lint/rules/comp/comp004-gate-needs-temporal.ts +66 -0
  440. package/src/lint/rules/comp/comp005-capability-kind-is-noun.ts +113 -0
  441. package/src/lint/rules/comp/comp006-shell-needs-reason.ts +55 -0
  442. package/src/lint/rules/comp/comp007-composition-sprawl.ts +71 -0
  443. package/src/lint/rules/comp/index.ts +37 -0
  444. package/src/lint/rules/comp/support.ts +173 -0
  445. package/src/lsp/lexicon-providers.test.ts +55 -0
  446. package/src/lsp/lexicon-providers.ts +13 -3
  447. package/src/op/activity-registry.test.ts +38 -0
  448. package/src/op/activity-registry.ts +31 -12
  449. package/src/op/activity-runtime.ts +73 -0
  450. package/src/op/builders.ts +92 -0
  451. package/src/op/index.ts +3 -1
  452. package/src/ownership.test.ts +24 -26
  453. package/src/ownership.ts +26 -38
@@ -0,0 +1,330 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import {
3
+ reconcileStatus,
4
+ liveEvidenceFromChangeSet,
5
+ resolveLiveNames,
6
+ compareAcrossEnvironments,
7
+ type LiveComponentEvidence,
8
+ type LiveNameMapping,
9
+ } from "./status";
10
+ import type { ReleaseRecord } from "./release-ledger";
11
+ import type { ChangeSet } from "./change-set";
12
+ import type { BuildLedgerEntry, ComponentBomSummary } from "./build-ledger";
13
+
14
+ function record(overrides?: Partial<ReleaseRecord>): ReleaseRecord {
15
+ return {
16
+ version: 1,
17
+ component: "search-service",
18
+ env: "prod",
19
+ digest: "sha256:abc",
20
+ gitSha: "deadbeef",
21
+ runId: "run-1",
22
+ timestamp: "2026-01-01T00:00:00.000Z",
23
+ actor: "ci-bot",
24
+ ...overrides,
25
+ };
26
+ }
27
+
28
+ describe("status", () => {
29
+ describe("liveEvidenceFromChangeSet", () => {
30
+ test("projects each change-set entry into live evidence keyed by name", () => {
31
+ const cs: ChangeSet = {
32
+ env: "prod",
33
+ entries: [
34
+ { name: "search-service", type: "T", action: "noop", evidence: { declared: true, inSnapshot: true, live: true }, ownership: "owned" },
35
+ { name: "orphan-thing", type: "T", action: "adopt", evidence: { declared: false, inSnapshot: false, live: true }, ownership: "foreign" },
36
+ ],
37
+ };
38
+ const evidence = liveEvidenceFromChangeSet(cs);
39
+ expect(evidence.get("search-service")).toEqual({ live: true, action: "noop", ownership: "owned" });
40
+ expect(evidence.get("orphan-thing")).toEqual({ live: true, action: "adopt", ownership: "foreign" });
41
+ });
42
+
43
+ // #598: a component's name need not equal the live entity/resource name
44
+ // it owns. Callers pass an explicit `nameMapping`; without one (or for a
45
+ // component absent from it), the name == entity join above is unchanged.
46
+ describe("with a nameMapping (#598)", () => {
47
+ test("joins a component to a live entity name that differs from the component name", () => {
48
+ const cs: ChangeSet = {
49
+ env: "prod",
50
+ entries: [
51
+ { name: "search-service-v2", type: "T", action: "noop", evidence: { declared: true, inSnapshot: true, live: true }, ownership: "owned" },
52
+ ],
53
+ };
54
+ const mapping: LiveNameMapping = new Map([["search-svc", ["search-service-v2"]]]);
55
+ const evidence = liveEvidenceFromChangeSet(cs, mapping);
56
+ expect(evidence.get("search-svc")).toEqual({ live: true, action: "noop", ownership: "owned" });
57
+ // The live entity's own name is no longer a separate top-level key once
58
+ // it's claimed by an explicit mapping's component.
59
+ });
60
+
61
+ test("a component with no entry in the mapping still resolves by identity", () => {
62
+ const cs: ChangeSet = {
63
+ env: "prod",
64
+ entries: [
65
+ { name: "search-service", type: "T", action: "noop", evidence: { declared: true, inSnapshot: true, live: true }, ownership: "owned" },
66
+ ],
67
+ };
68
+ const mapping: LiveNameMapping = new Map([["some-other-component", ["renamed-thing"]]]);
69
+ const evidence = liveEvidenceFromChangeSet(cs, mapping);
70
+ expect(evidence.get("search-service")).toEqual({ live: true, action: "noop", ownership: "owned" });
71
+ });
72
+
73
+ test("aggregates evidence across several live names owned by one component", () => {
74
+ const cs: ChangeSet = {
75
+ env: "prod",
76
+ entries: [
77
+ { name: "cluster-node-1", type: "T", action: "noop", evidence: { declared: true, inSnapshot: true, live: true }, ownership: "owned" },
78
+ { name: "cluster-node-2", type: "T", action: "update", evidence: { declared: true, inSnapshot: true, live: true }, ownership: "owned" },
79
+ ],
80
+ };
81
+ const mapping: LiveNameMapping = new Map([["neo4j-cluster", ["cluster-node-1", "cluster-node-2"]]]);
82
+ const evidence = liveEvidenceFromChangeSet(cs, mapping);
83
+ // Drift on any owned entity surfaces as drift for the component.
84
+ expect(evidence.get("neo4j-cluster")).toEqual({ live: true, action: "update", ownership: "owned" });
85
+ });
86
+
87
+ test("a mapped component with none of its live names observed has no evidence entry", () => {
88
+ const cs: ChangeSet = { env: "prod", entries: [] };
89
+ const mapping: LiveNameMapping = new Map([["ghost-component", ["ghost-entity"]]]);
90
+ const evidence = liveEvidenceFromChangeSet(cs, mapping);
91
+ expect(evidence.has("ghost-component")).toBe(false);
92
+ });
93
+ });
94
+ });
95
+
96
+ describe("resolveLiveNames (#598)", () => {
97
+ test("falls back to the component's own name when no mapping is given", () => {
98
+ expect(resolveLiveNames("search-service")).toEqual(["search-service"]);
99
+ });
100
+
101
+ test("falls back to the component's own name when the mapping has no entry for it", () => {
102
+ const mapping: LiveNameMapping = new Map([["other", ["other-entity"]]]);
103
+ expect(resolveLiveNames("search-service", mapping)).toEqual(["search-service"]);
104
+ });
105
+
106
+ test("uses the explicit liveNames when present", () => {
107
+ const mapping: LiveNameMapping = new Map([["search-svc", ["search-service-v2"]]]);
108
+ expect(resolveLiveNames("search-svc", mapping)).toEqual(["search-service-v2"]);
109
+ });
110
+ });
111
+
112
+ describe("reconcileStatus", () => {
113
+ test("no liveEvidence at all -> recorded rows are 'unknown', unrecorded stay 'unrecorded'", () => {
114
+ const rows = reconcileStatus("prod", [record()], { allComponents: ["search-service", "other-service"] });
115
+ const search = rows.find((r) => r.component === "search-service")!;
116
+ const other = rows.find((r) => r.component === "other-service")!;
117
+ expect(search.reconciliation).toBe("unknown");
118
+ expect(other.reconciliation).toBe("unrecorded");
119
+ });
120
+
121
+ test("recorded + live + owned + no drift -> reconciled", () => {
122
+ const liveEvidence = new Map<string, LiveComponentEvidence>([
123
+ ["search-service", { live: true, action: "noop", ownership: "owned" }],
124
+ ]);
125
+ const rows = reconcileStatus("prod", [record()], { liveEvidence });
126
+ expect(rows[0].reconciliation).toBe("reconciled");
127
+ });
128
+
129
+ test("recorded + live action=update -> drifted", () => {
130
+ const liveEvidence = new Map<string, LiveComponentEvidence>([
131
+ ["search-service", { live: true, action: "update", ownership: "owned" }],
132
+ ]);
133
+ const rows = reconcileStatus("prod", [record()], { liveEvidence });
134
+ expect(rows[0].reconciliation).toBe("drifted");
135
+ });
136
+
137
+ test("recorded but nothing live -> stale", () => {
138
+ const liveEvidence = new Map<string, LiveComponentEvidence>();
139
+ const rows = reconcileStatus("prod", [record()], { liveEvidence });
140
+ expect(rows[0].reconciliation).toBe("stale");
141
+ expect(rows[0].detail).toContain("nothing observed live");
142
+ });
143
+
144
+ test("live and owned but no release record -> unrecorded (the headline case: unrecorded deploy)", () => {
145
+ const liveEvidence = new Map<string, LiveComponentEvidence>([
146
+ ["mystery-service", { live: true, action: "noop", ownership: "owned" }],
147
+ ]);
148
+ const rows = reconcileStatus("prod", [], { liveEvidence, allComponents: ["mystery-service"] });
149
+ expect(rows[0]).toMatchObject({ component: "mystery-service", reconciliation: "unrecorded" });
150
+ expect(rows[0].detail).toContain("chant-owned");
151
+ });
152
+
153
+ test("neither recorded nor live -> unrecorded with a distinct detail message", () => {
154
+ const liveEvidence = new Map<string, LiveComponentEvidence>();
155
+ const rows = reconcileStatus("prod", [], { liveEvidence, allComponents: ["ghost-service"] });
156
+ expect(rows[0].reconciliation).toBe("unrecorded");
157
+ expect(rows[0].detail).toContain("no release record and nothing observed live");
158
+ });
159
+
160
+ test("attaches build-ledger detail keyed by the recorded digest", () => {
161
+ const build: BuildLedgerEntry = {
162
+ component: "search-service",
163
+ path: "image.tar",
164
+ digest: "sha256:abc",
165
+ createdAt: "2026-01-01T00:00:00.000Z",
166
+ manifestDigest: "sha256:manifest",
167
+ referrers: [{ kind: "sbom", mediaType: "x", digest: "sha256:sbom" }],
168
+ };
169
+ const buildsByDigest = new Map([["sha256:abc", build]]);
170
+ const liveEvidence = new Map<string, LiveComponentEvidence>([
171
+ ["search-service", { live: true, action: "noop", ownership: "owned" }],
172
+ ]);
173
+ const rows = reconcileStatus("prod", [record()], { liveEvidence, buildsByDigest });
174
+ expect(rows[0].build).toEqual(build);
175
+ });
176
+
177
+ // #606: the build ledger's `sbom` summary (format/package count/source)
178
+ // flows through to `chant components status` unchanged — `reconcileStatus`
179
+ // treats `BuildLedgerEntry` opaquely, so no reconciliation logic needed to
180
+ // change for the status surface to gain SBOM visibility.
181
+ test("surfaces the build-ledger's sbom summary (format, package count, source) by digest", () => {
182
+ const build: BuildLedgerEntry = {
183
+ component: "search-service",
184
+ path: "image.tar",
185
+ digest: "sha256:abc",
186
+ createdAt: "2026-01-01T00:00:00.000Z",
187
+ manifestDigest: "sha256:manifest",
188
+ referrers: [],
189
+ sbom: { mediaType: "application/spdx+json", packageCount: 12, generator: "syft", source: "archive" },
190
+ };
191
+ const buildsByDigest = new Map([["sha256:abc", build]]);
192
+ const liveEvidence = new Map<string, LiveComponentEvidence>([
193
+ ["search-service", { live: true, action: "noop", ownership: "owned" }],
194
+ ]);
195
+ const rows = reconcileStatus("prod", [record()], { liveEvidence, buildsByDigest });
196
+ expect(rows[0].build?.sbom).toEqual({
197
+ mediaType: "application/spdx+json",
198
+ packageCount: 12,
199
+ generator: "syft",
200
+ source: "archive",
201
+ });
202
+ });
203
+
204
+ // #614: component-level BOM aggregation summary, keyed by digest — the
205
+ // same join key as `buildsByDigest`, but independent of it (a
206
+ // config-only/infra component with no image entry still has a component
207
+ // BOM to report, even though it has no `BuildLedgerEntry` at all).
208
+ test("attaches the component BOM summary keyed by the recorded digest (#614)", () => {
209
+ const componentBom: ComponentBomSummary = {
210
+ leaves: [
211
+ { path: "image.tar.sbom.json", bomKind: "software", subjectDigest: "sha256:img1", mediaType: "application/spdx+json", packageCount: 17, generator: "syft" },
212
+ { path: "search.template.json.config-bom.json", bomKind: "config", subjectDigest: "sha256:tmpl1", mediaType: "application/spdx+json", packageCount: 5 },
213
+ ],
214
+ totalPackageCount: 22,
215
+ isAssembly: true,
216
+ };
217
+ const componentBomByDigest = new Map([["sha256:abc", componentBom]]);
218
+ const liveEvidence = new Map<string, LiveComponentEvidence>([
219
+ ["search-service", { live: true, action: "noop", ownership: "owned" }],
220
+ ]);
221
+ const rows = reconcileStatus("prod", [record()], { liveEvidence, componentBomByDigest });
222
+ expect(rows[0].componentBom).toEqual(componentBom);
223
+ });
224
+
225
+ test("componentBom is undefined when no summary is keyed to the recorded digest", () => {
226
+ const liveEvidence = new Map<string, LiveComponentEvidence>([
227
+ ["search-service", { live: true, action: "noop", ownership: "owned" }],
228
+ ]);
229
+ const rows = reconcileStatus("prod", [record()], { liveEvidence, componentBomByDigest: new Map() });
230
+ expect(rows[0].componentBom).toBeUndefined();
231
+ });
232
+
233
+ test("componentBom and build are independent — a componentBomByDigest hit with no matching buildsByDigest entry still surfaces the BOM summary", () => {
234
+ const componentBom: ComponentBomSummary = {
235
+ leaves: [{ path: "t.json.config-bom.json", bomKind: "config", mediaType: "application/spdx+json", packageCount: 3 }],
236
+ totalPackageCount: 3,
237
+ isAssembly: false,
238
+ };
239
+ const componentBomByDigest = new Map([["sha256:abc", componentBom]]);
240
+ const liveEvidence = new Map<string, LiveComponentEvidence>([
241
+ ["search-service", { live: true, action: "noop", ownership: "owned" }],
242
+ ]);
243
+ const rows = reconcileStatus("prod", [record()], { liveEvidence, componentBomByDigest });
244
+ expect(rows[0].build).toBeUndefined();
245
+ expect(rows[0].componentBom).toEqual(componentBom);
246
+ });
247
+
248
+ test("only the latest record per component is used for reconciliation", () => {
249
+ const records = [
250
+ record({ digest: "sha256:old", timestamp: "2026-01-01T00:00:00.000Z" }),
251
+ record({ digest: "sha256:new", timestamp: "2026-01-02T00:00:00.000Z" }),
252
+ ];
253
+ const liveEvidence = new Map<string, LiveComponentEvidence>([
254
+ ["search-service", { live: true, action: "noop", ownership: "owned" }],
255
+ ]);
256
+ const rows = reconcileStatus("prod", records, { liveEvidence });
257
+ expect(rows[0].recorded?.digest).toBe("sha256:new");
258
+ });
259
+
260
+ test("rows are sorted by component name", () => {
261
+ const rows = reconcileStatus("prod", [
262
+ record({ component: "zeta" }),
263
+ record({ component: "alpha" }),
264
+ ], { liveEvidence: new Map() });
265
+ expect(rows.map((r) => r.component)).toEqual(["alpha", "zeta"]);
266
+ });
267
+
268
+ // #598, end-to-end: a component whose live entity name differs from its
269
+ // component name reconciles correctly once the caller supplies a
270
+ // nameMapping to liveEvidenceFromChangeSet, and the identity default
271
+ // (name == entity) keeps working unchanged for everything else.
272
+ test("a component whose live entity name differs from its own name reconciles via a nameMapping", () => {
273
+ const cs: ChangeSet = {
274
+ env: "prod",
275
+ entries: [
276
+ { name: "search-service-v2", type: "T", action: "noop", evidence: { declared: true, inSnapshot: true, live: true }, ownership: "owned" },
277
+ ],
278
+ };
279
+ const mapping: LiveNameMapping = new Map([["search-svc", ["search-service-v2"]]]);
280
+ const liveEvidence = liveEvidenceFromChangeSet(cs, mapping);
281
+
282
+ const rows = reconcileStatus("prod", [record({ component: "search-svc" })], { liveEvidence });
283
+ expect(rows[0]).toMatchObject({ component: "search-svc", reconciliation: "reconciled" });
284
+ });
285
+
286
+ test("the name == entity default still works when no mapping is supplied at all", () => {
287
+ const cs: ChangeSet = {
288
+ env: "prod",
289
+ entries: [
290
+ { name: "search-service", type: "T", action: "noop", evidence: { declared: true, inSnapshot: true, live: true }, ownership: "owned" },
291
+ ],
292
+ };
293
+ const liveEvidence = liveEvidenceFromChangeSet(cs);
294
+
295
+ const rows = reconcileStatus("prod", [record({ component: "search-service" })], { liveEvidence });
296
+ expect(rows[0]).toMatchObject({ component: "search-service", reconciliation: "reconciled" });
297
+ });
298
+ });
299
+
300
+ describe("compareAcrossEnvironments", () => {
301
+ test("same digest in both envs -> same: true", () => {
302
+ const result = compareAcrossEnvironments(
303
+ "search-service",
304
+ { name: "staging", records: [record({ env: "staging", digest: "sha256:shared" })] },
305
+ { name: "prod", records: [record({ env: "prod", digest: "sha256:shared" })] },
306
+ );
307
+ expect(result).toMatchObject({ digestA: "sha256:shared", digestB: "sha256:shared", same: true });
308
+ });
309
+
310
+ test("different digests -> same: false", () => {
311
+ const result = compareAcrossEnvironments(
312
+ "search-service",
313
+ { name: "staging", records: [record({ env: "staging", digest: "sha256:staging-build" })] },
314
+ { name: "prod", records: [record({ env: "prod", digest: "sha256:prod-build" })] },
315
+ );
316
+ expect(result.same).toBe(false);
317
+ });
318
+
319
+ test("missing record in one env -> same: false, digest undefined for that side", () => {
320
+ const result = compareAcrossEnvironments(
321
+ "search-service",
322
+ { name: "staging", records: [] },
323
+ { name: "prod", records: [record({ env: "prod" })] },
324
+ );
325
+ expect(result.digestA).toBeUndefined();
326
+ expect(result.digestB).toBe("sha256:abc");
327
+ expect(result.same).toBe(false);
328
+ });
329
+ });
330
+ });
@@ -0,0 +1,278 @@
1
+ /**
2
+ * Status surface — reconciles the release ledger against live truth (#568,
3
+ * epic #551 "Build & deploy observability").
4
+ *
5
+ * Two ledgers, joined by one key. What's been built lives in the
6
+ * content-addressed `BuildArchive` (../components/verbs/build-archive.ts,
7
+ * ./build-ledger.ts). What we *recorded* deploying lives in the release
8
+ * ledger (./release-ledger.ts). What's *actually running* is live truth:
9
+ * ownership markers (../ownership.ts) plus `lifecycle diff --live`
10
+ * (./live-diff.ts). This module is the join: for each component/env, line up
11
+ * the latest recorded release against whatever live evidence is available,
12
+ * and flag the four things that matter —
13
+ *
14
+ * - **unrecorded** — something is live/owned but no release record exists
15
+ * for it. Someone (or some pipeline) deployed outside the recorded path.
16
+ * - **stale** — a release record exists but nothing live corresponds to it
17
+ * anymore (the component disappeared from observation).
18
+ * - **drifted** — the environment's live/owned resources don't confirm the
19
+ * recorded digest one way or the other from this join alone (recorded,
20
+ * but live identity can't be read back to a digest) — surfaced as a
21
+ * lower-confidence signal rather than silently treated as "reconciled".
22
+ * - **reconciled** — a release record exists and live evidence (via
23
+ * ownership) confirms the component is present and owned by chant.
24
+ *
25
+ * This is deliberately a light-touch reconciliation: chant's lexicons report
26
+ * resource-level status (`ResourceMetadata`), not "the digest currently
27
+ * running" (most lexicons have no notion of image digest at all — a
28
+ * CloudFormation stack doesn't expose "the ECS task's image digest" through
29
+ * `describeResources`). So the live axis here is presence/ownership, not a
30
+ * digest re-derivation; digest-level truth is what the *release ledger*
31
+ * itself provides, and cross-environment digest comparison
32
+ * (`buildIsInBothEnvs`) is the query the epic actually asks for ("which
33
+ * build is in prod, and is it the one tested in staging").
34
+ */
35
+
36
+ import type { ChangeSet, ChangeAction } from "./change-set";
37
+ import { latestPerComponent, type ReleaseRecord } from "./release-ledger";
38
+ import type { BuildLedgerEntry, ComponentBomSummary } from "./build-ledger";
39
+
40
+ /** One row of `chant components status [env]` — the per-component join of recorded vs live. */
41
+ export interface ComponentStatusRow {
42
+ component: string;
43
+ env: string;
44
+ /** The latest recorded release for this component/env, if any. */
45
+ recorded?: ReleaseRecord;
46
+ /** Build-ledger detail for the recorded digest, when discoverable (referrers, manifest digest, this artifact's own reproducibility — #614). */
47
+ build?: BuildLedgerEntry;
48
+ /**
49
+ * Component-level BOM aggregation summary (#614,
50
+ * ../components/verbs/component-bom.ts), when the recorded digest's build
51
+ * archive manifest is available (`opts.componentBomByDigest`). Reports
52
+ * every leaf BOM (software SBOM + IaC config-BOM) the component's archive
53
+ * carries and whether they compose 1:1 or as a real multi-artifact
54
+ * assembly — independent of `build`/`buildsByDigest` (which stays scoped
55
+ * to image entries), since a config-only/infra component with no image
56
+ * still has a component BOM to report.
57
+ */
58
+ componentBom?: ComponentBomSummary;
59
+ /** Live reconciliation verdict for this row. */
60
+ reconciliation: "reconciled" | "unrecorded" | "stale" | "drifted" | "unknown";
61
+ /** Human-readable detail backing the verdict. */
62
+ detail: string;
63
+ }
64
+
65
+ export interface ComponentStatusResult {
66
+ env: string;
67
+ rows: ComponentStatusRow[];
68
+ /** Count of malformed ledger lines skipped (surfaced so a corrupted ledger is never silently invisible). */
69
+ malformedLedgerLines: number;
70
+ }
71
+
72
+ /**
73
+ * Live evidence for one component, distilled from a `ChangeSet` entry
74
+ * (./change-set.ts) — the same read-only classification `lifecycle plan`
75
+ * already computes from ownership + `diffLive`. `chant components status`
76
+ * doesn't recompute live/ownership logic; it reuses `buildChangeSet`'s
77
+ * output, so "unrecorded deploy" and "drift" both key off the one
78
+ * ownership-aware classification chant already trusts.
79
+ */
80
+ export interface LiveComponentEvidence {
81
+ /** True if this component name was observed live at all (declared+live, or orphan+live). */
82
+ live: boolean;
83
+ /** The `ChangeSet` action chant's existing plan logic assigned, when the component maps to a tracked entity/resource name. */
84
+ action?: ChangeAction;
85
+ /** Ownership verdict, when known. */
86
+ ownership?: "owned" | "foreign" | "unknown";
87
+ }
88
+
89
+ /**
90
+ * Component -> live entity/resource name(s) it owns (#598). Mirrors
91
+ * `Component.liveNames` (../components/component.ts) without importing it —
92
+ * this module stays decoupled from the typed authoring form, since a
93
+ * hand-written JSON component or a future non-chant frontend can supply the
94
+ * same mapping without going through `Component` at all. A component absent
95
+ * from this map, or mapped to `undefined`/an empty array, falls back to its
96
+ * own name as the sole live name — the original name == entity join.
97
+ */
98
+ export type LiveNameMapping = Map<string, string[] | undefined>;
99
+
100
+ /** Resolve the live entity/resource name(s) a component owns: its explicit mapping, or `[component]` when none is given. */
101
+ export function resolveLiveNames(component: string, mapping?: LiveNameMapping): string[] {
102
+ const mapped = mapping?.get(component);
103
+ return mapped && mapped.length > 0 ? mapped : [component];
104
+ }
105
+
106
+ /**
107
+ * Merge live evidence for several entity/resource names owned by one
108
+ * component into a single verdict. `live` and `ownership` favor the
109
+ * most-actionable signal (present/owned) over "nothing here"; `action`
110
+ * favors `update` so that drift on *any* owned entity surfaces as drift for
111
+ * the component as a whole, matching `reconcileStatus`'s single check for
112
+ * `action === "update"`.
113
+ */
114
+ function mergeEvidence(entries: LiveComponentEvidence[]): LiveComponentEvidence | undefined {
115
+ if (entries.length === 0) return undefined;
116
+ if (entries.length === 1) return entries[0];
117
+
118
+ const live = entries.some((e) => e.live);
119
+ const ownership = entries.some((e) => e.ownership === "owned")
120
+ ? "owned"
121
+ : entries.some((e) => e.ownership === "foreign")
122
+ ? "foreign"
123
+ : "unknown";
124
+ const action = entries.some((e) => e.action === "update")
125
+ ? "update"
126
+ : entries.find((e) => e.action !== undefined)?.action;
127
+
128
+ return { live, ownership, action };
129
+ }
130
+
131
+ /**
132
+ * Build a `component -> live evidence` map from a `ChangeSet`. Components and
133
+ * chant entities are different namespaces (a component's `name` need not
134
+ * equal any single lexicon entity name), so callers that want live
135
+ * reconciliation for a component whose live name(s) differ from its own name
136
+ * pass `nameMapping` (typically projected from `Component.liveNames`, see
137
+ * ../components/component.ts) — e.g. `new Map([["search-svc",
138
+ * ["search-service-v2"]]])`. Without a mapping (or for a component with no
139
+ * entry in it), the component's own name is used as its sole live name — the
140
+ * identity-keyed join that was this function's only behavior before #598,
141
+ * preserved here as the fallback so existing callers see no change.
142
+ */
143
+ export function liveEvidenceFromChangeSet(
144
+ cs: ChangeSet,
145
+ nameMapping?: LiveNameMapping,
146
+ ): Map<string, LiveComponentEvidence> {
147
+ const evidenceByName = new Map<string, LiveComponentEvidence>();
148
+ for (const entry of cs.entries) {
149
+ evidenceByName.set(entry.name, {
150
+ live: entry.evidence.live,
151
+ action: entry.action,
152
+ ownership: entry.ownership,
153
+ });
154
+ }
155
+
156
+ if (!nameMapping) return evidenceByName;
157
+
158
+ // Re-key by component: for each component that has an explicit mapping,
159
+ // aggregate evidence across every live name it owns. Components with no
160
+ // mapping entry keep their identity-keyed evidence as-is (already present
161
+ // in evidenceByName under their own name).
162
+ const result = new Map<string, LiveComponentEvidence>(evidenceByName);
163
+ for (const [component, liveNames] of nameMapping) {
164
+ if (!liveNames || liveNames.length === 0) continue;
165
+ const merged = mergeEvidence(
166
+ liveNames
167
+ .map((n) => evidenceByName.get(n))
168
+ .filter((e): e is LiveComponentEvidence => e !== undefined),
169
+ );
170
+ if (merged) {
171
+ result.set(component, merged);
172
+ } else {
173
+ result.delete(component);
174
+ }
175
+ }
176
+ return result;
177
+ }
178
+
179
+ /**
180
+ * Reconcile the release ledger against live evidence for one environment.
181
+ * Pure — no I/O. Callers assemble `records` (./release-ledger.ts's
182
+ * `readReleaseLedger`), `liveEvidence` (from a `ChangeSet` via
183
+ * `liveEvidenceFromChangeSet`, or `undefined` when `--live` wasn't
184
+ * requested), `builds` (a `component -> BuildLedgerEntry` lookup keyed by
185
+ * digest, from ./build-ledger.ts), and `componentBomByDigest` (#614, a
186
+ * `digest -> ComponentBomSummary` lookup, also from ./build-ledger.ts) ahead
187
+ * of time.
188
+ */
189
+ export function reconcileStatus(
190
+ env: string,
191
+ records: ReleaseRecord[],
192
+ opts?: {
193
+ liveEvidence?: Map<string, LiveComponentEvidence>;
194
+ /** Build-ledger detail keyed by digest, when available. */
195
+ buildsByDigest?: Map<string, BuildLedgerEntry>;
196
+ /** Component-level BOM aggregation summary keyed by digest, when available (#614). Independent of `buildsByDigest` — a config-only/infra component with no image entry still has a component BOM to report. */
197
+ componentBomByDigest?: Map<string, ComponentBomSummary>;
198
+ /** Component names known to exist (from `discoverComponents`) but with no release record at all — still reported as `unrecorded` when live evidence says they're running. */
199
+ allComponents?: string[];
200
+ },
201
+ ): ComponentStatusRow[] {
202
+ const latest = latestPerComponent(records);
203
+ const liveEvidence = opts?.liveEvidence;
204
+ const componentNames = new Set<string>([...latest.keys(), ...(opts?.allComponents ?? [])]);
205
+
206
+ const rows: ComponentStatusRow[] = [];
207
+ for (const component of [...componentNames].sort()) {
208
+ const recorded = latest.get(component);
209
+ const evidence = liveEvidence?.get(component);
210
+ const build = recorded ? opts?.buildsByDigest?.get(recorded.digest) : undefined;
211
+ const componentBom = recorded ? opts?.componentBomByDigest?.get(recorded.digest) : undefined;
212
+
213
+ let reconciliation: ComponentStatusRow["reconciliation"];
214
+ let detail: string;
215
+
216
+ if (!liveEvidence) {
217
+ // No live evidence requested at all (digest-only / offline mode) — the
218
+ // ledger is the only signal available, so recorded-vs-unrecorded still
219
+ // makes sense but drift can't be assessed.
220
+ reconciliation = recorded ? "unknown" : "unrecorded";
221
+ detail = recorded
222
+ ? "recorded; live status not queried (pass --live to reconcile)"
223
+ : "no release record found";
224
+ } else if (!recorded && evidence?.live) {
225
+ reconciliation = "unrecorded";
226
+ detail = `live${evidence.ownership === "owned" ? " and chant-owned" : ""}, but no release record exists — deployed outside the recorded path`;
227
+ } else if (!recorded && !evidence?.live) {
228
+ reconciliation = "unrecorded";
229
+ detail = "no release record and nothing observed live";
230
+ } else if (recorded && !evidence?.live) {
231
+ reconciliation = "stale";
232
+ detail = `recorded ${recorded.timestamp} (digest ${recorded.digest}), but nothing observed live now`;
233
+ } else if (recorded && evidence?.action === "update") {
234
+ reconciliation = "drifted";
235
+ detail = `recorded ${recorded.timestamp} (digest ${recorded.digest}), but live configuration has drifted since`;
236
+ } else {
237
+ reconciliation = "reconciled";
238
+ detail = `recorded ${recorded!.timestamp} (digest ${recorded!.digest}), live and consistent`;
239
+ }
240
+
241
+ rows.push({ component, env, recorded, build, componentBom, reconciliation, detail });
242
+ }
243
+
244
+ return rows;
245
+ }
246
+
247
+ /**
248
+ * Answer "which build is in `envA`, and is it the one tested in `envB`" —
249
+ * the single-query comparison the epic names explicitly — for one component
250
+ * across two environments' release ledgers.
251
+ */
252
+ export interface CrossEnvComparison {
253
+ component: string;
254
+ envA: string;
255
+ envB: string;
256
+ digestA?: string;
257
+ digestB?: string;
258
+ /** True only when both envs have a recorded digest and they match. */
259
+ same: boolean;
260
+ }
261
+
262
+ /** Compare the latest recorded digest for `component` between two environments' release records. */
263
+ export function compareAcrossEnvironments(
264
+ component: string,
265
+ envA: { name: string; records: ReleaseRecord[] },
266
+ envB: { name: string; records: ReleaseRecord[] },
267
+ ): CrossEnvComparison {
268
+ const latestA = latestPerComponent(envA.records).get(component);
269
+ const latestB = latestPerComponent(envB.records).get(component);
270
+ return {
271
+ component,
272
+ envA: envA.name,
273
+ envB: envB.name,
274
+ digestA: latestA?.digest,
275
+ digestB: latestB?.digest,
276
+ same: !!latestA && !!latestB && latestA.digest === latestB.digest,
277
+ };
278
+ }