@intentius/chant 0.124.45 → 0.124.48

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 (772) hide show
  1. package/dist/__golden__/wave-loop/op-by-apply-after-fails.json +10 -0
  2. package/dist/__golden__/wave-loop/op-by-apply-hold.json +10 -0
  3. package/dist/__golden__/wave-loop/op-by-apply-ignored.json +10 -0
  4. package/dist/__golden__/wave-loop/op-by-apply-pass.json +10 -0
  5. package/dist/__golden__/wave-loop/op-by-apply-skip.json +10 -0
  6. package/dist/__golden__/wave-loop/op-cost-member-clash.json +6 -0
  7. package/dist/__golden__/wave-loop/op-cost-member.json +13 -0
  8. package/dist/__golden__/wave-loop/op-denied-source.json +16 -0
  9. package/dist/__golden__/wave-loop/op-denied.json +16 -0
  10. package/dist/__golden__/wave-loop/op-discover-rest-short.json +6 -0
  11. package/dist/__golden__/wave-loop/op-discover-rest-stops.json +13 -0
  12. package/dist/__golden__/wave-loop/op-discover-rest.json +26 -0
  13. package/dist/__golden__/wave-loop/op-discover-wide.json +26 -0
  14. package/dist/__golden__/wave-loop/op-held-on-again.json +16 -0
  15. package/dist/__golden__/wave-loop/op-held-on-before-apply.json +16 -0
  16. package/dist/__golden__/wave-loop/op-held-on.json +16 -0
  17. package/dist/__golden__/wave-loop/op-hooks-apply-fails.json +13 -0
  18. package/dist/__golden__/wave-loop/op-hooks-fail-after-apply.json +13 -0
  19. package/dist/__golden__/wave-loop/op-hooks-fail-before-apply.json +13 -0
  20. package/dist/__golden__/wave-loop/op-hooks-fail-before-plan.json +13 -0
  21. package/dist/__golden__/wave-loop/op-hooks-held.json +13 -0
  22. package/dist/__golden__/wave-loop/op-hooks-hold-before-apply.json +13 -0
  23. package/dist/__golden__/wave-loop/op-hooks-pass.json +13 -0
  24. package/dist/__golden__/wave-loop/op-hooks-refused-at-base.json +13 -0
  25. package/dist/__golden__/wave-loop/op-hooks-share.json +33 -0
  26. package/dist/__golden__/wave-loop/op-outcome-held.json +10 -0
  27. package/dist/__golden__/wave-loop/op-outcome-replan-approved.json +10 -0
  28. package/dist/__golden__/wave-loop/op-outcome-replan-forever.json +10 -0
  29. package/dist/__golden__/wave-loop/op-outcome-replan.json +10 -0
  30. package/dist/__golden__/wave-loop/op-outcome-stood-down.json +10 -0
  31. package/dist/__golden__/wave-loop/op-outcome-unreadable.json +10 -0
  32. package/dist/__golden__/wave-loop/op-overridden.json +16 -0
  33. package/dist/__golden__/wave-loop/op-per-run.json +20 -0
  34. package/dist/__golden__/wave-loop/op-point.json +13 -0
  35. package/dist/__golden__/wave-loop/op-pr-review.json +10 -0
  36. package/dist/__golden__/wave-loop/op-sealed.json +10 -0
  37. package/dist/__golden__/wave-loop/op-spend-change-only.json +10 -0
  38. package/dist/__golden__/wave-loop/op-spend.json +10 -0
  39. package/dist/__golden__/wave-loop/op-wave-commands.json +55 -0
  40. package/dist/__golden__/wave-loop/op-wide-refused.json +26 -0
  41. package/dist/__golden__/wave-loop/op-wide-waiting.json +29 -0
  42. package/dist/__golden__/wave-loop/op-wide.json +39 -0
  43. package/dist/action-pins.d.ts +61 -0
  44. package/dist/action-pins.d.ts.map +1 -0
  45. package/dist/action-pins.js +77 -0
  46. package/dist/capabilities/aws.paths +148 -0
  47. package/dist/capabilities/base-mappings.json +122 -0
  48. package/dist/capabilities/catalog.d.ts +89 -0
  49. package/dist/capabilities/catalog.d.ts.map +1 -0
  50. package/dist/capabilities/catalog.js +204 -0
  51. package/dist/capabilities/language.d.ts +103 -0
  52. package/dist/capabilities/language.d.ts.map +1 -0
  53. package/dist/capabilities/language.js +335 -0
  54. package/dist/capabilities/matcher.d.ts +65 -0
  55. package/dist/capabilities/matcher.d.ts.map +1 -0
  56. package/dist/capabilities/matcher.js +299 -0
  57. package/dist/capabilities/view.d.ts +207 -0
  58. package/dist/capabilities/view.d.ts.map +1 -0
  59. package/dist/capabilities/view.js +484 -0
  60. package/dist/cli/commands/carve.d.ts +3 -0
  61. package/dist/cli/commands/carve.d.ts.map +1 -1
  62. package/dist/cli/commands/carve.js +36 -7
  63. package/dist/cli/commands/import-report.d.ts +26 -0
  64. package/dist/cli/commands/import-report.d.ts.map +1 -0
  65. package/dist/cli/commands/import-report.js +65 -0
  66. package/dist/cli/commands/import.d.ts +11 -1
  67. package/dist/cli/commands/import.d.ts.map +1 -1
  68. package/dist/cli/commands/import.js +22 -3
  69. package/dist/cli/commands/lint.d.ts.map +1 -1
  70. package/dist/cli/commands/lint.js +1 -0
  71. package/dist/cli/commands/migrate.d.ts +13 -1
  72. package/dist/cli/commands/migrate.d.ts.map +1 -1
  73. package/dist/cli/commands/migrate.js +134 -10
  74. package/dist/cli/handlers/approve.d.ts.map +1 -1
  75. package/dist/cli/handlers/approve.js +21 -2
  76. package/dist/cli/handlers/build.d.ts.map +1 -1
  77. package/dist/cli/handlers/build.js +6 -1
  78. package/dist/cli/handlers/declared-graph.d.ts +44 -0
  79. package/dist/cli/handlers/declared-graph.d.ts.map +1 -0
  80. package/dist/cli/handlers/declared-graph.js +87 -0
  81. package/dist/cli/handlers/graph.d.ts +25 -0
  82. package/dist/cli/handlers/graph.d.ts.map +1 -1
  83. package/dist/cli/handlers/graph.js +93 -87
  84. package/dist/cli/handlers/ledger.d.ts +3 -0
  85. package/dist/cli/handlers/ledger.d.ts.map +1 -0
  86. package/dist/cli/handlers/ledger.js +55 -0
  87. package/dist/cli/handlers/migrate.d.ts.map +1 -1
  88. package/dist/cli/handlers/migrate.js +18 -11
  89. package/dist/cli/handlers/misc.d.ts.map +1 -1
  90. package/dist/cli/handlers/misc.js +12 -1
  91. package/dist/cli/handlers/pr-env.d.ts.map +1 -1
  92. package/dist/cli/handlers/pr-env.js +4 -3
  93. package/dist/cli/handlers/pr.d.ts +15 -1
  94. package/dist/cli/handlers/pr.d.ts.map +1 -1
  95. package/dist/cli/handlers/pr.js +143 -39
  96. package/dist/cli/handlers/query.d.ts +7 -2
  97. package/dist/cli/handlers/query.d.ts.map +1 -1
  98. package/dist/cli/handlers/query.js +16 -4
  99. package/dist/cli/handlers/release.d.ts.map +1 -1
  100. package/dist/cli/handlers/release.js +6 -3
  101. package/dist/cli/handlers/search-count.d.ts +89 -0
  102. package/dist/cli/handlers/search-count.d.ts.map +1 -0
  103. package/dist/cli/handlers/search-count.js +132 -0
  104. package/dist/cli/handlers/search.d.ts +356 -6
  105. package/dist/cli/handlers/search.d.ts.map +1 -1
  106. package/dist/cli/handlers/search.js +1039 -155
  107. package/dist/cli/main.d.ts.map +1 -1
  108. package/dist/cli/main.js +88 -17
  109. package/dist/cli/mcp/server.d.ts.map +1 -1
  110. package/dist/cli/mcp/server.js +3 -0
  111. package/dist/cli/mcp/tools/capabilities.d.ts +7 -0
  112. package/dist/cli/mcp/tools/capabilities.d.ts.map +1 -0
  113. package/dist/cli/mcp/tools/capabilities.js +27 -0
  114. package/dist/cli/mcp/workspace-tools.d.ts +3 -1
  115. package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
  116. package/dist/cli/mcp/workspace-tools.js +47 -2
  117. package/dist/cli/parse-args.d.ts.map +1 -1
  118. package/dist/cli/parse-args.js +100 -19
  119. package/dist/cli/registry.d.ts +36 -4
  120. package/dist/cli/registry.d.ts.map +1 -1
  121. package/dist/components/builders.d.ts +5 -1
  122. package/dist/components/builders.d.ts.map +1 -1
  123. package/dist/components/builders.js +5 -0
  124. package/dist/components/capability-plugin-loader.d.ts.map +1 -1
  125. package/dist/components/capability-plugin-loader.js +16 -1
  126. package/dist/components/capability-plugin.d.ts +14 -0
  127. package/dist/components/capability-plugin.d.ts.map +1 -1
  128. package/dist/components/pr-env-ports.d.ts +29 -0
  129. package/dist/components/pr-env-ports.d.ts.map +1 -0
  130. package/dist/components/pr-env-ports.js +77 -0
  131. package/dist/components/pr-env-run.d.ts +5 -21
  132. package/dist/components/pr-env-run.d.ts.map +1 -1
  133. package/dist/components/pr-env-run.js +6 -70
  134. package/dist/components/pr-environment.d.ts +2 -1
  135. package/dist/components/pr-environment.d.ts.map +1 -1
  136. package/dist/components/pr-environment.js +1 -1
  137. package/dist/components/pr-hooks.d.ts +21 -1
  138. package/dist/components/pr-hooks.d.ts.map +1 -1
  139. package/dist/components/pr-hooks.js +72 -4
  140. package/dist/components/pr-pipeline.d.ts +45 -10
  141. package/dist/components/pr-pipeline.d.ts.map +1 -1
  142. package/dist/components/pr-pipeline.js +47 -14
  143. package/dist/components/pr-policy.d.ts +36 -0
  144. package/dist/components/pr-policy.d.ts.map +1 -1
  145. package/dist/components/pr-policy.js +36 -0
  146. package/dist/components/starter-plugin.d.ts +1 -0
  147. package/dist/components/starter-plugin.d.ts.map +1 -1
  148. package/dist/components/starter-plugin.js +5 -1
  149. package/dist/components/verbs/index.d.ts +2 -0
  150. package/dist/components/verbs/index.d.ts.map +1 -1
  151. package/dist/components/verbs/index.js +2 -0
  152. package/dist/components/verbs/publish-artifact.d.ts +65 -0
  153. package/dist/components/verbs/publish-artifact.d.ts.map +1 -0
  154. package/dist/components/verbs/publish-artifact.js +74 -0
  155. package/dist/components/verbs/publish-image.d.ts +129 -0
  156. package/dist/components/verbs/publish-image.d.ts.map +1 -0
  157. package/dist/components/verbs/publish-image.js +145 -0
  158. package/dist/config-environments.d.ts +112 -0
  159. package/dist/config-environments.d.ts.map +1 -0
  160. package/dist/config-environments.js +114 -0
  161. package/dist/config.d.ts +32 -84
  162. package/dist/config.d.ts.map +1 -1
  163. package/dist/config.js +13 -96
  164. package/dist/gate-decision.d.ts +11 -2
  165. package/dist/gate-decision.d.ts.map +1 -1
  166. package/dist/gate-decision.js +15 -3
  167. package/dist/graph-detail.d.ts.map +1 -1
  168. package/dist/graph-detail.js +15 -3
  169. package/dist/graph-effective.d.ts +33 -2
  170. package/dist/graph-effective.d.ts.map +1 -1
  171. package/dist/graph-effective.js +215 -68
  172. package/dist/graph-ir.d.ts +48 -0
  173. package/dist/graph-ir.d.ts.map +1 -1
  174. package/dist/graph-ir.js +130 -0
  175. package/dist/graph-sub-template.d.ts +11 -0
  176. package/dist/graph-sub-template.d.ts.map +1 -0
  177. package/dist/graph-sub-template.js +19 -0
  178. package/dist/graph-walk.d.ts +66 -0
  179. package/dist/graph-walk.d.ts.map +1 -0
  180. package/dist/graph-walk.js +70 -0
  181. package/dist/import/parser.d.ts +19 -0
  182. package/dist/import/parser.d.ts.map +1 -1
  183. package/dist/lexicon.d.ts +80 -6
  184. package/dist/lexicon.d.ts.map +1 -1
  185. package/dist/lifecycle/gate-ledger.d.ts +11 -0
  186. package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
  187. package/dist/lifecycle/git.d.ts +2 -5
  188. package/dist/lifecycle/git.d.ts.map +1 -1
  189. package/dist/lifecycle/ledger-read.d.ts +65 -0
  190. package/dist/lifecycle/ledger-read.d.ts.map +1 -0
  191. package/dist/lifecycle/ledger-read.js +160 -0
  192. package/dist/lifecycle/observe.d.ts +28 -0
  193. package/dist/lifecycle/observe.d.ts.map +1 -1
  194. package/dist/lifecycle/observe.js +23 -1
  195. package/dist/lifecycle/release-workstation.d.ts +7 -0
  196. package/dist/lifecycle/release-workstation.d.ts.map +1 -1
  197. package/dist/lifecycle/release-workstation.js +25 -0
  198. package/dist/lifecycle/run-ledger.d.ts +4 -3
  199. package/dist/lifecycle/run-ledger.d.ts.map +1 -1
  200. package/dist/lifecycle/run-ledger.js +1 -0
  201. package/dist/lint/discover.js +3 -1
  202. package/dist/lint/post-synth.d.ts +127 -1
  203. package/dist/lint/post-synth.d.ts.map +1 -1
  204. package/dist/lint/post-synth.js +84 -2
  205. package/dist/lint/rule.d.ts +6 -0
  206. package/dist/lint/rule.d.ts.map +1 -1
  207. package/dist/lint/rules/op/index.d.ts +1 -0
  208. package/dist/lint/rules/op/index.d.ts.map +1 -1
  209. package/dist/lint/rules/op/index.js +2 -0
  210. package/dist/lint/rules/op/ops019-workspace-op-target.d.ts +32 -0
  211. package/dist/lint/rules/op/ops019-workspace-op-target.d.ts.map +1 -0
  212. package/dist/lint/rules/op/ops019-workspace-op-target.js +63 -0
  213. package/dist/live-endpoint.d.ts +1 -1
  214. package/dist/live-endpoint.d.ts.map +1 -1
  215. package/dist/live-endpoint.js +1 -1
  216. package/dist/observation.d.ts +48 -1
  217. package/dist/observation.d.ts.map +1 -1
  218. package/dist/observation.js +21 -5
  219. package/dist/op/activities/activity-contracts.d.ts +9 -6
  220. package/dist/op/activities/activity-contracts.d.ts.map +1 -1
  221. package/dist/op/activities/activity-contracts.js +5 -2
  222. package/dist/op/activities/index.d.ts +2 -2
  223. package/dist/op/activities/index.d.ts.map +1 -1
  224. package/dist/op/activities/index.js +1 -1
  225. package/dist/op/activities/review-agent.d.ts +90 -4
  226. package/dist/op/activities/review-agent.d.ts.map +1 -1
  227. package/dist/op/activities/review-agent.js +217 -19
  228. package/dist/op/activities/work-evidence.d.ts +8 -1
  229. package/dist/op/activities/work-evidence.d.ts.map +1 -1
  230. package/dist/op/activities/work-evidence.js +1 -0
  231. package/dist/op/builders.d.ts +9 -5
  232. package/dist/op/builders.d.ts.map +1 -1
  233. package/dist/op/builders.js +9 -5
  234. package/dist/op/composites/apply-op.d.ts +29 -2
  235. package/dist/op/composites/apply-op.d.ts.map +1 -1
  236. package/dist/op/composites/apply-op.js +20 -1
  237. package/dist/op/gate-approval.d.ts +11 -5
  238. package/dist/op/gate-approval.d.ts.map +1 -1
  239. package/dist/op/gate-approval.js +11 -6
  240. package/dist/op/gate-quorum.d.ts +100 -3
  241. package/dist/op/gate-quorum.d.ts.map +1 -1
  242. package/dist/op/gate-quorum.js +108 -6
  243. package/dist/op/gate-review.d.ts +9 -0
  244. package/dist/op/gate-review.d.ts.map +1 -1
  245. package/dist/op/gate-review.js +13 -0
  246. package/dist/op/gate.d.ts +44 -3
  247. package/dist/op/gate.d.ts.map +1 -1
  248. package/dist/op/gate.js +25 -4
  249. package/dist/op/index.d.ts +3 -3
  250. package/dist/op/index.d.ts.map +1 -1
  251. package/dist/op/index.js +1 -1
  252. package/dist/op/local-executor.d.ts +38 -3
  253. package/dist/op/local-executor.d.ts.map +1 -1
  254. package/dist/op/local-executor.js +48 -6
  255. package/dist/op/op-waves-run.d.ts +26 -1
  256. package/dist/op/op-waves-run.d.ts.map +1 -1
  257. package/dist/op/op-waves-run.js +259 -13
  258. package/dist/op/op-waves.d.ts +72 -5
  259. package/dist/op/op-waves.d.ts.map +1 -1
  260. package/dist/op/op-waves.js +110 -15
  261. package/dist/op/runtime.d.ts +11 -0
  262. package/dist/op/runtime.d.ts.map +1 -1
  263. package/dist/op/schedule-overdue.d.ts +16 -0
  264. package/dist/op/schedule-overdue.d.ts.map +1 -1
  265. package/dist/op/schedule-overdue.js +36 -0
  266. package/dist/op/stage-hooks.d.ts +9 -2
  267. package/dist/op/stage-hooks.d.ts.map +1 -1
  268. package/dist/op/stage-hooks.js +7 -5
  269. package/dist/op/workspace-run-env.d.ts +22 -0
  270. package/dist/op/workspace-run-env.d.ts.map +1 -0
  271. package/dist/op/workspace-run-env.js +21 -0
  272. package/dist/pr-forge.d.ts +24 -0
  273. package/dist/pr-forge.d.ts.map +1 -1
  274. package/dist/pr-forge.js +47 -0
  275. package/dist/pr-post.d.ts +42 -0
  276. package/dist/pr-post.d.ts.map +1 -1
  277. package/dist/pr-post.js +67 -1
  278. package/dist/read-filter.d.ts +65 -0
  279. package/dist/read-filter.d.ts.map +1 -0
  280. package/dist/read-filter.js +179 -0
  281. package/dist/run-event.d.ts +1 -1
  282. package/dist/run-event.d.ts.map +1 -1
  283. package/dist/terraform/carve.d.ts +24 -3
  284. package/dist/terraform/carve.d.ts.map +1 -1
  285. package/dist/terraform/carve.js +32 -14
  286. package/dist/terraform/cuts.d.ts +97 -0
  287. package/dist/terraform/cuts.d.ts.map +1 -0
  288. package/dist/terraform/cuts.js +138 -0
  289. package/dist/terraform/folds.d.ts +26 -0
  290. package/dist/terraform/folds.d.ts.map +1 -0
  291. package/dist/terraform/folds.js +38 -0
  292. package/dist/terraform/graph.d.ts.map +1 -1
  293. package/dist/terraform/graph.js +10 -0
  294. package/dist/terraform/reach.d.ts +140 -0
  295. package/dist/terraform/reach.d.ts.map +1 -0
  296. package/dist/terraform/reach.js +170 -0
  297. package/dist/terraform/score.d.ts +14 -0
  298. package/dist/terraform/score.d.ts.map +1 -1
  299. package/dist/terraform/score.js +14 -31
  300. package/dist/terraform/state.d.ts.map +1 -1
  301. package/dist/terraform/state.js +4 -2
  302. package/dist/terraform/types.d.ts +8 -0
  303. package/dist/terraform/types.d.ts.map +1 -1
  304. package/dist/wave-blocked.d.ts +10 -1
  305. package/dist/wave-blocked.d.ts.map +1 -1
  306. package/dist/wave-blocked.js +31 -3
  307. package/dist/wave-runtime.mjs +266 -256
  308. package/dist/workspace/capabilities-contract.d.ts +66 -0
  309. package/dist/workspace/capabilities-contract.d.ts.map +1 -0
  310. package/dist/workspace/capabilities-contract.js +107 -0
  311. package/dist/workspace/capabilities.schema.json +229 -0
  312. package/dist/workspace/check.schema.json +11 -2
  313. package/dist/workspace/checks/ci-branch.d.ts +44 -0
  314. package/dist/workspace/checks/ci-branch.d.ts.map +1 -0
  315. package/dist/workspace/checks/ci-branch.js +133 -0
  316. package/dist/workspace/checks/links.d.ts +3 -0
  317. package/dist/workspace/checks/links.d.ts.map +1 -1
  318. package/dist/workspace/checks/links.js +12 -1
  319. package/dist/workspace/checks/org.d.ts +48 -0
  320. package/dist/workspace/checks/org.d.ts.map +1 -0
  321. package/dist/workspace/checks/org.js +156 -0
  322. package/dist/workspace/checks.d.ts +11 -1
  323. package/dist/workspace/checks.d.ts.map +1 -1
  324. package/dist/workspace/checks.js +59 -7
  325. package/dist/workspace/ci-branches.d.ts +16 -0
  326. package/dist/workspace/ci-branches.d.ts.map +1 -1
  327. package/dist/workspace/ci-branches.js +27 -0
  328. package/dist/workspace/ci-green-cli.js +7 -7
  329. package/dist/workspace/ci-green-workflow.d.ts +1 -1
  330. package/dist/workspace/ci-green-workflow.d.ts.map +1 -1
  331. package/dist/workspace/ci-green-workflow.js +13 -8
  332. package/dist/workspace/ci-green.d.ts +14 -0
  333. package/dist/workspace/ci-green.d.ts.map +1 -1
  334. package/dist/workspace/ci-green.js +44 -11
  335. package/dist/workspace/ci-run.d.ts +9 -1
  336. package/dist/workspace/ci-run.d.ts.map +1 -1
  337. package/dist/workspace/ci-run.js +67 -6
  338. package/dist/workspace/ci-serve.d.ts +9 -0
  339. package/dist/workspace/ci-serve.d.ts.map +1 -1
  340. package/dist/workspace/ci-serve.js +114 -76
  341. package/dist/workspace/compose-graph.d.ts +23 -0
  342. package/dist/workspace/compose-graph.d.ts.map +1 -1
  343. package/dist/workspace/compose-graph.js +3 -1
  344. package/dist/workspace/composites.d.ts +4 -1
  345. package/dist/workspace/composites.d.ts.map +1 -1
  346. package/dist/workspace/composites.js +5 -2
  347. package/dist/workspace/composites.schema.json +22 -0
  348. package/dist/workspace/conformance/__writer_fixture__/work/work.schema.json +105 -1
  349. package/dist/workspace/conformance/index.d.ts +1 -1
  350. package/dist/workspace/conformance/index.d.ts.map +1 -1
  351. package/dist/workspace/conformance/index.js +39 -2
  352. package/dist/workspace/cross-root-lint.d.ts +56 -0
  353. package/dist/workspace/cross-root-lint.d.ts.map +1 -0
  354. package/dist/workspace/cross-root-lint.js +111 -0
  355. package/dist/workspace/dbt-models-reader.d.ts +44 -0
  356. package/dist/workspace/dbt-models-reader.d.ts.map +1 -0
  357. package/dist/workspace/dbt-models-reader.js +356 -0
  358. package/dist/workspace/dbt-project-common.d.ts +40 -0
  359. package/dist/workspace/dbt-project-common.d.ts.map +1 -0
  360. package/dist/workspace/dbt-project-common.js +106 -0
  361. package/dist/workspace/dbt-project-reader.d.ts +4 -3
  362. package/dist/workspace/dbt-project-reader.d.ts.map +1 -1
  363. package/dist/workspace/dbt-project-reader.js +14 -108
  364. package/dist/workspace/decision-rank.d.ts +26 -0
  365. package/dist/workspace/decision-rank.d.ts.map +1 -0
  366. package/dist/workspace/decision-rank.js +16 -0
  367. package/dist/workspace/declaration.d.ts +43 -2
  368. package/dist/workspace/declaration.d.ts.map +1 -1
  369. package/dist/workspace/declaration.js +45 -11
  370. package/dist/workspace/declaration.schema.json +133 -24
  371. package/dist/workspace/environments.d.ts +26 -2
  372. package/dist/workspace/environments.d.ts.map +1 -1
  373. package/dist/workspace/environments.js +26 -2
  374. package/dist/workspace/flyway-dir.d.ts +39 -0
  375. package/dist/workspace/flyway-dir.d.ts.map +1 -0
  376. package/dist/workspace/flyway-dir.js +104 -0
  377. package/dist/workspace/flyway-layout.d.ts +96 -0
  378. package/dist/workspace/flyway-layout.d.ts.map +1 -0
  379. package/dist/workspace/flyway-layout.js +308 -0
  380. package/dist/workspace/graph-cache.d.ts +8 -0
  381. package/dist/workspace/graph-cache.d.ts.map +1 -1
  382. package/dist/workspace/graph-cache.js +27 -3
  383. package/dist/workspace/graph-cli.d.ts +39 -2
  384. package/dist/workspace/graph-cli.d.ts.map +1 -1
  385. package/dist/workspace/graph-cli.js +92 -32
  386. package/dist/workspace/graph.schema.json +182 -0
  387. package/dist/workspace/intent.d.ts +2 -3
  388. package/dist/workspace/intent.d.ts.map +1 -1
  389. package/dist/workspace/intent.js +5 -9
  390. package/dist/workspace/kind-link-readers.d.ts +59 -2
  391. package/dist/workspace/kind-link-readers.d.ts.map +1 -1
  392. package/dist/workspace/kind-link-readers.js +17 -1
  393. package/dist/workspace/kind-readers.d.ts +3 -0
  394. package/dist/workspace/kind-readers.d.ts.map +1 -1
  395. package/dist/workspace/kind-readers.js +7 -1
  396. package/dist/workspace/kinds.d.ts +66 -2
  397. package/dist/workspace/kinds.d.ts.map +1 -1
  398. package/dist/workspace/kinds.js +98 -4
  399. package/dist/workspace/ledger.schema.json +158 -0
  400. package/dist/workspace/lexicon-outputs.d.ts +20 -1
  401. package/dist/workspace/lexicon-outputs.d.ts.map +1 -1
  402. package/dist/workspace/lexicon-outputs.js +102 -10
  403. package/dist/workspace/lineage-check.d.ts +3 -0
  404. package/dist/workspace/lineage-check.d.ts.map +1 -1
  405. package/dist/workspace/lineage-check.js +23 -4
  406. package/dist/workspace/links.d.ts +8 -1
  407. package/dist/workspace/links.d.ts.map +1 -1
  408. package/dist/workspace/links.js +25 -1
  409. package/dist/workspace/ls.d.ts +8 -0
  410. package/dist/workspace/ls.d.ts.map +1 -1
  411. package/dist/workspace/ls.js +7 -1
  412. package/dist/workspace/ls.schema.json +7 -0
  413. package/dist/workspace/member-commands.d.ts +10 -1
  414. package/dist/workspace/member-commands.d.ts.map +1 -1
  415. package/dist/workspace/member-commands.js +71 -6
  416. package/dist/workspace/migrate-links.d.ts +45 -0
  417. package/dist/workspace/migrate-links.d.ts.map +1 -0
  418. package/dist/workspace/migrate-links.js +100 -0
  419. package/dist/workspace/migration-dirs.d.ts +74 -0
  420. package/dist/workspace/migration-dirs.d.ts.map +1 -0
  421. package/dist/workspace/migration-dirs.js +254 -0
  422. package/dist/workspace/nested-graph.d.ts +2 -0
  423. package/dist/workspace/nested-graph.d.ts.map +1 -1
  424. package/dist/workspace/nested-graph.js +32 -1
  425. package/dist/workspace/reason-codes.d.ts +18 -0
  426. package/dist/workspace/reason-codes.d.ts.map +1 -1
  427. package/dist/workspace/reason-codes.js +23 -0
  428. package/dist/workspace/records-cli.d.ts +11 -0
  429. package/dist/workspace/records-cli.d.ts.map +1 -1
  430. package/dist/workspace/records-cli.js +57 -5
  431. package/dist/workspace/records.schema.json +5 -0
  432. package/dist/workspace/release-candidates.d.ts +2 -0
  433. package/dist/workspace/release-candidates.d.ts.map +1 -1
  434. package/dist/workspace/release-candidates.js +5 -2
  435. package/dist/workspace/release-train.d.ts +53 -0
  436. package/dist/workspace/release-train.d.ts.map +1 -0
  437. package/dist/workspace/release-train.js +134 -0
  438. package/dist/workspace/release-workflow.d.ts.map +1 -1
  439. package/dist/workspace/release-workflow.js +4 -3
  440. package/dist/workspace/repo-ref.d.ts +45 -0
  441. package/dist/workspace/repo-ref.d.ts.map +1 -0
  442. package/dist/workspace/repo-ref.js +114 -0
  443. package/dist/workspace/run-event.schema.json +2 -2
  444. package/dist/workspace/runs-cli.d.ts +9 -0
  445. package/dist/workspace/runs-cli.d.ts.map +1 -1
  446. package/dist/workspace/runs-cli.js +48 -3
  447. package/dist/workspace/runs.schema.json +7 -0
  448. package/dist/workspace/search-cli.d.ts +204 -0
  449. package/dist/workspace/search-cli.d.ts.map +1 -0
  450. package/dist/workspace/search-cli.js +466 -0
  451. package/dist/workspace/search-contract.d.ts +56 -0
  452. package/dist/workspace/search-contract.d.ts.map +1 -0
  453. package/dist/workspace/search-contract.js +70 -0
  454. package/dist/workspace/search.schema.json +822 -0
  455. package/dist/workspace/status-decisions.d.ts +37 -0
  456. package/dist/workspace/status-decisions.d.ts.map +1 -0
  457. package/dist/workspace/status-decisions.js +147 -0
  458. package/dist/workspace/status-gates.d.ts +9 -0
  459. package/dist/workspace/status-gates.d.ts.map +1 -1
  460. package/dist/workspace/status.d.ts +4 -0
  461. package/dist/workspace/status.d.ts.map +1 -1
  462. package/dist/workspace/status.js +14 -2
  463. package/dist/workspace/status.schema.json +6 -0
  464. package/dist/workspace/work-cli.d.ts.map +1 -1
  465. package/dist/workspace/work-cli.js +5 -3
  466. package/dist/workspace/work-evidence-ops.d.ts +83 -0
  467. package/dist/workspace/work-evidence-ops.d.ts.map +1 -0
  468. package/dist/workspace/work-evidence-ops.js +169 -0
  469. package/dist/workspace/work-evidence.d.ts +12 -2
  470. package/dist/workspace/work-evidence.d.ts.map +1 -1
  471. package/dist/workspace/work-evidence.js +21 -3
  472. package/dist/workspace/work-evidence.schema.json +49 -2
  473. package/dist/workspace/workspace-gates.d.ts +110 -0
  474. package/dist/workspace/workspace-gates.d.ts.map +1 -0
  475. package/dist/workspace/workspace-gates.js +140 -0
  476. package/dist/workspace/workspace-kinds.schema.json +58 -3
  477. package/dist/workspace/workspace-ledger.d.ts +76 -0
  478. package/dist/workspace/workspace-ledger.d.ts.map +1 -0
  479. package/dist/workspace/workspace-ledger.js +91 -0
  480. package/dist/workspace/workspace-run.d.ts +112 -0
  481. package/dist/workspace/workspace-run.d.ts.map +1 -0
  482. package/dist/workspace/workspace-run.js +432 -0
  483. package/package.json +1 -1
  484. package/src/__golden__/wave-loop/op-by-apply-after-fails.json +10 -0
  485. package/src/__golden__/wave-loop/op-by-apply-hold.json +10 -0
  486. package/src/__golden__/wave-loop/op-by-apply-ignored.json +10 -0
  487. package/src/__golden__/wave-loop/op-by-apply-pass.json +10 -0
  488. package/src/__golden__/wave-loop/op-by-apply-skip.json +10 -0
  489. package/src/__golden__/wave-loop/op-cost-member-clash.json +6 -0
  490. package/src/__golden__/wave-loop/op-cost-member.json +13 -0
  491. package/src/__golden__/wave-loop/op-denied-source.json +16 -0
  492. package/src/__golden__/wave-loop/op-denied.json +16 -0
  493. package/src/__golden__/wave-loop/op-discover-rest-short.json +6 -0
  494. package/src/__golden__/wave-loop/op-discover-rest-stops.json +13 -0
  495. package/src/__golden__/wave-loop/op-discover-rest.json +26 -0
  496. package/src/__golden__/wave-loop/op-discover-wide.json +26 -0
  497. package/src/__golden__/wave-loop/op-held-on-again.json +16 -0
  498. package/src/__golden__/wave-loop/op-held-on-before-apply.json +16 -0
  499. package/src/__golden__/wave-loop/op-held-on.json +16 -0
  500. package/src/__golden__/wave-loop/op-hooks-apply-fails.json +13 -0
  501. package/src/__golden__/wave-loop/op-hooks-fail-after-apply.json +13 -0
  502. package/src/__golden__/wave-loop/op-hooks-fail-before-apply.json +13 -0
  503. package/src/__golden__/wave-loop/op-hooks-fail-before-plan.json +13 -0
  504. package/src/__golden__/wave-loop/op-hooks-held.json +13 -0
  505. package/src/__golden__/wave-loop/op-hooks-hold-before-apply.json +13 -0
  506. package/src/__golden__/wave-loop/op-hooks-pass.json +13 -0
  507. package/src/__golden__/wave-loop/op-hooks-refused-at-base.json +13 -0
  508. package/src/__golden__/wave-loop/op-hooks-share.json +33 -0
  509. package/src/__golden__/wave-loop/op-outcome-held.json +10 -0
  510. package/src/__golden__/wave-loop/op-outcome-replan-approved.json +10 -0
  511. package/src/__golden__/wave-loop/op-outcome-replan-forever.json +10 -0
  512. package/src/__golden__/wave-loop/op-outcome-replan.json +10 -0
  513. package/src/__golden__/wave-loop/op-outcome-stood-down.json +10 -0
  514. package/src/__golden__/wave-loop/op-outcome-unreadable.json +10 -0
  515. package/src/__golden__/wave-loop/op-overridden.json +16 -0
  516. package/src/__golden__/wave-loop/op-per-run.json +20 -0
  517. package/src/__golden__/wave-loop/op-point.json +13 -0
  518. package/src/__golden__/wave-loop/op-pr-review.json +10 -0
  519. package/src/__golden__/wave-loop/op-sealed.json +10 -0
  520. package/src/__golden__/wave-loop/op-spend-change-only.json +10 -0
  521. package/src/__golden__/wave-loop/op-spend.json +10 -0
  522. package/src/__golden__/wave-loop/op-wave-commands.json +55 -0
  523. package/src/__golden__/wave-loop/op-wide-refused.json +26 -0
  524. package/src/__golden__/wave-loop/op-wide-waiting.json +29 -0
  525. package/src/__golden__/wave-loop/op-wide.json +39 -0
  526. package/src/__snapshots__/okf.test.ts.snap +10 -0
  527. package/src/action-pins.test.ts +78 -0
  528. package/src/action-pins.ts +92 -0
  529. package/src/capabilities/aws.paths +148 -0
  530. package/src/capabilities/base-mappings.json +122 -0
  531. package/src/capabilities/catalog.ts +230 -0
  532. package/src/capabilities/language.test.ts +191 -0
  533. package/src/capabilities/language.ts +373 -0
  534. package/src/capabilities/matcher.ts +315 -0
  535. package/src/capabilities/view-compose.test.ts +76 -0
  536. package/src/capabilities/view.ts +631 -0
  537. package/src/cli/commands/carve-score-once.test.ts +99 -0
  538. package/src/cli/commands/carve.ts +37 -6
  539. package/src/cli/commands/check-lexicon.test.ts +2 -2
  540. package/src/cli/commands/import-report.ts +71 -0
  541. package/src/cli/commands/import.ts +33 -3
  542. package/src/cli/commands/lint.ts +1 -0
  543. package/src/cli/commands/migrate-dialect.test.ts +145 -0
  544. package/src/cli/commands/migrate.ts +132 -11
  545. package/src/cli/handlers/__snapshots__/build-member.test.ts.snap +4 -4
  546. package/src/cli/handlers/approve.ts +23 -2
  547. package/src/cli/handlers/build.ts +7 -1
  548. package/src/cli/handlers/declared-graph.ts +99 -0
  549. package/src/cli/handlers/graph.test.ts +94 -3
  550. package/src/cli/handlers/graph.ts +123 -96
  551. package/src/cli/handlers/ledger.ts +54 -0
  552. package/src/cli/handlers/migrate.ts +21 -12
  553. package/src/cli/handlers/misc.ts +10 -1
  554. package/src/cli/handlers/pr-env.ts +3 -4
  555. package/src/cli/handlers/pr.ts +138 -36
  556. package/src/cli/handlers/query.ts +16 -4
  557. package/src/cli/handlers/release.ts +6 -3
  558. package/src/cli/handlers/search-build-roots.test.ts +144 -0
  559. package/src/cli/handlers/search-count.test.ts +191 -0
  560. package/src/cli/handlers/search-count.ts +165 -0
  561. package/src/cli/handlers/search-json.test.ts +333 -0
  562. package/src/cli/handlers/search-live.test.ts +387 -0
  563. package/src/cli/handlers/search-paths.test.ts +235 -0
  564. package/src/cli/handlers/search.ts +1291 -166
  565. package/src/cli/main.test.ts +43 -3
  566. package/src/cli/main.ts +88 -17
  567. package/src/cli/mcp/server.test.ts +3 -3
  568. package/src/cli/mcp/server.ts +3 -0
  569. package/src/cli/mcp/tools/capabilities.ts +32 -0
  570. package/src/cli/mcp/workspace-tools.e2e.test.ts +21 -1
  571. package/src/cli/mcp/workspace-tools.ts +47 -2
  572. package/src/cli/parse-args.ts +79 -15
  573. package/src/cli/registry.ts +36 -4
  574. package/src/components/builders.ts +8 -0
  575. package/src/components/capability-plugin-loader.ts +13 -1
  576. package/src/components/capability-plugin.ts +16 -0
  577. package/src/components/pr-env-ports.ts +89 -0
  578. package/src/components/pr-env-run-bundle.test.ts +54 -0
  579. package/src/components/pr-env-run.ts +5 -76
  580. package/src/components/pr-environment.ts +2 -1
  581. package/src/components/pr-hooks.test.ts +73 -5
  582. package/src/components/pr-hooks.ts +95 -5
  583. package/src/components/pr-pipeline.ts +64 -16
  584. package/src/components/pr-policy.test.ts +64 -1
  585. package/src/components/pr-policy.ts +53 -0
  586. package/src/components/registry.test.ts +1 -1
  587. package/src/components/starter-plugin.ts +6 -0
  588. package/src/components/verbs/index.ts +2 -0
  589. package/src/components/verbs/publish-artifact.test.ts +60 -0
  590. package/src/components/verbs/publish-artifact.ts +122 -0
  591. package/src/components/verbs/publish-image.test.ts +101 -0
  592. package/src/components/verbs/publish-image.ts +265 -0
  593. package/src/config-environments.ts +160 -0
  594. package/src/config.ts +44 -124
  595. package/src/gate-decision-bundle.test.ts +1 -0
  596. package/src/gate-decision-identity.test.ts +9 -0
  597. package/src/gate-decision.ts +26 -3
  598. package/src/graph-detail.ts +15 -3
  599. package/src/graph-effective.ts +226 -75
  600. package/src/graph-ir.test.ts +163 -0
  601. package/src/graph-ir.ts +174 -0
  602. package/src/graph-sub-template.ts +22 -0
  603. package/src/graph-walk.test.ts +44 -0
  604. package/src/graph-walk.ts +102 -0
  605. package/src/import/parser.ts +20 -0
  606. package/src/lexicon.ts +84 -6
  607. package/src/lifecycle/gate-ledger.ts +8 -0
  608. package/src/lifecycle/git.ts +2 -2
  609. package/src/lifecycle/ledger-read.test.ts +107 -0
  610. package/src/lifecycle/ledger-read.ts +195 -0
  611. package/src/lifecycle/observe.test.ts +27 -1
  612. package/src/lifecycle/observe.ts +43 -1
  613. package/src/lifecycle/release-workstation.ts +24 -0
  614. package/src/lifecycle/run-ledger.ts +5 -2
  615. package/src/lint/cross-root.test.ts +122 -0
  616. package/src/lint/discover.ts +3 -1
  617. package/src/lint/post-synth.ts +212 -2
  618. package/src/lint/rule.ts +6 -0
  619. package/src/lint/rules/op/index.ts +2 -0
  620. package/src/lint/rules/op/ops019-workspace-op-target.ts +89 -0
  621. package/src/live-endpoint.ts +1 -1
  622. package/src/observation.test.ts +25 -0
  623. package/src/observation.ts +69 -5
  624. package/src/op/activities/activity-contracts.ts +5 -2
  625. package/src/op/activities/index.ts +2 -2
  626. package/src/op/activities/review-agent.test.ts +221 -4
  627. package/src/op/activities/review-agent.ts +265 -22
  628. package/src/op/activities/work-evidence.ts +9 -1
  629. package/src/op/builders.ts +9 -5
  630. package/src/op/composites/apply-op.test.ts +108 -0
  631. package/src/op/composites/apply-op.ts +37 -2
  632. package/src/op/gate-approval.test.ts +6 -1
  633. package/src/op/gate-approval.ts +12 -6
  634. package/src/op/gate-quorum-at-base.test.ts +211 -0
  635. package/src/op/gate-quorum.ts +156 -8
  636. package/src/op/gate-review.ts +14 -0
  637. package/src/op/gate.ts +67 -8
  638. package/src/op/index.ts +3 -2
  639. package/src/op/local-executor.ts +69 -9
  640. package/src/op/op-waves-depends-on.test.ts +81 -0
  641. package/src/op/op-waves-hook-targets.test.ts +4 -2
  642. package/src/op/op-waves-quorum-product.test.ts +158 -0
  643. package/src/op/op-waves-run.ts +288 -14
  644. package/src/op/op-waves.ts +143 -14
  645. package/src/op/runtime.ts +8 -0
  646. package/src/op/schedule-overdue.ts +33 -0
  647. package/src/op/stage-hooks.ts +13 -5
  648. package/src/op/workspace-run-env.ts +29 -0
  649. package/src/pr-forge-bundle.test.ts +2 -2
  650. package/src/pr-forge.test.ts +47 -0
  651. package/src/pr-forge.ts +68 -0
  652. package/src/pr-post-open.e2e.test.ts +36 -0
  653. package/src/pr-post.test.ts +102 -3
  654. package/src/pr-post.ts +89 -1
  655. package/src/read-filter.test.ts +54 -0
  656. package/src/read-filter.ts +193 -0
  657. package/src/run-event.ts +1 -1
  658. package/src/stage-hooks-bundle.test.ts +1 -0
  659. package/src/terraform/carve.ts +52 -15
  660. package/src/terraform/cuts.test.ts +173 -0
  661. package/src/terraform/cuts.ts +188 -0
  662. package/src/terraform/folds.ts +44 -0
  663. package/src/terraform/graph.ts +9 -0
  664. package/src/terraform/radius.test.ts +90 -0
  665. package/src/terraform/reach.test.ts +146 -0
  666. package/src/terraform/reach.ts +247 -0
  667. package/src/terraform/score.ts +27 -29
  668. package/src/terraform/state.ts +3 -1
  669. package/src/terraform/types.ts +8 -0
  670. package/src/wave-blocked.ts +28 -3
  671. package/src/wave-loop-golden.test.ts +4 -3
  672. package/src/workspace/behold-kinds.test.ts +1 -1
  673. package/src/workspace/capabilities-contract.test.ts +88 -0
  674. package/src/workspace/capabilities-contract.ts +132 -0
  675. package/src/workspace/capabilities.schema.json +229 -0
  676. package/src/workspace/check-contract.test.ts +7 -6
  677. package/src/workspace/check.schema.json +11 -2
  678. package/src/workspace/checks/ci-branch.test.ts +128 -0
  679. package/src/workspace/checks/ci-branch.ts +141 -0
  680. package/src/workspace/checks/links.ts +12 -1
  681. package/src/workspace/checks/org.ts +169 -0
  682. package/src/workspace/checks.ts +64 -7
  683. package/src/workspace/ci-branches.ts +31 -0
  684. package/src/workspace/ci-green-branches.test.ts +153 -0
  685. package/src/workspace/ci-green-cli.ts +7 -7
  686. package/src/workspace/ci-green-workflow.test.ts +4 -3
  687. package/src/workspace/ci-green-workflow.ts +13 -9
  688. package/src/workspace/ci-green.ts +49 -12
  689. package/src/workspace/ci-run.test.ts +72 -1
  690. package/src/workspace/ci-run.ts +68 -8
  691. package/src/workspace/ci-serve.test.ts +95 -0
  692. package/src/workspace/ci-serve.ts +106 -65
  693. package/src/workspace/component-environments.test.ts +25 -2
  694. package/src/workspace/compose-graph.ts +19 -3
  695. package/src/workspace/composites.schema.json +22 -0
  696. package/src/workspace/composites.test.ts +39 -8
  697. package/src/workspace/composites.ts +6 -2
  698. package/src/workspace/conformance/__writer_fixture__/work/work.schema.json +105 -1
  699. package/src/workspace/conformance/conformance.e2e.test.ts +14 -1
  700. package/src/workspace/conformance/index.ts +31 -2
  701. package/src/workspace/cross-root-lint.test.ts +90 -0
  702. package/src/workspace/cross-root-lint.ts +126 -0
  703. package/src/workspace/dbt-kind.test.ts +3 -2
  704. package/src/workspace/dbt-models-reader.ts +362 -0
  705. package/src/workspace/dbt-models.test.ts +248 -0
  706. package/src/workspace/dbt-project-common.ts +113 -0
  707. package/src/workspace/dbt-project-reader.ts +29 -112
  708. package/src/workspace/decision-rank.ts +40 -0
  709. package/src/workspace/declaration.schema.json +133 -24
  710. package/src/workspace/declaration.ts +84 -15
  711. package/src/workspace/environments.ts +40 -4
  712. package/src/workspace/flyway-dir.ts +113 -0
  713. package/src/workspace/flyway-layout.ts +337 -0
  714. package/src/workspace/graph-cache.ts +27 -3
  715. package/src/workspace/graph-cli.ts +119 -32
  716. package/src/workspace/graph-contract.test.ts +4 -2
  717. package/src/workspace/graph.schema.json +182 -0
  718. package/src/workspace/identity.test.ts +14 -2
  719. package/src/workspace/intent.ts +6 -14
  720. package/src/workspace/kind-link-readers.ts +68 -3
  721. package/src/workspace/kind-readers.e2e.test.ts +28 -0
  722. package/src/workspace/kind-readers.test.ts +1 -1
  723. package/src/workspace/kind-readers.ts +7 -1
  724. package/src/workspace/kinds.test.ts +12 -7
  725. package/src/workspace/kinds.ts +132 -7
  726. package/src/workspace/ledger.schema.json +158 -0
  727. package/src/workspace/lexicon-outputs.ts +132 -13
  728. package/src/workspace/lineage-check.ts +30 -7
  729. package/src/workspace/links.ts +24 -1
  730. package/src/workspace/ls-contract.test.ts +1 -1
  731. package/src/workspace/ls.schema.json +7 -0
  732. package/src/workspace/ls.ts +10 -1
  733. package/src/workspace/member-commands.ts +82 -3
  734. package/src/workspace/member-file-schema.test.ts +124 -0
  735. package/src/workspace/migrate-links.test.ts +99 -0
  736. package/src/workspace/migrate-links.ts +108 -0
  737. package/src/workspace/migration-dirs.test.ts +272 -0
  738. package/src/workspace/migration-dirs.ts +267 -0
  739. package/src/workspace/nested-graph.ts +32 -1
  740. package/src/workspace/org-scope.test.ts +238 -0
  741. package/src/workspace/reason-codes.test.ts +13 -1
  742. package/src/workspace/reason-codes.ts +23 -0
  743. package/src/workspace/records-cli.ts +58 -5
  744. package/src/workspace/records.schema.json +5 -0
  745. package/src/workspace/release-candidates.ts +7 -2
  746. package/src/workspace/release-train.ts +157 -0
  747. package/src/workspace/release-workflow.ts +4 -3
  748. package/src/workspace/repo-ref.ts +113 -0
  749. package/src/workspace/run-event.schema.json +2 -2
  750. package/src/workspace/runs-cli.ts +48 -4
  751. package/src/workspace/runs.schema.json +7 -0
  752. package/src/workspace/search-cli.e2e.test.ts +305 -0
  753. package/src/workspace/search-cli.ts +591 -0
  754. package/src/workspace/search-contract.ts +95 -0
  755. package/src/workspace/search.schema.json +822 -0
  756. package/src/workspace/status-contract.test.ts +46 -1
  757. package/src/workspace/status-decisions.test.ts +100 -0
  758. package/src/workspace/status-decisions.ts +151 -0
  759. package/src/workspace/status-gates.ts +9 -0
  760. package/src/workspace/status.schema.json +6 -0
  761. package/src/workspace/status.ts +17 -3
  762. package/src/workspace/where-filters.test.ts +39 -0
  763. package/src/workspace/work-cli.ts +5 -3
  764. package/src/workspace/work-evidence-ops.test.ts +176 -0
  765. package/src/workspace/work-evidence-ops.ts +189 -0
  766. package/src/workspace/work-evidence.schema.json +49 -2
  767. package/src/workspace/work-evidence.ts +22 -3
  768. package/src/workspace/workspace-gates.ts +200 -0
  769. package/src/workspace/workspace-kinds.schema.json +58 -3
  770. package/src/workspace/workspace-ledger.ts +130 -0
  771. package/src/workspace/workspace-run.test.ts +322 -0
  772. package/src/workspace/workspace-run.ts +461 -0
@@ -2,16 +2,23 @@ import { resolve } from "node:path";
2
2
  import { build } from "../../build.js";
3
3
  import { buildGraphIr, buildLiveGraphIr, collectUnobserved, sourceOverlayGraphs } from "../../graph-ir.js";
4
4
  import { buildDeclaredPerStack } from "../../graph-declared.js";
5
- import { enrichEffectiveTopology } from "../../graph-effective.js";
5
+ import { enrichEffectiveTopology, terraformResourceType, terraformType } from "../../graph-effective.js";
6
+ import { attachCapabilities, attachView, capabilityEngine, parseCapabilityCall } from "../../capabilities/catalog.js";
7
+ import { CapabilityError } from "../../capabilities/matcher.js";
6
8
  import { reconstructEdges, mergeCatalogs } from "../../graph-refs.js";
7
9
  import { discover } from "../../discovery/index.js";
8
- import { observeResources } from "../../lifecycle/observe.js";
10
+ import { carryAnnotations, observeResources } from "../../lifecycle/observe.js";
9
11
  import { replaySnapshots, hasSnapshot } from "../../lifecycle/replay.js";
10
12
  import { diffLive } from "../../lifecycle/live-diff.js";
11
13
  import { formatUnobserved } from "../../observation.js";
14
+ import { readChantPackageVersion } from "../../package-version.js";
12
15
  import { loadChantConfig, matchesDeclaredEnvironment } from "../../config.js";
13
- import { loadPlugins, resolveProjectLexicons } from "../plugins.js";
16
+ import { collectBuildRootContributors, loadPlugins, resolveProjectLexicons } from "../plugins.js";
17
+ import { declaredBuildRootContributors, mergeDeclaredBuildRoots, withLexiconEdges } from "./declared-graph.js";
18
+ import { parseCountBy, countGroups, countsJson, formatCounts, groupMembers, hasManyTerraformRoots } from "./search-count.js";
14
19
  import { formatError, formatWarning } from "../format.js";
20
+ import { adjacencyOf, walkBreadthFirst } from "../../graph-walk.js";
21
+ import { SEARCH_CONTRACT_VERSION, SEARCH_OUTPUT_SCHEMA_ID } from "../../workspace/search-contract.js";
15
22
  /**
16
23
  * `chant search <query> [--live --env <name>]` — answer an estate question with
17
24
  * a COMPACT result instead of the whole graph. The point (measured on aws-bench,
@@ -20,11 +27,14 @@ import { formatError, formatWarning } from "../format.js";
20
27
  *
21
28
  * Query grammar (space-separated terms, all must match — AND):
22
29
  * bare word case-insensitive substring over id, kind, and attrs
23
- * kind:<substr> node kind contains <substr> (e.g. kind:EC2::Instance)
30
+ * kind:<substr> node kind contains <substr> (e.g. kind:EC2::Instance);
31
+ * on a terraform resource, its type (kind:aws_instance, kind:aws_iam_)
24
32
  * tag:<key>=<val> a Tags entry with Key=key and Value containing val
25
33
  * attr:<name>=<val> attribute <name> equals/contains <val>
26
34
  * ->kind:X / ->attr:.. this node has an edge TO a node matching the right side
27
35
  * <-kind:X / <-attr:.. this node has an edge FROM a node matching the right side
36
+ * ->*X, ->{n}X, ->{m,n}X the far end is any depth / n / m..n hops away (#4359)
37
+ * ->[via=a]X, ->[to=a]X only edges through consumer attr a / reading producer attr a
28
38
  *
29
39
  * The edge operators are the point of "edge-aware" search (#1139): a small
30
40
  * model shouldn't hand-join instance→subnet→public across many results — one
@@ -38,46 +48,36 @@ import { formatError, formatWarning } from "../format.js";
38
48
  export async function runSearch(ctx) {
39
49
  const { args } = ctx;
40
50
  const query = (args.path ?? "").trim();
41
- if (!query) {
42
- console.error(formatError({ message: "chant search needs a query: chant search \"<terms>\" [--live --env <name>]" }));
51
+ // A refusal (#4367): the error on stderr as always, and under --json the
52
+ // same message and hint as the one document on stdout, so a program reading
53
+ // stdout sees why there are no rows. Exit 1 either way. The code is from the
54
+ // read contract's closed list (#4374).
55
+ const refuse = (error) => {
56
+ console.error(formatError(error));
57
+ if (args.json)
58
+ console.log(JSON.stringify(searchErrorJson(query, error)));
43
59
  return 1;
60
+ };
61
+ // The capability catalog (#4375): the `capabilities` document, not an answer.
62
+ if (args.capabilities)
63
+ return printCapabilities(args, query, await resolveProjectLexicons(resolve(".")).catch(() => []));
64
+ // The snapshot to replay (#4374): `--snapshot <ref>`, or `--at <ref>`, which
65
+ // plain search keeps for compatibility. Everywhere else in the read contract
66
+ // `--at` is a git revision, so the workspace form takes only `--snapshot`.
67
+ if (args.snapshot !== undefined && args.at !== undefined && String(args.snapshot) !== String(args.at)) {
68
+ return refuse({
69
+ code: "usage-invalid",
70
+ message: `chant search was given --snapshot ${args.snapshot} and --at ${args.at}`,
71
+ hint: "they name the same thing, the recorded snapshot to answer from; give one",
72
+ });
44
73
  }
45
- let terms;
46
- try {
47
- terms = parseQuery(query);
48
- }
49
- catch (err) {
50
- if (err instanceof QueryError) {
51
- console.error(formatError({ message: err.message, hint: err.hint }));
52
- return 1;
53
- }
54
- throw err;
55
- }
74
+ const snapshotRef = args.snapshot ?? (args.at !== undefined ? String(args.at) : undefined);
75
+ const snapshotFlag = args.snapshot !== undefined ? "--snapshot" : "--at";
76
+ const checked = checkSearchArgs(args, query, { ref: snapshotRef, flag: snapshotFlag });
77
+ if ("error" in checked)
78
+ return refuse(checked.error);
79
+ const { terms, countBy } = checked;
56
80
  const show = parseShow(args);
57
- // Query-scoped drift (#1268): --check-live checks a snapshot answer against
58
- // a live read, --check-snapshot checks a live answer against the recorded
59
- // snapshot — the direction each needs is the source it does NOT already have.
60
- if (args.checkLive && !args.at) {
61
- console.error(formatError({
62
- message: "chant search --check-live needs --at <ref>",
63
- hint: "answer from a snapshot and check the matched rows against a live read: --at latest --check-live --env <name>",
64
- }));
65
- return 1;
66
- }
67
- if (args.checkSnapshot && !args.live) {
68
- console.error(formatError({
69
- message: "chant search --check-snapshot needs --live",
70
- hint: "answer live and check the matched rows against the recorded snapshot: --live --check-snapshot --env <name>",
71
- }));
72
- return 1;
73
- }
74
- if (args.failOnDrift && !args.checkLive && !args.checkSnapshot) {
75
- console.error(formatError({
76
- message: "chant search --fail-on-drift needs --check-live or --check-snapshot",
77
- hint: "it fails the check those flags run, so it is meaningless without one",
78
- }));
79
- return 1;
80
- }
81
81
  const projectPath = resolve(".");
82
82
  const { config } = await loadChantConfig(projectPath);
83
83
  let ir;
@@ -89,6 +89,9 @@ export async function runSearch(ctx) {
89
89
  // on this path" — printed with the provenance footer, after the rows, so the
90
90
  // answer is not preceded by what qualifies it.
91
91
  let liveNotes = [];
92
+ // Which read answered each entity (#2267), from a live read.
93
+ let readSources = {};
94
+ let rootReads = {};
92
95
  // Kinds that can exist in the account without being declared (#1278). Known
93
96
  // without a scan, so it costs nothing to mention.
94
97
  let ambientKinds = [];
@@ -102,58 +105,51 @@ export async function runSearch(ctx) {
102
105
  let declaredForDrift;
103
106
  let snapshotDepth;
104
107
  let driftFailure = false;
105
- if (args.live || args.at) {
106
- const environment = args.env;
107
- if (!environment) {
108
- const flag = args.at ? "--at" : "--live";
109
- console.error(formatError({ message: `chant search ${flag} needs an environment: ${flag} --env <name>` }));
110
- return 1;
111
- }
112
- if (args.live && args.at) {
113
- // Two different observations of the same estate, and no rule for which
114
- // wins. Comparing them is a real question (#1268) but it is not this one.
115
- console.error(formatError({
116
- message: "chant search takes --live or --at, not both",
117
- hint: "--live reads the estate now; --at answers from a recorded snapshot",
118
- }));
119
- return 1;
120
- }
108
+ // Why --check-snapshot had nothing to check against, for --json (#4367).
109
+ let driftUnavailable;
110
+ if (args.live || snapshotRef) {
111
+ // checkSearchArgs refused a live or snapshot read without one.
112
+ const environment = String(args.env);
121
113
  // Membership via matchesDeclaredEnvironment (#1221): handles `{ name,
122
114
  // endpoint }` entries (#1166) — a plain `.includes` never matched those —
123
115
  // and glob-pattern entries like `"pr-*"`.
124
116
  if (config.environments && config.environments.length > 0 && !matchesDeclaredEnvironment(config.environments, environment)) {
125
- console.error(formatError({ message: `Unknown environment "${environment}"` }));
126
- return 1;
117
+ return refuse({ code: "environment-unknown", message: `Unknown environment "${environment}"` });
127
118
  }
128
119
  const plugins = ctx.plugins.length > 0 ? ctx.plugins : await loadPlugins(await resolveProjectLexicons(projectPath));
120
+ // Build roots too (#4358), as `chant graph --live` does: a terraform
121
+ // root's blocks are entities only once its root is rendered, and an
122
+ // entity the build never produced is never observed.
123
+ const buildRoots = collectBuildRootContributors(plugins, config, projectPath);
129
124
  const buildResult = await build(resolve(args.src ?? config.sourceDir ?? "."), plugins.map((p) => p.serializer), undefined, {
125
+ buildRoots,
130
126
  config: config,
131
127
  });
132
128
  if (buildResult.errors.length > 0) {
133
- console.error(formatError({ message: "Build failed — fix errors before searching live state" }));
134
- return 1;
129
+ return refuse({ code: "build-failed", message: "Build failed — fix errors before searching live state" });
135
130
  }
136
131
  const observing = plugins.filter((p) => p.describeResources);
137
132
  ambientKinds = observing.flatMap((p) => p.ambientKinds?.() ?? []);
138
133
  const stacks = (config.stacks ?? []).map((s) => ({ name: s.name, region: s.region, src: s.src }));
139
134
  let observations;
135
+ // Facts about absent entities (#4372): a planned create's plan attributes.
136
+ let annotations;
140
137
  const liveAttrs = {};
141
- if (args.at) {
138
+ if (snapshotRef) {
142
139
  // Answer from a recorded observation (#1266). Everything downstream is
143
140
  // the live path's — the point is that a snapshot replays into the same
144
141
  // shape a live read produces, so one pipeline serves both and a fold
145
142
  // improvement reaches an old snapshot for free.
146
143
  const scoped = new Set(stacks.filter((st) => st.src).map((st) => st.name));
147
- const replay = await replaySnapshots(environment, String(args.at), scoped);
144
+ const replay = await replaySnapshots(environment, snapshotRef, scoped);
148
145
  if ("error" in replay) {
149
- console.error(formatError({ message: replay.error, ...(replay.hint ? { hint: replay.hint } : {}) }));
150
- return 1;
146
+ return refuse({ code: "snapshot-unavailable", message: replay.error, ...(replay.hint ? { hint: replay.hint } : {}) });
151
147
  }
152
148
  observations = replay.observations;
153
149
  replayAmbient = {
154
150
  recordedAmbient: replay.observations.some((o) => Object.values(o.resources).some((m) => m.ambient === true)),
155
151
  };
156
- source = { kind: "snapshot", commit: replay.commit, timestamp: replay.timestamp };
152
+ source = { kind: "snapshot", snapshot: snapshotRef, commit: replay.commit, timestamp: replay.timestamp };
157
153
  snapshotDepth = replay.depth;
158
154
  if (args.checkLive) {
159
155
  // A fresh live read, additional to the snapshot the answer itself came
@@ -184,6 +180,9 @@ export async function runSearch(ctx) {
184
180
  console.error(formatError({ message: `live read failed — ${e}` }));
185
181
  }
186
182
  observations = observed.observations;
183
+ annotations = observed.annotations;
184
+ readSources = observed.sources ?? {};
185
+ rootReads = observed.rootReads ?? {};
187
186
  liveNotes = observed.notes ?? [];
188
187
  source = { kind: "live" };
189
188
  if (args.checkSnapshot) {
@@ -195,6 +194,7 @@ export async function runSearch(ctx) {
195
194
  const checkReplay = await replaySnapshots(environment, "latest", scoped);
196
195
  if ("error" in checkReplay) {
197
196
  console.error(formatWarning({ message: `--check-snapshot: ${checkReplay.error}` }));
197
+ driftUnavailable = checkReplay.error;
198
198
  }
199
199
  else {
200
200
  checkObservations = checkReplay.observations;
@@ -208,7 +208,7 @@ export async function runSearch(ctx) {
208
208
  let containmentEdges = [];
209
209
  // Live-only: enrichment is a provider call, so it has no place in a replay.
210
210
  // A recorded answer that quietly reached for the API would stop being one.
211
- if (!args.at) {
211
+ if (!snapshotRef) {
212
212
  for (const p of observing) {
213
213
  if (!p.enrichLiveAttrs)
214
214
  continue;
@@ -265,9 +265,16 @@ export async function runSearch(ctx) {
265
265
  // `${stack}::${id}` — matching how observation qualifies. A flat whole-
266
266
  // project discovery would disambiguate colliding names by module path
267
267
  // (UsEast1Src…), which never matches the observed bare LogicalResourceIds.
268
+ //
269
+ // The flat canvas is the declared path's graph (#4365, #4358): discovery
270
+ // plus the build-root contributions (a terraform root's blocks, a rendered
271
+ // kustomize root) plus the lexicon edges, so a live answer over a Terraform
272
+ // root overlays onto a canvas that has the root's resources on it.
268
273
  const declared = stacks.length > 0
269
- ? await buildDeclaredPerStack(stacks, projectPath)
270
- : buildGraphIr((await discover(resolve(args.src ?? config.sourceDir ?? "."))).entities, projectPath);
274
+ ? await buildDeclaredPerStack(stacks, projectPath, (g, entities) => withLexiconEdges(g, plugins, entities))
275
+ : await declaredGraph(ctx, projectPath, resolve(args.src ?? config.sourceDir ?? "."), plugins);
276
+ if (!declared)
277
+ return refuse(BUILD_ROOT_FAILED);
271
278
  declaredForDrift = declared;
272
279
  // Carry the NOT-OBSERVED half of the tri-state (#1089) onto the rows, so a
273
280
  // declared entity nobody could read is painted `_unobserved` and a row can
@@ -305,21 +312,102 @@ export async function runSearch(ctx) {
305
312
  // account's default VPC, whose route table chant does not model). The
306
313
  // overlay copies physical identity but not attrs, so merge them here.
307
314
  ir = { ...ir, nodes: ir.nodes.map((n) => (liveAttrs[n.id] ? { ...n, attrs: { ...n.attrs, ...liveAttrs[n.id] } } : n)) };
315
+ // And what the read learned about an entity it found absent (#4372), so
316
+ // `attr:planAction=create` matches a block the plan will create. The node
317
+ // stays pending: only its attributes change.
318
+ ir = { ...ir, nodes: carryAnnotations(ir.nodes, annotations) };
308
319
  }
309
320
  else {
310
- const discovered = await discover(resolve(args.src ?? config.sourceDir ?? "."));
311
- ir = buildGraphIr(discovered.entities);
321
+ const declared = await declaredGraph(ctx, projectPath, resolve(args.src ?? config.sourceDir ?? "."));
322
+ if (!declared)
323
+ return refuse(BUILD_ROOT_FAILED);
324
+ ir = declared;
312
325
  }
313
326
  // Fold derived reachability facts (effectiveIngress, internetFacing) onto
314
327
  // instance nodes so multi-hop/launch-template joins are one node predicate (#1139).
315
328
  ir = enrichEffectiveTopology(ir);
329
+ // Capability terms (#4375) read the graph through the lexicons' mappings.
330
+ if (terms.some(usesCapability)) {
331
+ const refused = await bindCapabilities(ctx, projectPath, ir, terms);
332
+ if (refused)
333
+ return refuse({ code: "query-invalid", ...refused });
334
+ }
316
335
  // Resolve `kind:` terms against what is actually in this graph, before any
317
336
  // node is tested against them.
318
337
  resolveKindTerms(terms, ir.nodes);
319
338
  const nodeById = new Map(ir.nodes.map((n) => [n.id, n]));
320
339
  const matches = ir.nodes.filter((n) => terms.every((t) => matchTerm(n, t, ir, nodeById)));
340
+ // Query-scoped drift (#1268): the matched rows, and only the matched rows,
341
+ // checked against the observation the primary answer did NOT use. Scoping
342
+ // both sides of diffLive() to the matched ids is what keeps a query-scoped-
343
+ // out resource from reading as `missing` or `unobserved` — it was never
344
+ // asked about, which is a third thing from both.
345
+ let drift;
346
+ if (checkObservations && declaredForDrift && primaryObservations) {
347
+ const declaredIds = new Set(declaredForDrift.nodes.map((n) => n.id));
348
+ const matchedIds = new Set(matches.map((n) => n.id));
349
+ const nowObservations = args.checkLive ? checkObservations : primaryObservations;
350
+ const thenObservations = args.checkLive ? primaryObservations : checkObservations;
351
+ drift = diffLive({
352
+ declared: new Set([...matchedIds].filter((id) => declaredIds.has(id))),
353
+ observedNow: pickResources(mergeResources(nowObservations), matchedIds),
354
+ observedThen: pickResources(mergeResources(thenObservations), matchedIds),
355
+ unobserved: pickResources(collectUnobserved(nowObservations), matchedIds),
356
+ });
357
+ }
358
+ // Name the plan read when it answered (#4384).
359
+ if (source.kind === "live")
360
+ source = withReadDepth(source, matches, readSources, rootReads);
361
+ const driftAgainst = args.checkLive ? "live" : "snapshot";
362
+ // Machine-readable answer (#4359, #4367): the rows, their paths, and every
363
+ // footer the text prints as prose, as one document for a caller that draws
364
+ // it (behold#913). Same exit codes as the text path.
365
+ if (args.json) {
366
+ const recorded = source.kind === "live" && !matches.some((n) => n.physicalId) && args.env
367
+ ? (await hasSnapshot(String(args.env))) ? "yes" : undefined
368
+ : undefined;
369
+ const env = args.env && source.kind !== "declared" ? String(args.env) : undefined;
370
+ const provenanceText = provenanceLine(matches, source, recorded, liveFailures, [...derivedUsed(terms, matches, ir), ...capabilitiesUsed(terms)]).replace(/^— /, "");
371
+ const capNotes = capabilityNotes(terms, ir, source);
372
+ const plannedCreates = source.kind !== "declared" && liveFailures.length === 0 ? plannedCreateCount(matches) : 0;
373
+ const driftJson = drift
374
+ ? searchDriftJson(drift, matches.length, driftAgainst, snapshotDepth)
375
+ : driftUnavailable !== undefined
376
+ ? { against: driftAgainst, unavailable: driftUnavailable }
377
+ : undefined;
378
+ // With --count-by (#4360) the object carries the groups and the total in
379
+ // place of the matches, the same answer the grouped lines give, with the
380
+ // provenance line beside it (#4367).
381
+ const head = { ...searchHead(), ...(await workspaceIdentity(projectPath)) };
382
+ const payload = countBy
383
+ ? {
384
+ ...head,
385
+ ...countsJson(query, env ? { ...source, env } : source, groupMembers(matches, countBy, hasManyTerraformRoots(ir)), countBy, liveFailures),
386
+ provenance: provenanceText,
387
+ ...(plannedCreates > 0 ? { plannedCreates } : {}),
388
+ ...(driftJson ? { drift: driftJson } : {}),
389
+ }
390
+ : {
391
+ ...head,
392
+ ...searchJson(query, source, matches, terms, ir, nodeById, liveFailures, {
393
+ ...(env ? { env } : {}),
394
+ show,
395
+ provenance: provenanceText,
396
+ ...(plannedCreates > 0 ? { plannedCreates } : {}),
397
+ ...(capNotes.length > 0 ? { capabilities: capNotes } : {}),
398
+ ...(args.explain ? { explain: explainData(terms, matches, ir, nodeById) } : {}),
399
+ ...(driftJson ? { drift: driftJson } : {}),
400
+ }),
401
+ };
402
+ console.log(JSON.stringify(payload));
403
+ for (const n of liveNotes)
404
+ console.error(formatWarning({ message: n }));
405
+ if (liveFailures.length > 0)
406
+ return 1;
407
+ return args.failOnDrift && drift && driftCountOf(drift) > 0 ? 1 : 0;
408
+ }
321
409
  if (matches.length === 0) {
322
- console.log("(no matches)");
410
+ console.log(countBy ? "(no matches) · 0 total" : "(no matches)");
323
411
  availableAttrs(terms, ir);
324
412
  if (args.explain)
325
413
  explain(terms, matches, ir, nodeById, query);
@@ -327,8 +415,14 @@ export async function runSearch(ctx) {
327
415
  console.error(formatWarning({ message: n }));
328
416
  return 0;
329
417
  }
330
- for (const n of matches) {
331
- console.log(formatRow(n, show));
418
+ if (countBy) {
419
+ for (const line of formatCounts(countGroups(matches, countBy, hasManyTerraformRoots(ir)), countBy))
420
+ console.log(line);
421
+ }
422
+ else {
423
+ for (const n of matches) {
424
+ console.log(formatRow(n, show));
425
+ }
332
426
  }
333
427
  const backed = source.kind === "declared" || matches.some((n) => n.physicalId);
334
428
  // Only worth asking when the live read came back empty — that is the one case
@@ -336,33 +430,26 @@ export async function runSearch(ctx) {
336
430
  const recorded = source.kind === "live" && !matches.some((n) => n.physicalId) && args.env
337
431
  ? (await hasSnapshot(String(args.env))) ? "yes" : undefined
338
432
  : undefined;
339
- provenance(matches, source, recorded, liveFailures);
433
+ provenance(matches, source, recorded, liveFailures, [...derivedUsed(terms, matches, ir), ...capabilitiesUsed(terms)]);
434
+ for (const c of capabilityNotes(terms, ir, source)) {
435
+ if (c.notEvaluated.length > 0)
436
+ console.log(`— ${c.name} does not evaluate: ${c.notEvaluated.join("; ")}`);
437
+ }
340
438
  // Qualifies the provenance line, so it sits with it: one line per distinct
341
439
  // note for the whole run, not one per stack, and after the rows (#1265).
342
440
  for (const n of liveNotes)
343
441
  console.error(formatWarning({ message: n }));
344
- // Query-scoped drift (#1268): the matched rows, and only the matched rows,
345
- // checked against the observation the primary answer did NOT use. Scoping
346
- // both sides of diffLive() to the matched ids is what keeps a query-scoped-
347
- // out resource from reading as `missing` or `unobserved` — it was never
348
- // asked about, which is a third thing from both.
349
- if (checkObservations && declaredForDrift && primaryObservations) {
350
- const declaredIds = new Set(declaredForDrift.nodes.map((n) => n.id));
351
- const matchedIds = new Set(matches.map((n) => n.id));
352
- const nowObservations = args.checkLive ? checkObservations : primaryObservations;
353
- const thenObservations = args.checkLive ? primaryObservations : checkObservations;
354
- const driftCount = renderDrift(diffLive({
355
- declared: new Set([...matchedIds].filter((id) => declaredIds.has(id))),
356
- observedNow: pickResources(mergeResources(nowObservations), matchedIds),
357
- observedThen: pickResources(mergeResources(thenObservations), matchedIds),
358
- unobserved: pickResources(collectUnobserved(nowObservations), matchedIds),
359
- }), matches.length, args.checkLive ? "live" : "snapshot", snapshotDepth);
442
+ if (drift) {
443
+ const driftCount = renderDrift(drift, matches.length, driftAgainst, snapshotDepth);
360
444
  if (args.failOnDrift && driftCount > 0)
361
445
  driftFailure = true;
362
446
  }
363
447
  ambientHint(matches, ambientKinds, args.ambient === true, replayAmbient);
364
- showMiss(matches, show);
365
- regionSpread(terms, matches, show);
448
+ // Both point at `--show`, which a grouped count does not print (#4360).
449
+ if (!countBy) {
450
+ showMiss(matches, show);
451
+ regionSpread(terms, matches, show);
452
+ }
366
453
  derivedSurface(terms, matches, ir, backed);
367
454
  if (args.explain)
368
455
  explain(terms, matches, ir, nodeById, query);
@@ -383,6 +470,317 @@ export async function runSearch(ctx) {
383
470
  return 1;
384
471
  return 0;
385
472
  }
473
+ /**
474
+ * Check a search's query and flags before anything is read (#4374): the
475
+ * grammar, `--count-by`, and the flags that need or exclude each other. The
476
+ * rules `chant search` and `chant workspace search` share, so the workspace
477
+ * form refuses once, with the same code and message, instead of once per
478
+ * member. `snapshot` is the snapshot asked for and the flag that asked:
479
+ * `--snapshot`, or plain search's `--at`.
480
+ */
481
+ export function checkSearchArgs(args, query, snapshot) {
482
+ const flag = snapshot.flag;
483
+ // `--count-by` groups the matched set (#4360). It takes any query, and no
484
+ // query at all counts the whole graph: "which kinds does this estate hold".
485
+ let countBy;
486
+ if (args.countBy !== undefined) {
487
+ const parsed = parseCountBy(args.countBy);
488
+ if ("error" in parsed)
489
+ return { error: { code: "query-invalid", message: parsed.error, hint: parsed.hint } };
490
+ countBy = parsed;
491
+ }
492
+ if (!query && !countBy) {
493
+ return { error: { code: "query-invalid", message: "chant search needs a query: chant search \"<terms>\" [--live --env <name>]" } };
494
+ }
495
+ let terms;
496
+ try {
497
+ terms = parseQuery(query);
498
+ }
499
+ catch (err) {
500
+ if (err instanceof QueryError)
501
+ return { error: { code: "query-invalid", message: err.message, hint: err.hint } };
502
+ throw err;
503
+ }
504
+ // Query-scoped drift (#1268): --check-live checks a snapshot answer against
505
+ // a live read, --check-snapshot checks a live answer against the recorded
506
+ // snapshot — the direction each needs is the source it does NOT already have.
507
+ if (args.checkLive && !snapshot.ref) {
508
+ return {
509
+ error: {
510
+ code: "usage-invalid",
511
+ message: `chant search --check-live needs ${flag} <ref>`,
512
+ hint: `answer from a snapshot and check the matched rows against a live read: ${flag} latest --check-live --env <name>`,
513
+ },
514
+ };
515
+ }
516
+ if (args.checkSnapshot && !args.live) {
517
+ return {
518
+ error: {
519
+ code: "usage-invalid",
520
+ message: "chant search --check-snapshot needs --live",
521
+ hint: "answer live and check the matched rows against the recorded snapshot: --live --check-snapshot --env <name>",
522
+ },
523
+ };
524
+ }
525
+ if (args.failOnDrift && !args.checkLive && !args.checkSnapshot) {
526
+ return {
527
+ error: {
528
+ code: "usage-invalid",
529
+ message: "chant search --fail-on-drift needs --check-live or --check-snapshot",
530
+ hint: "it fails the check those flags run, so it is meaningless without one",
531
+ },
532
+ };
533
+ }
534
+ if ((args.live || snapshot.ref) && !args.env) {
535
+ const needs = snapshot.ref ? flag : "--live";
536
+ return { error: { code: "usage-invalid", message: `chant search ${needs} needs an environment: ${needs} --env <name>` } };
537
+ }
538
+ if (args.live && snapshot.ref) {
539
+ // Two different observations of the same estate, and no rule for which
540
+ // wins. Comparing them is a real question (#1268) but it is not this one.
541
+ return {
542
+ error: {
543
+ code: "usage-invalid",
544
+ message: `chant search takes --live or ${flag}, not both`,
545
+ hint: `--live reads the estate now; ${flag} answers from a recorded snapshot`,
546
+ },
547
+ };
548
+ }
549
+ return { terms, ...(countBy ? { countBy } : {}) };
550
+ }
551
+ /** Why a search over a project whose build root failed refuses (#4365), as the refusal prints it. */
552
+ const BUILD_ROOT_FAILED = {
553
+ code: "build-failed",
554
+ message: "chant search: a build root failed to render, so the graph it would answer over is incomplete",
555
+ hint: "fix the error above; `chant graph . --format ir` reports the same one",
556
+ };
557
+ /**
558
+ * The declared graph a search answers over (#4362, #4365, #4368): discovery,
559
+ * plus the build-root entities (a terraform root's blocks, a rendered
560
+ * kustomize root), plus the edges the lexicons' `graphEdges` hooks add. The
561
+ * same join and the same edges `chant graph --format ir` draws, through the
562
+ * same helpers (`./declared-graph.ts`), so a node's id here is its id there.
563
+ *
564
+ * Returns undefined, having printed the build root's own errors, when a build
565
+ * root failed to render or collided with a discovered name; the caller refuses
566
+ * with {@link BUILD_ROOT_FAILED}. A search over the rest would answer
567
+ * `(no matches)` for a root it never read, which is a wrong answer stated as
568
+ * a fact.
569
+ */
570
+ async function declaredGraph(ctx, projectPath, srcDir, loaded) {
571
+ const discovered = await discover(srcDir);
572
+ if (!(await mergeDeclaredBuildRoots(discovered.entities, await declaredBuildRootContributors(ctx, projectPath))))
573
+ return undefined;
574
+ let plugins = loaded ?? ctx.plugins;
575
+ if (plugins.length === 0) {
576
+ try {
577
+ plugins = await loadPlugins(await resolveProjectLexicons(projectPath));
578
+ }
579
+ catch {
580
+ plugins = [];
581
+ }
582
+ }
583
+ return withLexiconEdges(buildGraphIr(discovered.entities, projectPath), plugins, discovered.entities);
584
+ }
585
+ /**
586
+ * Print the capability catalog (#4375): the `capabilities` document under
587
+ * `--json`, a line per capability otherwise. It takes no query and no other
588
+ * search flag, since it answers nothing about the estate. Shared by `chant
589
+ * search` and `chant workspace search`, which differ only in the lexicons
590
+ * they report on.
591
+ */
592
+ export async function printCapabilities(args, query, lexicons) {
593
+ const { capabilitiesDocument, capabilitiesFailure, formatCapabilities } = await import("../../workspace/capabilities-contract.js");
594
+ const extra = [
595
+ // `args.path` is "." when no query was given.
596
+ ...(query && query !== "." ? [`the query "${query}"`] : []),
597
+ ...["live", "env", "snapshot", "at", "countBy", "explain", "show"]
598
+ .filter((k) => args[k] !== undefined && args[k] !== false)
599
+ .map((k) => `--${k.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)}`),
600
+ ];
601
+ if (extra.length > 0) {
602
+ const error = {
603
+ code: "usage-invalid",
604
+ message: `--capabilities prints the catalog, so it takes no query and no other search flag; it was given ${extra.join(", ")}`,
605
+ hint: "ask with a capability instead: chant search \"cap:internet-reachable(22)\"",
606
+ };
607
+ console.error(formatError(error));
608
+ if (args.json)
609
+ console.log(JSON.stringify(await capabilitiesFailure(error)));
610
+ return 1;
611
+ }
612
+ const doc = await capabilitiesDocument(lexicons);
613
+ if (args.json)
614
+ console.log(JSON.stringify(doc));
615
+ else
616
+ for (const line of formatCapabilities(doc))
617
+ console.log(line);
618
+ return 0;
619
+ }
620
+ /** The capabilities a query names, in query order, for the provenance line (#4375). */
621
+ function capabilitiesUsed(terms) {
622
+ const out = [];
623
+ const walk = (t) => {
624
+ if (t.negated)
625
+ return;
626
+ const call = t.kind === "cap" ? t.a : t.capability;
627
+ const name = call !== undefined ? parseCapabilityCall(call)?.name : undefined;
628
+ if (name && !out.includes(name))
629
+ out.push(name);
630
+ if (t.sub)
631
+ walk(t.sub);
632
+ };
633
+ terms.forEach(walk);
634
+ return out;
635
+ }
636
+ /**
637
+ * What each capability the query used could not evaluate (#4375), with a
638
+ * count where this graph holds the thing left out: an answer computed from
639
+ * declared references says what it skipped, so a caller knows the rows are a
640
+ * floor, not the whole of it.
641
+ */
642
+ function capabilityNotes(terms, ir, source) {
643
+ const names = capabilitiesUsed(terms);
644
+ if (names.length === 0)
645
+ return [];
646
+ const engine = capabilityEngine(ir);
647
+ const statements = [...engine.view.nodes.values()].filter((n) => n.label === "Statement" && n.attrs.effect === "Allow");
648
+ const counts = {
649
+ 'Resource "*"': statements.filter((n) => n.attrs.anyResource).length,
650
+ "IAM Condition keys": statements.filter((n) => n.attrs.condition).length,
651
+ "resources the graph holds but can't resolve, such as an expression naming nothing declared": statements.filter((n) => n.attrs.unresolved).length,
652
+ };
653
+ return names.map((name) => ({
654
+ name,
655
+ source: source.kind,
656
+ notEvaluated: (engine.rule(name)?.meta["not-evaluated"] ?? "")
657
+ .split(/;|\n/)
658
+ .map((x) => x.trim())
659
+ .filter(Boolean)
660
+ .map((x) => (counts[x] ? `${x} (${counts[x]} statement${counts[x] === 1 ? "" : "s"} here)` : x)),
661
+ }));
662
+ }
663
+ /**
664
+ * Answer capability terms over a graph that is not this project's own (#4375):
665
+ * `chant workspace search` composes its members' graphs and views and asks
666
+ * here, so the rules run once over the whole of it. Given packs, the view is
667
+ * built here; given a view, it is the one the members' own chants mapped
668
+ * (#4378). Returns the rows
669
+ * as `--json` carries them, or the refusal for a capability the query names
670
+ * wrongly.
671
+ */
672
+ export function answerOverGraph(ir, terms, query, packsOrView) {
673
+ // A workspace hands over the view its members' chants mapped (#4378).
674
+ const engine = Array.isArray(packsOrView) ? attachCapabilities(ir, packsOrView) : attachView(ir, packsOrView);
675
+ const refused = checkCapabilityTerms(engine, terms);
676
+ if (refused)
677
+ return { error: refused };
678
+ resolveKindTerms(terms, ir.nodes);
679
+ const byId = new Map(ir.nodes.map((n) => [n.id, n]));
680
+ const matches = ir.nodes.filter((n) => terms.every((t) => matchTerm(n, t, ir, byId)));
681
+ const doc = searchJson(query, { kind: "declared" }, matches, terms, ir, byId);
682
+ return { matches: doc.matches, capabilities: capabilityNotes(terms, ir, { kind: "declared" }) };
683
+ }
684
+ /** Whether a term, or the leaf under it, names a capability (#4375). */
685
+ export function usesCapability(t) {
686
+ return t.kind === "cap" || t.capability !== undefined || (t.sub !== undefined && usesCapability(t.sub));
687
+ }
688
+ /**
689
+ * Bind the graph to its lexicons' capability packs and check every capability
690
+ * the query names (#4375): an unknown name, a set used as an edge or the
691
+ * wrong arguments is a refusal, never an empty answer.
692
+ */
693
+ async function bindCapabilities(ctx, projectPath, ir, terms) {
694
+ let plugins = ctx.plugins;
695
+ if (plugins.length === 0) {
696
+ try {
697
+ plugins = await loadPlugins(await resolveProjectLexicons(projectPath));
698
+ }
699
+ catch {
700
+ plugins = [];
701
+ }
702
+ }
703
+ const engine = attachCapabilities(ir, plugins.map((p) => p.capabilityPack).filter((p) => !!p));
704
+ return checkCapabilityTerms(engine, terms);
705
+ }
706
+ /** Refuse a capability the query names that doesn't exist, a set used as an edge, or the wrong arguments. */
707
+ function checkCapabilityTerms(engine, terms) {
708
+ const known = [...new Set(engine.publicNames())].join(", ");
709
+ const check = (t) => {
710
+ if (t.sub) {
711
+ const inner = check(t.sub);
712
+ if (inner)
713
+ return inner;
714
+ }
715
+ const call = t.kind === "cap" ? t.a : t.capability;
716
+ if (call === undefined)
717
+ return undefined;
718
+ const parsed = parseCapabilityCall(call);
719
+ if (!engine.has(parsed.name)) {
720
+ return { message: `no capability "${parsed.name}"`, hint: `the capabilities are ${known}; chant search --capabilities describes each` };
721
+ }
722
+ const shape = engine.rule(parsed.name).vars.length === 1 ? "set" : "edge";
723
+ if (t.capability !== undefined && shape === "set") {
724
+ return { message: `"${parsed.name}" is a set, not an edge`, hint: `test it as a term: cap:${call}` };
725
+ }
726
+ try {
727
+ engine.bind(parsed.name, parsed.args);
728
+ }
729
+ catch (err) {
730
+ if (err instanceof CapabilityError)
731
+ return { message: `cap:${call}: ${err.message}`, hint: "chant search --capabilities lists each capability's parameters" };
732
+ throw err;
733
+ }
734
+ return undefined;
735
+ };
736
+ for (const t of terms) {
737
+ const r = check(t);
738
+ if (r)
739
+ return r;
740
+ }
741
+ return undefined;
742
+ }
743
+ /**
744
+ * The read a plan-depth root fell back to, from the lexicon's structured
745
+ * per-root report. Never from note text. "state" wins over "declared" when
746
+ * roots disagree, because some read still answered.
747
+ */
748
+ function planFallback(rootReads) {
749
+ const falls = Object.values(rootReads)
750
+ .filter((r) => (r.depth === "plan" || r.depth === "live+plan") && r.fellBackTo)
751
+ .map((r) => r.fellBackTo);
752
+ if (falls.includes("state"))
753
+ return "state";
754
+ if (falls.includes("live"))
755
+ return "live";
756
+ if (falls.includes("declared"))
757
+ return "declared";
758
+ return undefined;
759
+ }
760
+ /**
761
+ * Add the read depth to a live source (#4384). Only bound matches count: a
762
+ * row nobody read has no read to name. Nothing is added when no match was
763
+ * answered by the plan and the plan did not fall back.
764
+ */
765
+ function withReadDepth(source, matches, sources, rootReads) {
766
+ const reads = [...new Set(matches.filter((n) => n.physicalId).map((n) => sources[n.id]).filter((r) => !!r))].sort();
767
+ const fellBack = planFallback(rootReads);
768
+ const planned = reads.filter((r) => r === "plan" || r === "live+plan");
769
+ if (planned.length === 0 && !fellBack)
770
+ return source;
771
+ return {
772
+ ...source,
773
+ ...(planned.length > 0 ? { depth: reads.length === 1 ? reads[0] : "mixed" } : {}),
774
+ ...(reads.length > 0 ? { reads } : {}),
775
+ ...(fellBack ? { planFellBackTo: fellBack } : {}),
776
+ };
777
+ }
778
+ const READ_PHRASE = {
779
+ plan: "at the plan depth (terraform plan -lock=false)",
780
+ "live+plan": "live and at the plan depth (live-plan -json, terraform plan -lock=false)",
781
+ state: "from state",
782
+ live: "live",
783
+ };
386
784
  /**
387
785
  * Name the `--show` columns nothing carries (#1279).
388
786
  *
@@ -479,10 +877,25 @@ function ambientHint(matches, ambientKinds, asked, replay) {
479
877
  * already done. That is a fact about the query, printed for every query, and it
480
878
  * encodes no expected answer.
481
879
  */
482
- function provenance(matches, source, recorded, liveFailures = []) {
880
+ function provenanceLine(matches, source, recorded, liveFailures = [], derived = []) {
881
+ const line = provenanceBase(matches, source, recorded, liveFailures, derived);
882
+ // The plan read fell back (#4384): say to what, on whichever line this is.
883
+ return source.kind === "live" ? line + fellBackText(source) : line;
884
+ }
885
+ /** ` · the plan read fell back to state`, or nothing. */
886
+ function fellBackText(source) {
887
+ if (!source.planFellBackTo)
888
+ return "";
889
+ // A live root has no state read to fall to: live-plan answered alone.
890
+ const to = source.planFellBackTo === "live" ? "live-plan alone" : source.planFellBackTo;
891
+ return ` · the plan read fell back to ${to}`;
892
+ }
893
+ function provenanceBase(matches, source, recorded, liveFailures = [], derived = []) {
483
894
  if (source.kind === "declared") {
484
- console.log("— declared only · no observation · physical ids unavailable");
485
- return;
895
+ // A derived fact the query relied on (#4362) was folded from declared
896
+ // references alone, so it describes the source, not the account.
897
+ const from = derived.length > 0 ? ` · ${derived.join(", ")} derived from declared references` : "";
898
+ return `— declared only · no observation · physical ids unavailable${from}`;
486
899
  }
487
900
  const bound = matches.filter((n) => n.physicalId).length;
488
901
  const what = source.kind === "live" ? "live read" : "snapshot";
@@ -494,10 +907,15 @@ function provenance(matches, source, recorded, liveFailures = []) {
494
907
  const rest = recorded
495
908
  ? "a snapshot of this environment is recorded — answer from it with --at latest"
496
909
  : "answered from the declared graph · physical ids unavailable";
497
- console.log(`— live read failed (${lexicons}) · ${unobserved}/${matches.length} rows unobserved · ${rest}`);
498
- return;
910
+ return `— live read failed (${lexicons}) · ${unobserved}/${matches.length} rows unobserved · ${rest}`;
499
911
  }
500
912
  if (bound === 0) {
913
+ // Every match a planned create (#4372): absent on purpose, so "returned no
914
+ // bound resources" reads as a failed read when the read worked.
915
+ const planned = plannedCreateCount(matches);
916
+ if (planned > 0) {
917
+ return `— ${what}: ${planned} planned ${planned === 1 ? "create" : "creates"}, none bound yet · the plan has not been applied`;
918
+ }
501
919
  // The estate was asked for and nothing came back bound. Naming it is the
502
920
  // difference between "these do not exist" and "nobody could see them".
503
921
  // A snapshot sitting unused is the actionable half of this. Denied network,
@@ -505,21 +923,37 @@ function provenance(matches, source, recorded, liveFailures = []) {
505
923
  // retrying `--live` — the tool knew the estate was unreachable AND that a
506
924
  // recording of it was on disk, and said only the first half.
507
925
  if (recorded) {
508
- console.log(`— ${what} returned no bound resources · a snapshot of this environment is recorded — answer from it with --at latest`);
509
- return;
926
+ return (`— ${what} returned no bound resources · a snapshot of this environment is recorded — answer from it with --at latest`);
510
927
  }
511
- console.log(`— ${what} returned no bound resources · answered from the declared graph · physical ids unavailable`);
512
- return;
928
+ return (`— ${what} returned no bound resources · answered from the declared graph · physical ids unavailable`);
513
929
  }
514
930
  if (source.kind === "live") {
515
- console.log(`— observed live · bound ${bound}/${matches.length}`);
516
- return;
931
+ // The plan answered some or all of it (#4384): say so, and name each read.
932
+ if (source.depth && source.reads) {
933
+ return `— observed ${source.reads.map((r) => READ_PHRASE[r] ?? r).join(" and ")} · bound ${bound}/${matches.length}`;
934
+ }
935
+ return `— observed live · bound ${bound}/${matches.length}`;
517
936
  }
518
937
  // Time is the whole risk of a recorded answer, so it leads. A caller can see
519
938
  // how old this is and decide, rather than discovering staleness later.
520
939
  const taken = source.timestamp ? ` taken ${source.timestamp}` : "";
521
940
  const at = source.commit ? ` ${source.commit.slice(0, 7)}` : "";
522
- console.log(`— observed from snapshot${at}${taken} · bound ${bound}/${matches.length}`);
941
+ return `— observed from snapshot${at}${taken} · bound ${bound}/${matches.length}`;
942
+ }
943
+ /**
944
+ * How many matches there are when every one is a planned create (#4372): an
945
+ * absent entity (no physical id) carrying `planAction=create`. Zero for no
946
+ * matches or any other mix, so a real empty read keeps its wording.
947
+ */
948
+ function plannedCreateCount(matches) {
949
+ if (matches.length === 0)
950
+ return 0;
951
+ const all = matches.every((n) => !n.physicalId && n.attrs?.planAction === "create");
952
+ return all ? matches.length : 0;
953
+ }
954
+ /** Print {@link provenanceLine}: the text footer. */
955
+ function provenance(matches, source, recorded, liveFailures = [], derived = []) {
956
+ console.log(provenanceLine(matches, source, recorded, liveFailures, derived));
523
957
  }
524
958
  /** Union `LiveObservation.resources` across lexicons into one name-keyed map. */
525
959
  function mergeResources(observations) {
@@ -564,12 +998,36 @@ function renderDrift(diff, checked, direction, depth) {
564
998
  console.log(` · ${r.name} (${r.type}) — runtime, owned by ${r.owner}`);
565
999
  for (const u of diff.unobserved)
566
1000
  console.log(` ? ${formatUnobserved(u.name, u)}`);
567
- const driftCount = diff.missing.length + diff.orphan.length + diff.disappeared.length + diff.driftedSinceSnapshot.length;
1001
+ const driftCount = driftCountOf(diff);
568
1002
  const label = direction === "live" ? "a live read" : "the recorded snapshot";
569
1003
  const depthNote = depth === "deep" ? " · snapshot recorded at deep depth, compared here at identity" : "";
570
1004
  console.log(`— checked against ${label} · ${driftCount > 0 ? `${driftCount} of ${checked} matched drifted` : `no drift across ${checked} matched`}${depthNote}`);
571
1005
  return driftCount;
572
1006
  }
1007
+ /** What counts as drift: the set `lifecycle diff` totals (#1268). */
1008
+ function driftCountOf(diff) {
1009
+ return diff.missing.length + diff.orphan.length + diff.disappeared.length + diff.driftedSinceSnapshot.length;
1010
+ }
1011
+ function searchDriftJson(diff, checked, against, depth) {
1012
+ return {
1013
+ against,
1014
+ checked,
1015
+ count: driftCountOf(diff),
1016
+ missing: diff.missing,
1017
+ orphan: diff.orphan,
1018
+ disappeared: diff.disappeared,
1019
+ drifted: diff.driftedSinceSnapshot.flatMap((d) => d.changes.map((c) => ({ id: d.name, path: c.path, old: c.oldValue, new: c.newValue }))),
1020
+ newlyObserved: diff.newlyObserved,
1021
+ runtimeChildren: diff.runtimeChildren.map((r) => ({ id: r.name, type: r.type, owner: r.owner })),
1022
+ unobserved: diff.unobserved.map((u) => ({
1023
+ id: u.name,
1024
+ ...(u.type ? { type: u.type } : {}),
1025
+ reason: u.reason,
1026
+ ...(u.detail ? { detail: u.detail } : {}),
1027
+ })),
1028
+ ...(depth ? { depth } : {}),
1029
+ };
1030
+ }
573
1031
  function formatValue(v) {
574
1032
  if (v === undefined)
575
1033
  return "<unset>";
@@ -599,19 +1057,31 @@ function derivedSurface(terms, matches, ir, backed = true) {
599
1057
  return;
600
1058
  const used = new Set(terms.filter((t) => t.kind === "attr").map((t) => t.a));
601
1059
  const unused = new Set();
602
- for (const n of matches) {
603
- for (const [kind, names] of Object.entries(derived)) {
604
- if (!n.kind?.includes(kind))
605
- continue;
606
- for (const name of names)
607
- if (!used.has(name))
608
- unused.add(name);
609
- }
610
- }
1060
+ for (const n of matches)
1061
+ for (const name of derivedNames(n, ir))
1062
+ if (!used.has(name))
1063
+ unused.add(name);
611
1064
  if (unused.size === 0)
612
1065
  return;
613
1066
  console.log(`— also derived for these resources: ${[...unused].sort().join(", ")}`);
614
1067
  }
1068
+ /** The derived attributes recorded for a node's kind (or its terraform resource type, #4362). */
1069
+ function derivedNames(n, ir) {
1070
+ const tfType = terraformResourceType(n);
1071
+ return Object.entries(ir.derivedAttrs ?? {})
1072
+ .filter(([kind]) => n.kind?.includes(kind) || kind === tfType)
1073
+ .flatMap(([, names]) => names);
1074
+ }
1075
+ /** The derived attributes the query's `attr:` terms relied on, for the provenance line. */
1076
+ function derivedUsed(terms, matches, ir) {
1077
+ const queried = new Set(terms.filter((t) => t.kind === "attr" && !t.negated).map((t) => t.a));
1078
+ const out = new Set();
1079
+ for (const n of matches)
1080
+ for (const name of derivedNames(n, ir))
1081
+ if (queried.has(name))
1082
+ out.add(name);
1083
+ return [...out].sort();
1084
+ }
615
1085
  /**
616
1086
  * `--explain` footer (#1139): a compact, model-DERIVED summary that gives a
617
1087
  * small model a reason to trust the result instead of re-deriving it with a
@@ -622,33 +1092,70 @@ function derivedSurface(terms, matches, ir, backed = true) {
622
1092
  * any expected answer, so it stays a fair, question-agnostic capability.
623
1093
  */
624
1094
  function explain(terms, matches, ir, byId, query) {
625
- const kinds = new Set(matches.map((n) => n.kind).filter((k) => !!k));
626
- const universe = kinds.size > 0 ? ir.nodes.filter((n) => n.kind && kinds.has(n.kind)) : ir.nodes;
627
- const matched = new Set(matches.map((n) => n.id));
628
- const excluded = universe.filter((n) => !matched.has(n.id));
629
- const kindLabel = kinds.size > 0 ? [...kinds].join("/") : "nodes";
630
- console.log(`— ${matches.length} of ${universe.length} ${kindLabel} matched (query: ${query})`);
1095
+ const data = explainData(terms, matches, ir, byId);
1096
+ const kindLabel = data.universe.kinds.length > 0 ? data.universe.kinds.join("/") : "nodes";
1097
+ console.log(`— ${data.universe.matched} of ${data.universe.of} ${kindLabel} matched (query: ${query})`);
631
1098
  // Inclusion evidence: for a derived fact a CLI can't easily re-verify
632
1099
  // (internetFacing, resolved across the default VPC's routing), name WHY each
633
1100
  // match qualifies, so the agent trusts the result instead of dropping it.
1101
+ for (const e of data.evidence)
1102
+ console.log(` ✓ ${bareId(e.id)} ${e.attr} via ${e.via}`);
1103
+ // The chain each match was reached by (#4359), so a caller can quote the
1104
+ // path instead of re-deriving it with one search per hop.
1105
+ for (const n of matches) {
1106
+ for (const p of matchPaths(n, terms, ir, byId)) {
1107
+ // A capability's path (#4375) turns as the rule does, so each hop says which way it went.
1108
+ if (p.term.kind === "cap" || p.term.capability !== undefined) {
1109
+ // A set a node is in by its own attributes (stores-data) has no path to print.
1110
+ if (p.hops.length === 0 && !p.because)
1111
+ continue;
1112
+ console.log(` path ${bareId(n.id)} ${termText({ ...p.term, negated: false })}: ${formatHops(p.hops, n.id) || "(the node itself)"}`);
1113
+ for (const b of p.because ?? []) {
1114
+ console.log(` ${b.capability} ${bareId(b.id)}: ${formatHops(b.hops, b.id) || "(an attribute of the node)"}`);
1115
+ }
1116
+ }
1117
+ else
1118
+ console.log(` path ${formatPath(p.hops, p.term.dir)}`);
1119
+ }
1120
+ }
1121
+ for (const m of data.nearMisses) {
1122
+ console.log(` · excluded ${bareId(m.id)} — fails ${m.term ? describeTerm(m.term) : "(query)"}`);
1123
+ }
1124
+ if (data.moreExcluded > 0)
1125
+ console.log(` · …and ${data.moreExcluded} more excluded`);
1126
+ }
1127
+ /** How many near-misses `--explain` names before summarising the rest. */
1128
+ const NEAR_MISS_LIMIT = 8;
1129
+ /**
1130
+ * The facts behind the `--explain` footer, shared by the text and `--json`
1131
+ * (#4367). The universe is every node of the matched kinds (every node when
1132
+ * nothing matched); a near-miss is a node of that universe the query
1133
+ * excluded, with the first term it fails.
1134
+ */
1135
+ function explainData(terms, matches, ir, byId) {
1136
+ // Counted against a terraform block's type, not `Terraform::Resource` (#4366):
1137
+ // "2 of 2 aws_instance", not "2 of 185 Terraform::Resource".
1138
+ const kinds = new Set(matches.map(displayKind).filter((k) => !!k));
1139
+ const universe = kinds.size > 0 ? ir.nodes.filter((n) => kinds.has(displayKind(n) ?? "")) : ir.nodes;
1140
+ const matched = new Set(matches.map((n) => n.id));
1141
+ const excluded = universe.filter((n) => !matched.has(n.id));
1142
+ const evidence = [];
634
1143
  for (const t of terms) {
635
1144
  if (t.kind !== "attr")
636
1145
  continue;
637
1146
  for (const n of matches) {
638
1147
  const via = n.attrs?.[`${t.a}Via`];
639
- const id = n.id.includes("::") ? n.id.slice(n.id.lastIndexOf("::") + 2) : n.id;
640
1148
  if (typeof via === "string")
641
- console.log(` ✓ ${id} ${t.a} via ${via}`);
1149
+ evidence.push({ id: n.id, attr: t.a, via });
642
1150
  }
643
1151
  }
644
- const shown = excluded.slice(0, 8);
645
- for (const n of shown) {
646
- const failing = terms.find((t) => !matchTerm(n, t, ir, byId));
647
- const id = n.id.includes("::") ? n.id.slice(n.id.lastIndexOf("::") + 2) : n.id;
648
- console.log(` · excluded ${id} — fails ${failing ? describeTerm(failing) : "(query)"}`);
649
- }
650
- if (excluded.length > shown.length)
651
- console.log(` · …and ${excluded.length - shown.length} more excluded`);
1152
+ const shown = excluded.slice(0, NEAR_MISS_LIMIT);
1153
+ return {
1154
+ universe: { matched: matches.length, of: universe.length, kinds: [...kinds] },
1155
+ evidence,
1156
+ nearMisses: shown.map((n) => ({ id: n.id, term: terms.find((t) => !matchTerm(n, t, ir, byId)) })),
1157
+ moreExcluded: excluded.length - shown.length,
1158
+ };
652
1159
  }
653
1160
  /**
654
1161
  * On a miss, name the attributes the queried kind actually carries. A graph knows
@@ -662,7 +1169,7 @@ function availableAttrs(terms, ir) {
662
1169
  const kindTerm = terms.find((t) => t.kind === "kind");
663
1170
  if (!kindTerm)
664
1171
  return;
665
- const of = ir.nodes.filter((n) => n.kind?.includes(kindTerm.a));
1172
+ const of = ir.nodes.filter((n) => matchTerm(n, { ...kindTerm, negated: false }));
666
1173
  if (of.length === 0)
667
1174
  return;
668
1175
  const names = new Set();
@@ -703,11 +1210,86 @@ function describeTerm(t) {
703
1210
  // the opposite of what it is.
704
1211
  if (t.negated)
705
1212
  return `!${describeTerm({ ...t, negated: false })}`;
706
- const leaf = (x) => x.kind === "kind" ? `kind:${x.a}` : x.kind === "attr" ? `attr:${x.a}${x.b !== undefined ? "=" + x.b : ""}`
1213
+ if (t.kind === "edge")
1214
+ return `${edgeOperator(t, { out: "→", in: "←" })}${t.sub ? leafText(t.sub) : ""} (no such edge)`;
1215
+ return leafText(t);
1216
+ }
1217
+ function leafText(x) {
1218
+ if (x.kind === "cap")
1219
+ return `cap:${x.a}`;
1220
+ return x.kind === "kind" ? `kind:${x.a}` : x.kind === "attr" ? `attr:${x.a}${x.b !== undefined ? "=" + x.b : ""}`
707
1221
  : x.kind === "tag" ? `tag:${x.a}${x.b !== undefined ? "=" + x.b : ""}` : `"${x.a}"`;
708
- if (t.kind === "edge" && t.sub)
709
- return `${t.dir === "out" ? "→" : "←"}${leaf(t.sub)} (no such edge)`;
710
- return leaf(t);
1222
+ }
1223
+ function searchErrorJson(query, error) {
1224
+ return { ...searchHead(), query, error: { code: error.code, message: error.message, ...(error.hint ? { hint: error.hint } : {}) } };
1225
+ }
1226
+ /** The head of every `search` document (#4374): the contract, its schema, and the chant that wrote it. */
1227
+ function searchHead() {
1228
+ return { $schema: SEARCH_OUTPUT_SCHEMA_ID, contract: SEARCH_CONTRACT_VERSION, chant: readChantPackageVersion() };
1229
+ }
1230
+ /**
1231
+ * The workspace a project belongs to, when it belongs to one (#4374, ws-016):
1232
+ * its declared name and its root relative to the git root, with `at: null`,
1233
+ * since plain search reads the working tree. Outside a workspace, or when its
1234
+ * declaration can't be read, only `at`.
1235
+ */
1236
+ async function workspaceIdentity(cwd) {
1237
+ try {
1238
+ const [{ locateWorkspace }, { readDeclaration }] = await Promise.all([import("../../workspace/which-chant.js"), import("../../workspace/declaration.js")]);
1239
+ const located = locateWorkspace(cwd);
1240
+ return { workspace: { name: readDeclaration(located.tree, "").name, root: located.root }, at: null };
1241
+ }
1242
+ catch {
1243
+ return { at: null };
1244
+ }
1245
+ }
1246
+ function searchJson(query, source, matches, terms, ir, byId, liveFailures = [], extra = {}) {
1247
+ const show = extra.show ?? [];
1248
+ return {
1249
+ query,
1250
+ source: extra.env ? { ...source, env: extra.env } : source,
1251
+ matches: matches.map((n) => {
1252
+ const attrs = (n.attrs ?? {});
1253
+ const physical = n.physicalId ?? attrs["physicalId"] ?? attrs["InstanceId"] ?? attrs["Id"];
1254
+ return {
1255
+ id: n.id,
1256
+ displayId: bareId(n.id),
1257
+ ...(n.kind ? { kind: n.kind } : {}),
1258
+ ...(physical != null && typeof physical !== "object" ? { physicalId: String(physical) } : {}),
1259
+ ...(typeof attrs["_unobserved"] === "string" ? { unobserved: attrs["_unobserved"] } : {}),
1260
+ ...(show.length > 0 ? { show: Object.fromEntries(showValues(attrs, show)) } : {}),
1261
+ paths: matchPaths(n, terms, ir, byId).map((p) => ({
1262
+ term: termText({ ...p.term, negated: false }),
1263
+ dir: p.term.dir ?? "out",
1264
+ hops: p.hops,
1265
+ ...(p.because ? { because: p.because } : {}),
1266
+ })),
1267
+ };
1268
+ }),
1269
+ ...(extra.provenance !== undefined ? { provenance: extra.provenance } : {}),
1270
+ ...(extra.plannedCreates ? { plannedCreates: extra.plannedCreates } : {}),
1271
+ ...(extra.capabilities ? { capabilities: extra.capabilities } : {}),
1272
+ ...(liveFailures.length > 0 ? { liveFailures } : {}),
1273
+ ...(extra.explain
1274
+ ? {
1275
+ explain: {
1276
+ universe: extra.explain.universe,
1277
+ evidence: extra.explain.evidence,
1278
+ nearMisses: extra.explain.nearMisses.map((m) => ({ id: m.id, ...(m.term ? { fails: termText(m.term) } : {}) })),
1279
+ moreExcluded: extra.explain.moreExcluded,
1280
+ },
1281
+ }
1282
+ : {}),
1283
+ ...(extra.drift ? { drift: extra.drift } : {}),
1284
+ };
1285
+ }
1286
+ /** A term as the query grammar spells it, `!` and ASCII arrows included (#4367). */
1287
+ function termText(t) {
1288
+ if (t.negated)
1289
+ return `!${termText({ ...t, negated: false })}`;
1290
+ if (t.kind === "edge")
1291
+ return `${edgeOperator(t, { out: "->", in: "<-" })}${t.sub ? leafText(t.sub) : ""}`;
1292
+ return leafText(t);
711
1293
  }
712
1294
  /**
713
1295
  * Decide which kinds a `kind:` term means, given what is in the graph.
@@ -730,14 +1312,28 @@ function describeTerm(t) {
730
1312
  * Substring is kept as the fallback, so `kind:Gateway` still finds both
731
1313
  * `InternetGateway` and `VPCGatewayAttachment` — a genuine substring search
732
1314
  * that no segment rule would serve. Most specific wins; nothing else changes.
1315
+ *
1316
+ * A terraform resource is `Terraform::Resource` whatever it is, so the term is
1317
+ * also read against each resource's type (#4366), with the same boundary rule
1318
+ * on `_`: `kind:aws_iam_role` means that type exactly and never
1319
+ * `aws_iam_role_policy`, and a term ending in `_` (`kind:aws_iam_`) or a bare
1320
+ * provider name (`kind:aws`) means every type it begins. A data source's type
1321
+ * keeps its `data.` prefix (`kind:data.aws_ami`), the same type `--count-by kind`
1322
+ * groups it under, so `kind:aws_ami` is not a data source. A term that names a
1323
+ * type turns off the substring fallback for the IR kind, so `kind:aws_instance`
1324
+ * is the instances and nothing found by accident.
733
1325
  */
734
1326
  export function resolveKindTerms(terms, nodes) {
735
1327
  const present = [...new Set(nodes.map((n) => n.kind).filter((k) => !!k))];
1328
+ const tfPresent = [...new Set(nodes.map(terraformType).filter((k) => !!k))];
736
1329
  const walk = (t) => {
737
1330
  if (t.sub)
738
1331
  walk(t.sub);
739
1332
  if (t.kind !== "kind")
740
1333
  return;
1334
+ const types = resolveTerraformTypes(t.a, tfPresent);
1335
+ if (types.length > 0)
1336
+ t.tfTypes = new Set(types);
741
1337
  const want = t.a.toLowerCase().split("::").filter(Boolean);
742
1338
  if (want.length === 0)
743
1339
  return;
@@ -754,7 +1350,29 @@ export function resolveKindTerms(terms, nodes) {
754
1350
  };
755
1351
  terms.forEach(walk);
756
1352
  }
1353
+ /** The terraform resource types a `kind:` term names, by the rule in {@link resolveKindTerms}. */
1354
+ function resolveTerraformTypes(term, present) {
1355
+ const want = term.toLowerCase();
1356
+ if (want.length === 0 || want.includes("::"))
1357
+ return [];
1358
+ const exact = present.filter((p) => p === want);
1359
+ if (exact.length > 0)
1360
+ return exact;
1361
+ const prefix = want.endsWith("_") || want.endsWith(".") ? want : /[_.]/.test(want) ? undefined : `${want}_`;
1362
+ return prefix ? present.filter((p) => p.startsWith(prefix)) : [];
1363
+ }
1364
+ /** What a row and `--explain` call a node's kind: a terraform block's type (#4366), else its IR kind. Same as `--count-by kind`. */
1365
+ function displayKind(n) {
1366
+ return terraformType(n) ?? n.kind;
1367
+ }
757
1368
  function parseLeaf(tok) {
1369
+ const cap = /^cap:(.*)$/i.exec(tok);
1370
+ if (cap) {
1371
+ if (!parseCapabilityCall(cap[1])) {
1372
+ throw new QueryError(`"${tok}" is not a capability term`, "name a capability, with arguments in parentheses when it takes any: cap:internet-reachable, cap:open-ingress(22); chant search --capabilities lists them");
1373
+ }
1374
+ return { kind: "cap", a: cap[1] };
1375
+ }
758
1376
  const m = /^(kind|tag|attr):(.*)$/i.exec(tok);
759
1377
  if (m) {
760
1378
  const key = m[1].toLowerCase();
@@ -789,6 +1407,69 @@ class QueryError extends Error {
789
1407
  this.hint = hint;
790
1408
  }
791
1409
  }
1410
+ /**
1411
+ * Parse an edge term (#4359): `->` or `<-`, then an optional depth, then an
1412
+ * optional attribute qualifier, then the target leaf.
1413
+ *
1414
+ * ->kind:X one hop, as it always was
1415
+ * ->*kind:X any number of hops, one or more
1416
+ * ->{3}kind:X exactly three hops
1417
+ * ->{1,3}kind:X one to three hops ({2,} is two or more)
1418
+ * <-[via=vpc_id]kind:X only edges through the consumer attribute vpc_id
1419
+ * ->[to=id]kind:X only edges that read the producer attribute id
1420
+ * <-*[via=subnet_id] a qualifier may stand without a target: any node
1421
+ *
1422
+ * A qualifier constrains every edge on the path, not only the first one.
1423
+ */
1424
+ function parseEdge(tok, negated) {
1425
+ const dir = tok.startsWith("->") ? "out" : "in";
1426
+ const arrow = tok.slice(0, 2);
1427
+ let rest = tok.slice(2);
1428
+ const term = { kind: "edge", a: "", dir };
1429
+ if (rest.startsWith("*")) {
1430
+ term.depth = { min: 1, max: Infinity };
1431
+ rest = rest.slice(1);
1432
+ }
1433
+ else if (rest.startsWith("{")) {
1434
+ const m = /^\{(\d+)(,(\d*))?\}/.exec(rest);
1435
+ const min = m ? Number(m[1]) : NaN;
1436
+ const max = !m ? NaN : m[2] === undefined ? min : m[3] === "" ? Infinity : Number(m[3]);
1437
+ if (!m || min < 1 || max < min) {
1438
+ throw new QueryError(`"${tok}" has a depth this grammar cannot read`, `a depth is {n}, {min,max} or {min,} with 1 <= min <= max, e.g. ${arrow}{1,3}kind:aws_vpc; ${arrow}* is any depth`);
1439
+ }
1440
+ term.depth = { min, max };
1441
+ rest = rest.slice(m[0].length);
1442
+ }
1443
+ if (rest.startsWith("[")) {
1444
+ const close = rest.indexOf("]");
1445
+ // Split on commas outside parentheses, so `[cap=open-ingress(22,tcp)]` is one part.
1446
+ const parts = close < 0 ? [""] : (rest.slice(1, close).match(/(?:[^,(]|\([^)]*\))+/g) ?? [""]).map((p) => p.trim());
1447
+ for (const part of parts) {
1448
+ const c = /^cap=(.+)$/.exec(part);
1449
+ if (c && parseCapabilityCall(c[1])) {
1450
+ term.capability = c[1];
1451
+ continue;
1452
+ }
1453
+ const q = /^(via|to)=(.+)$/.exec(part);
1454
+ if (!q) {
1455
+ throw new QueryError(`"${tok}" has an edge qualifier this grammar cannot read`, `qualify an edge by attribute with [via=<consumer attr>] or [to=<producer attr>], e.g. <-[via=vpc_id]kind:aws_subnet or ->[via=subnet_id,to=id], or walk a capability with [cap=<name>], e.g. ->[cap=reaches]cap:stores-data`);
1456
+ }
1457
+ if (q[1] === "via")
1458
+ term.viaAttr = q[2];
1459
+ else
1460
+ term.toAttr = q[2];
1461
+ }
1462
+ rest = rest.slice(close + 1);
1463
+ }
1464
+ if (rest.trim() === "") {
1465
+ if (term.viaAttr === undefined && term.toAttr === undefined && term.capability === undefined) {
1466
+ const bang = negated ? "!" : "";
1467
+ throw new QueryError(`"${bang}${tok}" needs a target`, `say what the edge reaches: ${bang}${tok}kind:EC2::Instance, or qualify it by attribute: ${bang}${tok}[via=vpc_id]`);
1468
+ }
1469
+ return term;
1470
+ }
1471
+ return { ...term, sub: parseLeaf(rest) };
1472
+ }
792
1473
  function parseQuery(query) {
793
1474
  // Split on whitespace but keep quoted phrases together.
794
1475
  const tokens = query.match(/"[^"]*"|\S+/g) ?? [];
@@ -827,11 +1508,7 @@ function parseQuery(query) {
827
1508
  if (/^(->|<-)\s*$/.test(tok)) {
828
1509
  throw new QueryError(`"${negated ? "!" : ""}${tok}" needs a target`, `say what the edge reaches: ${negated ? "!" : ""}${tok}kind:EC2::Instance, or ${negated ? "!" : ""}${tok}attr:Name=web`);
829
1510
  }
830
- const term = tok.startsWith("->")
831
- ? { kind: "edge", a: "", dir: "out", sub: parseLeaf(tok.slice(2)) }
832
- : tok.startsWith("<-")
833
- ? { kind: "edge", a: "", dir: "in", sub: parseLeaf(tok.slice(2)) }
834
- : parseLeaf(tok);
1511
+ const term = tok.startsWith("->") || tok.startsWith("<-") ? parseEdge(tok, negated) : parseLeaf(tok);
835
1512
  return negated ? { ...term, negated: true } : term;
836
1513
  });
837
1514
  }
@@ -847,12 +1524,30 @@ function attrString(v) {
847
1524
  }
848
1525
  return String(v);
849
1526
  }
1527
+ /**
1528
+ * Attributes a bare word does not search, by lexicon (#4368). A terraform
1529
+ * node's `source` is the whole `.tf` file its block came from (the lint
1530
+ * checks need the raw text), so a bare `aws_security_group` matched every
1531
+ * node declared in a file that mentions one: 21 nodes on behold's example
1532
+ * estate, where three are security groups. The block itself is still
1533
+ * searched, through `address` and `body`, and `attr:source=<text>` still
1534
+ * reads the file when that is what the caller means.
1535
+ */
1536
+ const BARE_WORD_SKIPS = {
1537
+ terraform: new Set(["source"]),
1538
+ };
850
1539
  function matchTerm(n, t, ir, byId) {
851
1540
  if (t.negated)
852
1541
  return !matchTerm(n, { ...t, negated: false }, ir, byId);
853
1542
  const attrs = n.attrs ?? {};
854
1543
  if (t.kind === "edge") {
855
- if (!ir || !byId || !t.sub)
1544
+ if (!ir || !byId)
1545
+ return false;
1546
+ // Depth and attribute qualifiers (#4359) go through the path search. A
1547
+ // plain one-hop term keeps the original code below, unchanged.
1548
+ if (t.depth || t.viaAttr !== undefined || t.toAttr !== undefined || t.capability !== undefined)
1549
+ return findPath(n, t, ir, byId) !== null;
1550
+ if (!t.sub)
856
1551
  return false;
857
1552
  // A node matches if it has an edge (out or in) to a node satisfying `sub`.
858
1553
  const edges = ir.edges ?? [];
@@ -862,11 +1557,18 @@ function matchTerm(n, t, ir, byId) {
862
1557
  .filter((x) => !!x);
863
1558
  return neighbors.some((m) => matchTerm(m, t.sub, ir, byId));
864
1559
  }
1560
+ if (t.kind === "cap")
1561
+ return !!ir && capabilityEvidence(n, t, ir) !== undefined;
865
1562
  if (t.kind === "kind") {
866
1563
  // Resolved kinds when the term lined up with `::` boundaries; the
867
- // documented substring otherwise.
1564
+ // documented substring otherwise. A term that named a terraform type
1565
+ // (#4366) matches resources of that type as well, and drops the substring.
1566
+ if (t.tfTypes?.has(terraformType(n) ?? ""))
1567
+ return true;
868
1568
  if (t.kinds)
869
1569
  return t.kinds.has(n.kind ?? "");
1570
+ if (t.tfTypes)
1571
+ return false;
870
1572
  return (n.kind ?? "").toLowerCase().includes(t.a.toLowerCase());
871
1573
  }
872
1574
  if (t.kind === "attr") {
@@ -883,15 +1585,186 @@ function matchTerm(n, t, ir, byId) {
883
1585
  return key.toLowerCase() === t.a.toLowerCase() && (t.b === undefined || val.toLowerCase().includes(t.b.toLowerCase()));
884
1586
  });
885
1587
  }
886
- // bare word: substring over id, kind, and all attr values
887
- const hay = [n.id, n.kind, ...Object.values(attrs).map(attrString)].join(" ").toLowerCase();
1588
+ // bare word: substring over id, kind, and all attr values, less the ones
1589
+ // that hold text about other nodes (see BARE_WORD_SKIPS).
1590
+ const skip = BARE_WORD_SKIPS[n.lexicon];
1591
+ const values = Object.entries(attrs)
1592
+ .filter(([k]) => !skip?.has(k))
1593
+ .map(([, v]) => attrString(v));
1594
+ const hay = [n.id, n.kind, ...values].join(" ").toLowerCase();
888
1595
  return hay.includes(t.a.toLowerCase());
889
1596
  }
1597
+ const adjacency = new WeakMap();
1598
+ function edgeIndex(ir) {
1599
+ let idx = adjacency.get(ir);
1600
+ if (!idx) {
1601
+ idx = adjacencyOf(ir.edges ?? []);
1602
+ adjacency.set(ir, idx);
1603
+ }
1604
+ return idx;
1605
+ }
1606
+ function sameName(a, b) {
1607
+ return a !== undefined && a.toLowerCase() === b.toLowerCase();
1608
+ }
1609
+ /**
1610
+ * The shortest path by which an edge term reaches `n` (#4359), or null.
1611
+ *
1612
+ * Breadth-first over the edges in the term's direction, keeping only edges
1613
+ * whose attributes satisfy its qualifier, and accepting a node that satisfies
1614
+ * the target leaf (any node when the term has none) at a depth inside the
1615
+ * term's range. A plain `->`/`<-` is the range 1..1.
1616
+ *
1617
+ * The walk is `walkBreadthFirst` (graph-walk.ts), shared with carve's
1618
+ * reachability (#2325); it is what terminates on a cycle.
1619
+ */
1620
+ function findPath(n, t, ir, byId) {
1621
+ if (t.kind !== "edge")
1622
+ return null;
1623
+ const { min, max } = t.depth ?? { min: 1, max: 1 };
1624
+ const idx = edgeIndex(ir);
1625
+ const out = t.dir === "out";
1626
+ // A capability edge (#4375) stands for the hops that proved it.
1627
+ const steps = (id) => {
1628
+ if (t.capability !== undefined)
1629
+ return capabilitySteps(id, t.capability, out, ir);
1630
+ return (((out ? idx.out.get(id) : idx.in.get(id)) ?? [])
1631
+ .filter((e) => (t.viaAttr === undefined || sameName(e.viaAttr, t.viaAttr)) && (t.toAttr === undefined || sameName(e.toAttr, t.toAttr)))
1632
+ .map((e) => ({
1633
+ far: out ? e.to : e.from,
1634
+ hops: [{ from: e.from, ...(e.viaAttr ? { fromAttr: e.viaAttr } : {}), to: e.to, ...(e.toAttr ? { toAttr: e.toAttr } : {}) }],
1635
+ })));
1636
+ };
1637
+ // A node the graph does not hold is no step at all, and the match is the
1638
+ // first arrival in range that satisfies the target leaf (#4359).
1639
+ const found = walkBreadthFirst(n.id, {
1640
+ min,
1641
+ max,
1642
+ steps: (id) => steps(id).filter((step) => byId.has(step.far)),
1643
+ arrive: ({ id }) => !t.sub || matchTerm(byId.get(id), t.sub, ir, byId),
1644
+ });
1645
+ return found ? found.hops : null;
1646
+ }
1647
+ function bareId(id) {
1648
+ return id.includes("::") ? id.slice(id.lastIndexOf("::") + 2) : id;
1649
+ }
1650
+ /**
1651
+ * A path as one line of `id.attr` steps, read from the match outward (#4359).
1652
+ * Walking `->`, each hop reads `consumer.via -> producer.attr`; walking `<-`,
1653
+ * `producer.attr <- consumer.via`. Hops are joined with `; `.
1654
+ */
1655
+ function formatPath(hops, dir) {
1656
+ const end = (id, attr) => (attr ? `${bareId(id)}.${attr}` : bareId(id));
1657
+ return hops
1658
+ .map((h) => dir === "out"
1659
+ ? `${end(h.from, h.fromAttr)} -> ${end(h.to, h.toAttr)}`
1660
+ : `${end(h.to, h.toAttr)} <- ${end(h.from, h.fromAttr)}`)
1661
+ .join("; ");
1662
+ }
1663
+ /** The edge operator as written: arrow, depth and qualifier, no target. */
1664
+ function edgeOperator(t, arrows) {
1665
+ const arrow = t.dir === "out" ? arrows.out : arrows.in;
1666
+ const depth = !t.depth
1667
+ ? ""
1668
+ : t.depth.min === 1 && t.depth.max === Infinity
1669
+ ? "*"
1670
+ : t.depth.min === t.depth.max
1671
+ ? `{${t.depth.min}}`
1672
+ : `{${t.depth.min},${t.depth.max === Infinity ? "" : t.depth.max}}`;
1673
+ const quals = [
1674
+ ...(t.viaAttr !== undefined ? [`via=${t.viaAttr}`] : []),
1675
+ ...(t.toAttr !== undefined ? [`to=${t.toAttr}`] : []),
1676
+ ...(t.capability !== undefined ? [`cap=${t.capability}`] : []),
1677
+ ];
1678
+ return `${arrow}${depth}${quals.length > 0 ? `[${quals.join(",")}]` : ""}`;
1679
+ }
1680
+ /** Each positive edge term's path to `n`, in query order (#4359). */
1681
+ function matchPaths(n, terms, ir, byId) {
1682
+ const out = [];
1683
+ for (const t of terms) {
1684
+ if (t.negated)
1685
+ continue;
1686
+ if (t.kind === "cap") {
1687
+ const ev = capabilityEvidence(n, t, ir);
1688
+ if (ev)
1689
+ out.push({ term: t, hops: ev.hops.map(pathHop), because: becauseOf(ev) });
1690
+ continue;
1691
+ }
1692
+ if (t.kind !== "edge")
1693
+ continue;
1694
+ const hops = findPath(n, t, ir, byId);
1695
+ if (hops)
1696
+ out.push({ term: t, hops });
1697
+ }
1698
+ return out;
1699
+ }
1700
+ /**
1701
+ * The evidence for a `cap:` term on a node (#4375): its path when the node
1702
+ * is in the set, or the source of an edge capability, else undefined. A
1703
+ * capability or argument the catalog can't take throws a CapabilityError,
1704
+ * which `runSearch` turns into a refusal before any node is tested.
1705
+ */
1706
+ function capabilityEvidence(n, t, ir) {
1707
+ const call = parseCapabilityCall(t.a);
1708
+ if (!call)
1709
+ return undefined;
1710
+ const engine = capabilityEngine(ir);
1711
+ const args = engine.bind(call.name, call.args);
1712
+ if (engine.rule(call.name).vars.length === 1)
1713
+ return engine.set(call.name, args).get(n.id);
1714
+ const targets = engine.relation(call.name, args).out.get(n.id);
1715
+ return targets ? targets.values().next().value : undefined;
1716
+ }
1717
+ /** One step along an edge capability from `id` (#4375), each with the hops that proved it, in walk order. */
1718
+ function capabilitySteps(id, call, out, ir) {
1719
+ const parsed = parseCapabilityCall(call);
1720
+ if (!parsed)
1721
+ return [];
1722
+ const engine = capabilityEngine(ir);
1723
+ const rel = engine.relation(parsed.name, engine.bind(parsed.name, parsed.args));
1724
+ const m = (out ? rel.out : rel.in).get(id);
1725
+ if (!m)
1726
+ return [];
1727
+ return [...m].map(([far, ev]) => ({ far, hops: (out ? ev.hops : [...ev.hops].reverse()).map(pathHop) }));
1728
+ }
1729
+ function pathHop(h) {
1730
+ return { from: h.from, ...(h.fromAttr ? { fromAttr: h.fromAttr } : {}), to: h.to, ...(h.toAttr ? { toAttr: h.toAttr } : {}), edge: h.edge };
1731
+ }
1732
+ function becauseOf(ev) {
1733
+ const out = [];
1734
+ const walk = (e) => {
1735
+ for (const b of e.because) {
1736
+ out.push({ capability: b.capability, id: b.id, hops: b.evidence.hops.map(pathHop) });
1737
+ walk(b.evidence);
1738
+ }
1739
+ };
1740
+ walk(ev);
1741
+ return out.length > 0 ? out : undefined;
1742
+ }
1743
+ /**
1744
+ * A capability's path as one line (#4375), in #4359's hop format, read from
1745
+ * `start`: each hop is `consumer.via -> producer.attr` when the walk follows
1746
+ * the reference, `producer.attr <- consumer.via` when it goes against it. A
1747
+ * hop with no IR attribute names the capability edge instead (`-statement->`).
1748
+ */
1749
+ function formatHops(hops, start) {
1750
+ const end = (id, attr) => (attr ? `${bareId(id)}.${attr}` : bareId(id));
1751
+ let at = start;
1752
+ return hops
1753
+ .map((h) => {
1754
+ const forward = h.from === at || h.to !== at;
1755
+ at = forward ? h.to : h.from;
1756
+ return forward
1757
+ ? `${end(h.from, h.fromAttr)} -> ${end(h.to, h.toAttr)}`
1758
+ : `${end(h.to, h.toAttr)} <- ${end(h.from, h.fromAttr)}`;
1759
+ })
1760
+ .join("; ");
1761
+ }
890
1762
  function formatRow(n, show) {
891
1763
  const attrs = (n.attrs ?? {});
892
1764
  // Display the bare logical id, not the `${stack}::` qualification (#1162).
893
1765
  const displayId = n.id.includes("::") ? n.id.slice(n.id.lastIndexOf("::") + 2) : n.id;
894
- const parts = [displayId, n.kind ?? ""];
1766
+ // A terraform resource shows its type (#4366); its IR kind is `Terraform::Resource` for all of them.
1767
+ const parts = [displayId, displayKind(n) ?? ""];
895
1768
  // Prefer the node-level live physicalId (set by the overlay), then attrs;
896
1769
  // skip source-mode AttrRef placeholders (objects).
897
1770
  const physical = n.physicalId ?? attrs["physicalId"] ?? attrs["InstanceId"] ?? attrs["Id"];
@@ -902,6 +1775,23 @@ function formatRow(n, show) {
902
1775
  // established; the reason is the tri-state's (#1089).
903
1776
  else if (typeof attrs["_unobserved"] === "string")
904
1777
  parts.push(`(unobserved: ${attrs["_unobserved"]})`);
1778
+ for (const [actual, v] of showValues(attrs, show)) {
1779
+ // A column the caller explicitly asked for is shown whatever shape it is.
1780
+ // Skipping non-scalars silently meant `--show effectiveIngress` — the
1781
+ // derived reachability fact, and the reason to reach for chant at all —
1782
+ // printed a blank column, because it is a list. The agent read that as
1783
+ // "chant does not have this" and hand-rolled the answer from raw
1784
+ // security-group rows, which is exactly the work the fold exists to avoid.
1785
+ parts.push(`${actual}=${typeof v === "object" ? JSON.stringify(v) : attrString(v)}`);
1786
+ }
1787
+ return parts.filter(Boolean).join(" ");
1788
+ }
1789
+ /**
1790
+ * The `--show` columns a row carries, as `[name as the graph spells it, value]`,
1791
+ * in the order asked. Shared by the text row and `--json` (#4367).
1792
+ */
1793
+ function showValues(attrs, show) {
1794
+ const out = [];
905
1795
  for (const key of show) {
906
1796
  // Match the name case-insensitively, and report what was actually found.
907
1797
  // AWS attribute names are PascalCase and chant's derived ones are not, so a
@@ -912,15 +1802,9 @@ function formatRow(n, show) {
912
1802
  const v = actual == null ? undefined : attrs[actual];
913
1803
  if (v == null)
914
1804
  continue;
915
- // A column the caller explicitly asked for is shown whatever shape it is.
916
- // Skipping non-scalars silently meant `--show effectiveIngress` — the
917
- // derived reachability fact, and the reason to reach for chant at all —
918
- // printed a blank column, because it is a list. The agent read that as
919
- // "chant does not have this" and hand-rolled the answer from raw
920
- // security-group rows, which is exactly the work the fold exists to avoid.
921
- parts.push(`${actual}=${typeof v === "object" ? JSON.stringify(v) : attrString(v)}`);
1805
+ out.push([actual, v]);
922
1806
  }
923
- return parts.filter(Boolean).join(" ");
1807
+ return out;
924
1808
  }
925
1809
  /** Internals exposed for unit tests. */
926
- export const __searchInternals = { parseQuery, matchTerm, formatRow, explain, describeTerm, derivedSurface, availableAttrs, ambientHint, regionSpread, showMiss, provenance, renderDrift, mergeResources, pickResources };
1810
+ export const __searchInternals = { parseQuery, matchTerm, findPath, formatPath, searchJson, searchErrorJson, explainData, termText, formatRow, explain, describeTerm, derivedSurface, availableAttrs, ambientHint, regionSpread, showMiss, provenance, provenanceLine, renderDrift, searchDriftJson, mergeResources, pickResources };