@intentius/chant 0.124.44 → 0.124.46

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 (249) hide show
  1. package/dist/capabilities/aws.paths +148 -0
  2. package/dist/capabilities/base-mappings.json +122 -0
  3. package/dist/capabilities/catalog.d.ts +89 -0
  4. package/dist/capabilities/catalog.d.ts.map +1 -0
  5. package/dist/capabilities/catalog.js +204 -0
  6. package/dist/capabilities/language.d.ts +103 -0
  7. package/dist/capabilities/language.d.ts.map +1 -0
  8. package/dist/capabilities/language.js +335 -0
  9. package/dist/capabilities/matcher.d.ts +65 -0
  10. package/dist/capabilities/matcher.d.ts.map +1 -0
  11. package/dist/capabilities/matcher.js +299 -0
  12. package/dist/capabilities/view.d.ts +207 -0
  13. package/dist/capabilities/view.d.ts.map +1 -0
  14. package/dist/capabilities/view.js +484 -0
  15. package/dist/ci-identity.d.ts +183 -12
  16. package/dist/ci-identity.d.ts.map +1 -1
  17. package/dist/ci-identity.js +362 -53
  18. package/dist/cli/commands/lint.d.ts.map +1 -1
  19. package/dist/cli/commands/lint.js +1 -0
  20. package/dist/cli/handlers/build.d.ts.map +1 -1
  21. package/dist/cli/handlers/build.js +17 -1
  22. package/dist/cli/handlers/declared-graph.d.ts +44 -0
  23. package/dist/cli/handlers/declared-graph.d.ts.map +1 -0
  24. package/dist/cli/handlers/declared-graph.js +87 -0
  25. package/dist/cli/handlers/graph.d.ts +25 -0
  26. package/dist/cli/handlers/graph.d.ts.map +1 -1
  27. package/dist/cli/handlers/graph.js +93 -87
  28. package/dist/cli/handlers/pr-env.d.ts +8 -2
  29. package/dist/cli/handlers/pr-env.d.ts.map +1 -1
  30. package/dist/cli/handlers/pr-env.js +127 -225
  31. package/dist/cli/handlers/pr.d.ts +3 -0
  32. package/dist/cli/handlers/pr.d.ts.map +1 -1
  33. package/dist/cli/handlers/pr.js +1 -0
  34. package/dist/cli/handlers/search-count.d.ts +89 -0
  35. package/dist/cli/handlers/search-count.d.ts.map +1 -0
  36. package/dist/cli/handlers/search-count.js +132 -0
  37. package/dist/cli/handlers/search.d.ts +358 -6
  38. package/dist/cli/handlers/search.d.ts.map +1 -1
  39. package/dist/cli/handlers/search.js +1065 -155
  40. package/dist/cli/main.d.ts.map +1 -1
  41. package/dist/cli/main.js +23 -3
  42. package/dist/cli/mcp/server.d.ts.map +1 -1
  43. package/dist/cli/mcp/server.js +3 -0
  44. package/dist/cli/mcp/tools/capabilities.d.ts +7 -0
  45. package/dist/cli/mcp/tools/capabilities.d.ts.map +1 -0
  46. package/dist/cli/mcp/tools/capabilities.js +27 -0
  47. package/dist/cli/mcp/workspace-tools.d.ts +3 -1
  48. package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
  49. package/dist/cli/mcp/workspace-tools.js +47 -2
  50. package/dist/cli/parse-args.d.ts.map +1 -1
  51. package/dist/cli/parse-args.js +64 -17
  52. package/dist/cli/registry.d.ts +22 -0
  53. package/dist/cli/registry.d.ts.map +1 -1
  54. package/dist/components/cli-support.d.ts +2 -0
  55. package/dist/components/cli-support.d.ts.map +1 -1
  56. package/dist/components/cli-support.js +4 -2
  57. package/dist/components/fan-out-support.d.ts +3 -0
  58. package/dist/components/fan-out-support.d.ts.map +1 -1
  59. package/dist/components/fan-out-support.js +1 -1
  60. package/dist/components/pr-env-ports.d.ts +29 -0
  61. package/dist/components/pr-env-ports.d.ts.map +1 -0
  62. package/dist/components/pr-env-ports.js +77 -0
  63. package/dist/components/pr-env-run.d.ts +254 -0
  64. package/dist/components/pr-env-run.d.ts.map +1 -0
  65. package/dist/components/pr-env-run.js +376 -0
  66. package/dist/components/pr-environment.d.ts +91 -15
  67. package/dist/components/pr-environment.d.ts.map +1 -1
  68. package/dist/components/pr-environment.js +95 -9
  69. package/dist/components/pr-pipeline.d.ts +10 -1
  70. package/dist/components/pr-pipeline.d.ts.map +1 -1
  71. package/dist/config-environments.d.ts +92 -0
  72. package/dist/config-environments.d.ts.map +1 -0
  73. package/dist/config-environments.js +102 -0
  74. package/dist/config.d.ts +24 -84
  75. package/dist/config.d.ts.map +1 -1
  76. package/dist/config.js +9 -96
  77. package/dist/graph-detail.d.ts.map +1 -1
  78. package/dist/graph-detail.js +15 -3
  79. package/dist/graph-effective.d.ts +33 -2
  80. package/dist/graph-effective.d.ts.map +1 -1
  81. package/dist/graph-effective.js +215 -68
  82. package/dist/graph-ir.d.ts +23 -0
  83. package/dist/graph-ir.d.ts.map +1 -1
  84. package/dist/graph-ir.js +130 -0
  85. package/dist/graph-sub-template.d.ts +11 -0
  86. package/dist/graph-sub-template.d.ts.map +1 -0
  87. package/dist/graph-sub-template.js +19 -0
  88. package/dist/lexicon.d.ts +9 -0
  89. package/dist/lexicon.d.ts.map +1 -1
  90. package/dist/lifecycle/observe.d.ts +28 -0
  91. package/dist/lifecycle/observe.d.ts.map +1 -1
  92. package/dist/lifecycle/observe.js +23 -1
  93. package/dist/lint/rule.d.ts +6 -0
  94. package/dist/lint/rule.d.ts.map +1 -1
  95. package/dist/live-endpoint.d.ts +1 -1
  96. package/dist/live-endpoint.d.ts.map +1 -1
  97. package/dist/live-endpoint.js +1 -1
  98. package/dist/observation.d.ts +48 -1
  99. package/dist/observation.d.ts.map +1 -1
  100. package/dist/observation.js +21 -5
  101. package/dist/op/activities/activity-contracts.d.ts +4 -4
  102. package/dist/op/op-waves.d.ts +17 -0
  103. package/dist/op/op-waves.d.ts.map +1 -1
  104. package/dist/op/op-waves.js +23 -1
  105. package/dist/pr-forge.d.ts +15 -1
  106. package/dist/pr-forge.d.ts.map +1 -1
  107. package/dist/pr-forge.js +28 -16
  108. package/dist/pr-requires.d.ts +5 -1
  109. package/dist/pr-requires.d.ts.map +1 -1
  110. package/dist/wave-runtime.mjs +251 -250
  111. package/dist/workspace/capabilities-contract.d.ts +66 -0
  112. package/dist/workspace/capabilities-contract.d.ts.map +1 -0
  113. package/dist/workspace/capabilities-contract.js +107 -0
  114. package/dist/workspace/capabilities.schema.json +229 -0
  115. package/dist/workspace/ci-cloud-identity.d.ts +3 -1
  116. package/dist/workspace/ci-cloud-identity.d.ts.map +1 -1
  117. package/dist/workspace/ci-cloud-identity.js +16 -1
  118. package/dist/workspace/ci-run.d.ts +35 -8
  119. package/dist/workspace/ci-run.d.ts.map +1 -1
  120. package/dist/workspace/ci-run.js +95 -21
  121. package/dist/workspace/ci-secrets.d.ts.map +1 -1
  122. package/dist/workspace/ci-secrets.js +4 -3
  123. package/dist/workspace/compose-graph.d.ts +9 -0
  124. package/dist/workspace/compose-graph.d.ts.map +1 -1
  125. package/dist/workspace/compose-graph.js +3 -1
  126. package/dist/workspace/conformance/index.d.ts +1 -1
  127. package/dist/workspace/conformance/index.d.ts.map +1 -1
  128. package/dist/workspace/conformance/index.js +39 -2
  129. package/dist/workspace/declaration.d.ts +10 -0
  130. package/dist/workspace/declaration.d.ts.map +1 -1
  131. package/dist/workspace/declaration.js +17 -1
  132. package/dist/workspace/declaration.schema.json +36 -1
  133. package/dist/workspace/graph-cache.d.ts +8 -0
  134. package/dist/workspace/graph-cache.d.ts.map +1 -1
  135. package/dist/workspace/graph-cache.js +27 -3
  136. package/dist/workspace/graph-cli.d.ts +39 -2
  137. package/dist/workspace/graph-cli.d.ts.map +1 -1
  138. package/dist/workspace/graph-cli.js +66 -24
  139. package/dist/workspace/graph.schema.json +148 -0
  140. package/dist/workspace/kind-readers.d.ts +3 -0
  141. package/dist/workspace/kind-readers.d.ts.map +1 -1
  142. package/dist/workspace/kind-readers.js +7 -1
  143. package/dist/workspace/member-commands.d.ts +10 -1
  144. package/dist/workspace/member-commands.d.ts.map +1 -1
  145. package/dist/workspace/member-commands.js +17 -1
  146. package/dist/workspace/nested-graph.d.ts +2 -0
  147. package/dist/workspace/nested-graph.d.ts.map +1 -1
  148. package/dist/workspace/nested-graph.js +32 -1
  149. package/dist/workspace/reason-codes.d.ts +10 -0
  150. package/dist/workspace/reason-codes.d.ts.map +1 -1
  151. package/dist/workspace/reason-codes.js +12 -0
  152. package/dist/workspace/search-cli.d.ts +204 -0
  153. package/dist/workspace/search-cli.d.ts.map +1 -0
  154. package/dist/workspace/search-cli.js +466 -0
  155. package/dist/workspace/search-contract.d.ts +56 -0
  156. package/dist/workspace/search-contract.d.ts.map +1 -0
  157. package/dist/workspace/search-contract.js +70 -0
  158. package/dist/workspace/search.schema.json +822 -0
  159. package/package.json +1 -1
  160. package/src/__snapshots__/okf.test.ts.snap +10 -0
  161. package/src/capabilities/aws.paths +148 -0
  162. package/src/capabilities/base-mappings.json +122 -0
  163. package/src/capabilities/catalog.ts +230 -0
  164. package/src/capabilities/language.test.ts +191 -0
  165. package/src/capabilities/language.ts +373 -0
  166. package/src/capabilities/matcher.ts +315 -0
  167. package/src/capabilities/view-compose.test.ts +76 -0
  168. package/src/capabilities/view.ts +631 -0
  169. package/src/ci-identity-targets.test.ts +275 -0
  170. package/src/ci-identity.ts +394 -61
  171. package/src/cli/commands/lint.ts +1 -0
  172. package/src/cli/handlers/build.ts +15 -1
  173. package/src/cli/handlers/declared-graph.ts +99 -0
  174. package/src/cli/handlers/graph.test.ts +94 -3
  175. package/src/cli/handlers/graph.ts +123 -96
  176. package/src/cli/handlers/pr-env.ts +135 -227
  177. package/src/cli/handlers/pr.ts +4 -0
  178. package/src/cli/handlers/search-build-roots.test.ts +144 -0
  179. package/src/cli/handlers/search-count.test.ts +191 -0
  180. package/src/cli/handlers/search-count.ts +165 -0
  181. package/src/cli/handlers/search-json.test.ts +333 -0
  182. package/src/cli/handlers/search-live.test.ts +387 -0
  183. package/src/cli/handlers/search-paths.test.ts +235 -0
  184. package/src/cli/handlers/search.ts +1312 -166
  185. package/src/cli/main.test.ts +28 -3
  186. package/src/cli/main.ts +23 -3
  187. package/src/cli/mcp/server.test.ts +3 -3
  188. package/src/cli/mcp/server.ts +3 -0
  189. package/src/cli/mcp/tools/capabilities.ts +32 -0
  190. package/src/cli/mcp/workspace-tools.e2e.test.ts +21 -1
  191. package/src/cli/mcp/workspace-tools.ts +47 -2
  192. package/src/cli/parse-args.ts +52 -13
  193. package/src/cli/registry.ts +22 -0
  194. package/src/components/cli-support.ts +6 -1
  195. package/src/components/fan-out-support.ts +4 -1
  196. package/src/components/pr-env-ports.ts +89 -0
  197. package/src/components/pr-env-run-bundle.test.ts +54 -0
  198. package/src/components/pr-env-run.test.ts +290 -0
  199. package/src/components/pr-env-run.ts +563 -0
  200. package/src/components/pr-environment.ts +161 -13
  201. package/src/components/pr-pipeline.ts +10 -1
  202. package/src/config-environments.ts +133 -0
  203. package/src/config.ts +37 -124
  204. package/src/graph-detail.ts +15 -3
  205. package/src/graph-effective.ts +226 -75
  206. package/src/graph-ir.test.ts +163 -0
  207. package/src/graph-ir.ts +147 -0
  208. package/src/graph-sub-template.ts +22 -0
  209. package/src/lexicon.ts +10 -0
  210. package/src/lifecycle/observe.test.ts +27 -1
  211. package/src/lifecycle/observe.ts +43 -1
  212. package/src/lint/rule.ts +6 -0
  213. package/src/live-endpoint.ts +1 -1
  214. package/src/observation.test.ts +25 -0
  215. package/src/observation.ts +69 -5
  216. package/src/op/op-waves.ts +38 -1
  217. package/src/pr-forge-bundle.test.ts +1 -1
  218. package/src/pr-forge.ts +44 -17
  219. package/src/pr-requires.test.ts +43 -6
  220. package/src/pr-requires.ts +5 -1
  221. package/src/workspace/capabilities-contract.test.ts +88 -0
  222. package/src/workspace/capabilities-contract.ts +132 -0
  223. package/src/workspace/capabilities.schema.json +229 -0
  224. package/src/workspace/ci-cloud-identity.test.ts +20 -0
  225. package/src/workspace/ci-cloud-identity.ts +14 -1
  226. package/src/workspace/ci-run.test.ts +74 -1
  227. package/src/workspace/ci-run.ts +122 -29
  228. package/src/workspace/ci-secrets.test.ts +1 -1
  229. package/src/workspace/ci-secrets.ts +4 -3
  230. package/src/workspace/compose-graph.ts +10 -1
  231. package/src/workspace/conformance/conformance.e2e.test.ts +14 -1
  232. package/src/workspace/conformance/index.ts +31 -2
  233. package/src/workspace/declaration.schema.json +36 -1
  234. package/src/workspace/declaration.test.ts +7 -0
  235. package/src/workspace/declaration.ts +24 -2
  236. package/src/workspace/graph-cache.ts +27 -3
  237. package/src/workspace/graph-cli.ts +96 -24
  238. package/src/workspace/graph.schema.json +148 -0
  239. package/src/workspace/kind-readers.e2e.test.ts +28 -0
  240. package/src/workspace/kind-readers.test.ts +1 -1
  241. package/src/workspace/kind-readers.ts +7 -1
  242. package/src/workspace/member-commands.ts +22 -1
  243. package/src/workspace/nested-graph.ts +32 -1
  244. package/src/workspace/reason-codes.test.ts +7 -0
  245. package/src/workspace/reason-codes.ts +12 -0
  246. package/src/workspace/search-cli.e2e.test.ts +305 -0
  247. package/src/workspace/search-cli.ts +591 -0
  248. package/src/workspace/search-contract.ts +95 -0
  249. package/src/workspace/search.schema.json +822 -0
@@ -0,0 +1,99 @@
1
+ import { dirname } from "node:path";
2
+ import { mergeBuildRootEntities } from "../../build";
3
+ import { loadChantConfigUpward, type ChantConfig } from "../../config";
4
+ import type { Declarable } from "../../declarable";
5
+ import { applyLexiconGraphEdges, type GraphIR, type LexiconGraphEdges } from "../../graph-ir";
6
+ import type { BuildRootContributor, LexiconPlugin } from "../../lexicon";
7
+ import { formatError, formatWarning } from "../format";
8
+ import { collectBuildRootContributors, loadPlugins, resolveProjectLexicons } from "../plugins";
9
+ import type { CommandContext } from "../registry";
10
+
11
+ /**
12
+ * The pieces of the declared graph that `chant graph` and `chant search` both
13
+ * read (#1626, #4365, #4368). They live here, once, so the two commands answer
14
+ * over the same entity set and the same edges: search drifted from graph once
15
+ * already, answering `(no matches)` over a `terraform.roots` project graph drew
16
+ * in full, because it never ran the build-root contributors.
17
+ */
18
+
19
+ /**
20
+ * The build-root contributors this invocation's project configures (#1626):
21
+ * a `terraform.roots` root's blocks, a `k8s.kustomize.roots` root's rendered
22
+ * objects. The same closures `chant build` and the lifecycle handlers hand to
23
+ * `BuildOptions.buildRoots`, bound here for the discover-based paths, which
24
+ * never run `build()` and so never ran the hook.
25
+ *
26
+ * Neither command is `requiresPlugins` (Op/source-graph modes must work
27
+ * without a lexicon), so plugins are loaded here when `ctx.plugins` is empty.
28
+ * Rooted at the config's own directory, like `cli/commands/build.ts`: a
29
+ * relative root resolves against where it was declared, not against a
30
+ * sourceDir-scoped path. A project that fails to load plugins degrades to no
31
+ * contributors with a warning rather than failing modes that never needed
32
+ * plugins at all.
33
+ */
34
+ export async function declaredBuildRootContributors(
35
+ ctx: CommandContext,
36
+ projectPath: string,
37
+ ): Promise<BuildRootContributor[]> {
38
+ const { config, configPath } = await loadChantConfigUpward(projectPath).catch(
39
+ () => ({ config: {} as ChantConfig, configPath: undefined }),
40
+ );
41
+ let plugins = ctx.plugins;
42
+ if (plugins.length === 0) {
43
+ try {
44
+ plugins = await loadPlugins(await resolveProjectLexicons(projectPath));
45
+ } catch (err) {
46
+ console.error(formatWarning({
47
+ message: `could not load the project's lexicons — build-root entities (terraform and kustomize roots) will be missing from the graph (${err instanceof Error ? err.message : String(err)})`,
48
+ }));
49
+ return [];
50
+ }
51
+ }
52
+ return collectBuildRootContributors(
53
+ plugins,
54
+ config as unknown as Record<string, unknown>,
55
+ configPath ? dirname(configPath) : projectPath,
56
+ );
57
+ }
58
+
59
+ /**
60
+ * Merge build-root contributions into a discovered entity set: the exact
61
+ * merge `build()` performs (one implementation: `mergeBuildRootEntities`,
62
+ * ../../build.ts), with its warnings and errors printed. Returns false when a
63
+ * contributor failed or a name collided; the caller stops, because a graph
64
+ * missing the entities the build would refuse over must not be answered from
65
+ * as if complete.
66
+ */
67
+ export async function mergeDeclaredBuildRoots(
68
+ entities: Map<string, Declarable>,
69
+ contributors: BuildRootContributor[],
70
+ ): Promise<boolean> {
71
+ if (contributors.length === 0) return true;
72
+ const merged = await mergeBuildRootEntities(entities, contributors);
73
+ for (const w of merged.warnings) console.error(formatWarning({ message: w }));
74
+ if (merged.errors.length > 0) {
75
+ for (const message of merged.errors) console.error(formatError({ message }));
76
+ return false;
77
+ }
78
+ return true;
79
+ }
80
+
81
+ /**
82
+ * Add the edges the lexicons' `graphEdges` hooks resolve (#3910): CI
83
+ * dependencies written as strings (`needs`, `uses:`, `trigger:`, `include:`,
84
+ * `extends`), which the AttrRef walk in `buildGraphIr` cannot see. A hook that
85
+ * throws costs its own edges and a warning, not the graph.
86
+ */
87
+ export function withLexiconEdges(ir: GraphIR, plugins: readonly LexiconPlugin[], entities: Map<string, Declarable>): GraphIR {
88
+ const reports: LexiconGraphEdges[] = [];
89
+ for (const p of plugins) {
90
+ if (!p.graphEdges) continue;
91
+ try {
92
+ const report = p.graphEdges(entities);
93
+ if (report) reports.push(report);
94
+ } catch (err) {
95
+ console.error(formatWarning({ message: `the ${p.name} lexicon's graphEdges failed (${err instanceof Error ? err.message : String(err)}); its edges are left out` }));
96
+ }
97
+ }
98
+ return reports.length === 0 ? ir : applyLexiconGraphEdges(ir, reports);
99
+ }
@@ -75,9 +75,10 @@ vi.mock("../../lifecycle/replay", () => ({
75
75
  hasSnapshot: (...a: unknown[]) => hasSnapshotMock(...a),
76
76
  }));
77
77
 
78
- vi.mock("../../lifecycle/observe", () => ({
79
- observeResources: (...a: unknown[]) => observeMock(...a),
80
- }));
78
+ vi.mock("../../lifecycle/observe", async () => {
79
+ const actual = await vi.importActual<typeof import("../../lifecycle/observe")>("../../lifecycle/observe");
80
+ return { carryAnnotations: actual.carryAnnotations, observeResources: (...a: unknown[]) => observeMock(...a) };
81
+ });
81
82
  // `runGraphLive` resolves each component's `cfn-deploy` stack(s) (#57) so a
82
83
  // multi-stack, per-component project observes the right stacks; default to
83
84
  // "no components" (empty map, no errors) so the existing single-stack --live
@@ -544,6 +545,66 @@ describe("runGraph", () => {
544
545
  });
545
546
  });
546
547
 
548
+ // behold#919: a project that reads source it did not author (a workspace
549
+ // reader project, behold's scratch project for a bare Terraform folder)
550
+ // sets `graph.lint: "report"`, and a lint error is a finding about the
551
+ // estate, carried on meta.lint, not a reason to draw nothing.
552
+ describe('graph.lint: "report" (behold#919)', () => {
553
+ const finding = { file: "/abs/estate/db.tf", line: 4, column: 3, ruleId: "TF033", severity: "error", message: "[estate/aws_db_instance.orders] storage_encrypted is false", entity: "estate/aws_db_instance.orders" };
554
+ const warning = { file: "/abs/estate/net.tf", line: 9, column: 1, ruleId: "TF010", severity: "warning", message: "a warning" };
555
+ const reportMode = (): void => {
556
+ loadChantConfigUpwardMock.mockResolvedValue({ config: { graph: { lint: "report" } } });
557
+ };
558
+
559
+ test("emits the graph with lint errors, every finding on meta.lint", async () => {
560
+ reportMode(); discovered();
561
+ lintMock.mockResolvedValue({ success: false, diagnostics: [finding, warning] });
562
+ const exit = await runGraph({ args: makeArgs({ format: "ir" }), plugins: [], serializers: [] });
563
+ expect(exit).toBe(0);
564
+ const ir = JSON.parse(stdoutBuf.join("\n"));
565
+ expect(ir.nodes.map((n: { id: string }) => n.id).sort()).toEqual(["pod", "subnet", "vpc"]);
566
+ expect(ir.meta.lint).toEqual({
567
+ errors: 1,
568
+ warnings: 1,
569
+ findings: [
570
+ { ruleId: "TF033", severity: "error", message: "[estate/aws_db_instance.orders] storage_encrypted is false", file: "/abs/estate/db.tf", line: 4, column: 3, entity: "estate/aws_db_instance.orders" },
571
+ { ruleId: "TF010", severity: "warning", message: "a warning", file: "/abs/estate/net.tf", line: 9, column: 1 },
572
+ ],
573
+ });
574
+ expect(stderrBuf.join("\n")).toMatch(/with 1 lint error: graph\.lint is "report"/);
575
+ });
576
+
577
+ test("a clean read still carries meta.lint, so a reader can tell clean from not looked", async () => {
578
+ reportMode(); lintClean(); discovered();
579
+ const exit = await runGraph({ args: makeArgs({ format: "ir" }), plugins: [], serializers: [] });
580
+ expect(exit).toBe(0);
581
+ expect(JSON.parse(stdoutBuf.join("\n")).meta.lint).toEqual({ errors: 0, warnings: 0, findings: [] });
582
+ });
583
+
584
+ test("the default gate is unchanged, and adds no meta.lint", async () => {
585
+ lintClean(); discovered();
586
+ await runGraph({ args: makeArgs({ format: "ir" }), plugins: [], serializers: [] });
587
+ expect(JSON.parse(stdoutBuf.join("\n")).meta?.lint).toBeUndefined();
588
+ });
589
+
590
+ test("an uninstalled dependency still refuses: there is no source to read", async () => {
591
+ const project = realpathSync(mkdtempSync(join(tmpdir(), "chant-919-graph-")));
592
+ try {
593
+ reportMode(); discovered();
594
+ lintMock.mockResolvedValue({
595
+ success: false,
596
+ diagnostics: [{ file: join(project, "a.ts"), line: 1, column: 1, ruleId: "LEX002", severity: "error", message: `Cannot find package '@intentius/chant-lexicon-aws' imported from ${join(project, "a.ts")}` }],
597
+ });
598
+ const exit = await runGraph({ args: makeArgs({ format: "ir", path: project }), plugins: [], serializers: [] });
599
+ expect(exit).toBe(1);
600
+ expect(stdoutBuf.join("\n")).toBe("");
601
+ expect(stderrBuf.join("\n")).toContain("dependencies not installed");
602
+ } finally {
603
+ rmSync(project, { recursive: true, force: true });
604
+ }
605
+ });
606
+ });
607
+
547
608
  test("--format mermaid emits a flowchart", async () => {
548
609
  lintClean(); discovered();
549
610
  const exit = await runGraph({ args: makeArgs({ format: "mermaid" }), plugins: [], serializers: [] });
@@ -872,6 +933,36 @@ describe("runGraph", () => {
872
933
  expect(out).toContain("web-vpc");
873
934
  });
874
935
 
936
+ // #4372 — a planned create is absent, and its plan attributes ride beside
937
+ // the verdict as annotations. `--overlay` puts them on the declared node,
938
+ // which keeps its pending paint: behold's plan cards read this IR.
939
+ test("--overlay carries an absent entity's annotations onto its declared node, still pending", async () => {
940
+ resolveLexMock.mockResolvedValue(["terraform"]);
941
+ loadPluginsMock.mockResolvedValue([
942
+ { name: "terraform", serializer: {}, describeResources: () => Promise.resolve({}) },
943
+ ]);
944
+ discoverMock.mockResolvedValue({
945
+ entities: new Map([
946
+ ["app/terraform_data.created", decl({ lexicon: "terraform", entityType: "Terraform::Resource", props: {} })],
947
+ ]),
948
+ errors: [],
949
+ sourceFiles: [],
950
+ });
951
+ observeMock.mockResolvedValue({
952
+ observations: [],
953
+ errors: [],
954
+ warnings: [],
955
+ annotations: { "app/terraform_data.created": { planAction: "create", planAfterUnknown: ["id"] } },
956
+ });
957
+ // `--detail 3`, as behold asks: lower tiers thin list attributes.
958
+ const exit = await runGraph({ args: makeArgs({ format: "ir", live: true, overlay: true, env: "prod", detail: 3 }), plugins: [], serializers: [] });
959
+ expect(exit).toBe(0);
960
+ const ir = JSON.parse(stdoutBuf.join("\n")) as { nodes: Array<{ id: string; physicalId?: string; attrs: Record<string, unknown> }> };
961
+ const node = ir.nodes.find((n) => n.id === "app/terraform_data.created");
962
+ expect(node?.attrs).toMatchObject({ _status: "accent", planAction: "create", planAfterUnknown: ["id"] });
963
+ expect(node?.physicalId).toBeUndefined();
964
+ });
965
+
875
966
  // #2377 — the overlay carries the prediction. behold's reader takes
876
967
  // `attrs._behaviour` per node and `meta._behaviour` on the graph, and
877
968
  // reads the absence of both as "not looked" rather than as a refusal, so
@@ -1,15 +1,15 @@
1
- import { dirname, resolve } from "node:path";
1
+ import { isAbsolute, relative, resolve } from "node:path";
2
2
  import { discoverOps } from "../../op/discover";
3
3
  import { discover } from "../../discovery/index";
4
- import { partitionByLexicon, computeStackGraph, build, mergeBuildRootEntities } from "../../build";
5
- import { applyLexiconGraphEdges, applyLexiconOutputs, buildGraphIr, buildLiveGraphIr, collectUnobserved, overlayGraphs, sourceOverlayGraphs, GRAPH_IR_VERSION, type GraphIR, type IRPipeline, type LexiconGraphEdges, type LexiconWorkspaceOutputs, type LiveObservation } from "../../graph-ir";
4
+ import { partitionByLexicon, computeStackGraph, build } from "../../build";
5
+ import { applyLexiconOutputs, buildGraphIr, buildLiveGraphIr, collectUnobserved, overlayGraphs, sourceOverlayGraphs, GRAPH_IR_VERSION, type GraphIR, type IRPipeline, type LexiconWorkspaceOutputs, type LiveObservation } from "../../graph-ir";
6
6
  import { behaviourOverlay } from "../../behaviour-overlay";
7
7
  import type { SerializerResult } from "../../serializer";
8
8
  import { applyBehaviourOverlay, behaviourRequestFromIr } from "../../behaviour-graph";
9
9
  import { buildDeclaredPerStack } from "../../graph-declared";
10
10
  import { mergeProjectOps } from "../../graph-ops";
11
11
  import { reconstructEdges, mergeCatalogs, containmentGroups, type ReferenceCatalog, type ContainmentPair } from "../../graph-refs";
12
- import { observeResources } from "../../lifecycle/observe";
12
+ import { carryAnnotations, observeResources } from "../../lifecycle/observe";
13
13
  import { withLiveReadSession } from "../../live-read-session";
14
14
  import { replaySnapshots, hasSnapshot } from "../../lifecycle/replay";
15
15
  import { loadChantConfig, environmentNames, matchesDeclaredEnvironment, loadChantConfigUpward, type ChantConfig } from "../../config";
@@ -22,6 +22,12 @@ import { getLayoutEngine, toLayoutInput, type NodeSize } from "../../graph-layou
22
22
  import { lintCommand, type LintResult } from "../commands/lint";
23
23
  import { reportUninstalledDependencies } from "../dependencies";
24
24
  import { loadPlugins, resolveProjectLexicons, collectBuildRootContributors } from "../plugins";
25
+ // One copy for graph and search (#4365): the build-root join and the lexicon edges.
26
+ import {
27
+ declaredBuildRootContributors as graphBuildRootContributors,
28
+ mergeDeclaredBuildRoots as mergeGraphBuildRoots,
29
+ withLexiconEdges,
30
+ } from "./declared-graph";
25
31
  import { isNoLexiconDetected } from "../../detectLexicon";
26
32
  import { readFileSync } from "node:fs";
27
33
  import { formatError, formatWarning, formatBold } from "../format";
@@ -78,69 +84,6 @@ async function graphBuildParams(
78
84
  return resolution.provenance;
79
85
  }
80
86
 
81
- /**
82
- * The build-root contributors this invocation's project configures (#1626) —
83
- * the same closures `chant build` and the lifecycle handlers hand to
84
- * `BuildOptions.buildRoots`, bound here for the graph command's
85
- * discover-based paths, which never run `build()` and so never ran the hook:
86
- * a `k8s.kustomize.roots` estate graphed only its typed source while its
87
- * built (and deployed) output carried the rendered manifests too.
88
- *
89
- * `graph` is not `requiresPlugins` (Op/source-graph modes must work without a
90
- * lexicon), so plugins are loaded here when `ctx.plugins` is empty, mirroring
91
- * `runGraphLive`. Rooted at the config's own directory, like
92
- * `cli/commands/build.ts` — a relative kustomize root resolves against where
93
- * it was declared, not against a sourceDir-scoped graph path. A project that
94
- * fails to load plugins degrades to no contributors with a warning rather
95
- * than failing modes that never needed plugins at all.
96
- */
97
- async function graphBuildRootContributors(
98
- ctx: CommandContext,
99
- projectPath: string,
100
- ): Promise<Array<import("../../lexicon").BuildRootContributor>> {
101
- const { config, configPath } = await loadChantConfigUpward(projectPath).catch(
102
- () => ({ config: {} as ChantConfig, configPath: undefined }),
103
- );
104
- let plugins = ctx.plugins;
105
- if (plugins.length === 0) {
106
- try {
107
- plugins = await loadPlugins(await resolveProjectLexicons(projectPath));
108
- } catch (err) {
109
- console.error(formatWarning({
110
- message: `could not load the project's lexicons — build-root entities (kustomize roots) will be missing from the graph (${err instanceof Error ? err.message : String(err)})`,
111
- }));
112
- return [];
113
- }
114
- }
115
- return collectBuildRootContributors(
116
- plugins,
117
- config as unknown as Record<string, unknown>,
118
- configPath ? dirname(configPath) : projectPath,
119
- );
120
- }
121
-
122
- /**
123
- * Merge build-root contributions into a discovered entity set — the exact
124
- * merge `build()` performs (one implementation: `mergeBuildRootEntities`,
125
- * ../../build.ts), surfaced in the graph command's error style. Returns false
126
- * when a contributor failed or a name collided; the caller stops, matching
127
- * how the discover-based paths treat discovery errors — a graph missing the
128
- * entities the build would refuse over must not be emitted as if complete.
129
- */
130
- async function mergeGraphBuildRoots(
131
- entities: Map<string, import("../../declarable").Declarable>,
132
- contributors: Array<import("../../lexicon").BuildRootContributor>,
133
- ): Promise<boolean> {
134
- if (contributors.length === 0) return true;
135
- const merged = await mergeBuildRootEntities(entities, contributors);
136
- for (const w of merged.warnings) console.error(formatWarning({ message: w }));
137
- if (merged.errors.length > 0) {
138
- for (const message of merged.errors) console.error(formatError({ message }));
139
- return false;
140
- }
141
- return true;
142
- }
143
-
144
87
  /** #1675 — join git-root ops into a discovered entity map; op-discovery
145
88
  * errors are warnings here (the lint gate never saw those files). */
146
89
  async function mergeGraphOps(
@@ -187,6 +130,15 @@ async function runGraphCommand(ctx: CommandContext): Promise<number> {
187
130
  }));
188
131
  return 1;
189
132
  }
133
+ // The capability view (#4378) is built from the declared entity graph's IR,
134
+ // the read `chant workspace graph --capability-view` asks each member for.
135
+ if (ctx.args.capabilityView && (ctx.args.format !== "ir" || ctx.args.components || ctx.args.live || ctx.args.at)) {
136
+ console.error(formatError({
137
+ message: "--capability-view adds the capability view to the declared graph's IR, so it needs --format ir and takes no --components, --live or --at",
138
+ hint: "chant graph --format ir --detail 3 --capability-view",
139
+ }));
140
+ return 1;
141
+ }
190
142
  // `--at` graphs a recorded observation instead of reading the estate (#1279).
191
143
  // Two different observations with no rule for which wins, so not both.
192
144
  if (ctx.args.at && ctx.args.live) {
@@ -325,6 +277,8 @@ async function runGraphLive(
325
277
 
326
278
  let ir: GraphIR;
327
279
  let observations: LiveObservation[];
280
+ // Facts about absent entities (#4372), carried onto the overlay's nodes.
281
+ let annotations: Record<string, Record<string, unknown>> | undefined;
328
282
  // A replay is a graph of what was recorded, so it must not reach the provider
329
283
  // at all — not for observation and not for enrichment. Anything that quietly
330
284
  // called out would make `--at` a live read with an older label on it.
@@ -353,6 +307,7 @@ async function runGraphLive(
353
307
  ...(args.namespace ? { namespace: args.namespace } : {}),
354
308
  });
355
309
  observations = observeResult.observations;
310
+ annotations = observeResult.annotations;
356
311
  const { errors, warnings, notes = [] } = observeResult;
357
312
  for (const e of errors) console.error(formatWarning({ message: e }));
358
313
  // Unobserved entities (#1089) arrive as warnings — a node missing from the
@@ -465,6 +420,10 @@ async function runGraphLive(
465
420
  : sourceOverlayGraphs(declaredIr, ir, overlayOpts);
466
421
  }
467
422
  }
423
+ // What a read learned about an entity it found absent (#4372), on the
424
+ // declared node beside the overlay's `_status`. A planned create keeps its
425
+ // pending paint and carries the plan's attributes.
426
+ ir = { ...ir, nodes: carryAnnotations(ir.nodes, annotations) };
468
427
  }
469
428
 
470
429
  // Behaviour overlay (#2377, epic #2355): carry a prediction on the graph.
@@ -574,6 +533,82 @@ function refuseLintGate(lint: LintResult, projectPath: string): void {
574
533
  );
575
534
  }
576
535
 
536
+ /** One lint finding as `meta.lint` carries it (INTENTIUS/behold#919). */
537
+ export interface GraphLintFinding {
538
+ ruleId: string;
539
+ severity: string;
540
+ message: string;
541
+ /** Relative to the project when inside it, else as lint reported it. */
542
+ file: string;
543
+ line: number;
544
+ column: number;
545
+ /** The entity a post-synth check named, which is the graph node's id. Absent for a source-file rule. */
546
+ entity?: string;
547
+ }
548
+
549
+ /**
550
+ * What a `graph.lint: "report"` project's entity graph carries on `meta.lint`:
551
+ * the counts `chant lint` would print and every finding behind them, warnings
552
+ * included. Present on every graph such a project emits, findings or none, so
553
+ * a reader can tell "linted, clean" from "not looked" (no key).
554
+ */
555
+ export interface GraphLintReport {
556
+ errors: number;
557
+ warnings: number;
558
+ findings: GraphLintFinding[];
559
+ }
560
+
561
+ /** The project's `graph.lint`: `"report"` only when it says so (#919). */
562
+ async function graphLintMode(projectPath: string): Promise<"gate" | "report"> {
563
+ const { config } = await loadChantConfigUpward(projectPath).catch(() => ({ config: {} as ChantConfig }));
564
+ return config.graph?.lint === "report" ? "report" : "gate";
565
+ }
566
+
567
+ export function graphLintReport(lint: LintResult, projectPath: string): GraphLintReport {
568
+ const rel = (file: string) => {
569
+ if (!isAbsolute(file)) return file;
570
+ const r = relative(projectPath, file);
571
+ return r.startsWith("..") ? file : r;
572
+ };
573
+ const findings = (lint.diagnostics ?? []).map((d) => ({
574
+ ruleId: d.ruleId,
575
+ severity: d.severity,
576
+ message: d.message,
577
+ file: rel(d.file),
578
+ line: d.line,
579
+ column: d.column,
580
+ ...(d.entity ? { entity: d.entity } : {}),
581
+ }));
582
+ return {
583
+ errors: findings.filter((f) => f.severity === "error").length,
584
+ warnings: findings.filter((f) => f.severity === "warning").length,
585
+ findings,
586
+ };
587
+ }
588
+
589
+ /**
590
+ * The lint gate for the entity graph. Returns false when the graph must not be
591
+ * emitted, having printed why. A `graph.lint: "report"` project passes with
592
+ * lint errors and says so on stderr, unless an error is an uninstalled
593
+ * dependency: then there is no source to read, and it refuses as the gate does.
594
+ */
595
+ function passLintGate(lint: LintResult, projectPath: string, mode: "gate" | "report"): boolean {
596
+ if (lint.success) return true;
597
+ if (mode === "gate") {
598
+ refuseLintGate(lint, projectPath);
599
+ return false;
600
+ }
601
+ const errors = (lint.diagnostics ?? []).filter((d) => d.severity === "error").map((d) => d.message);
602
+ if (reportUninstalledDependencies(errors, projectPath)) return false;
603
+ const n = errors.length;
604
+ console.error(
605
+ formatWarning({
606
+ message: `Emitting the graph with ${n} lint error${n === 1 ? "" : "s"}: graph.lint is "report", so every finding is on the IR's meta.lint. Run \`chant lint\` to see them.`,
607
+ }),
608
+ );
609
+ return true;
610
+ }
611
+
577
612
  /**
578
613
  * `chant graph --components --format ir|mermaid|dot|layout` — the component DAG
579
614
  * projected into the renderable IR (nodes = components, `groups.byWave` = the
@@ -718,7 +753,9 @@ async function buildPipelineProjection(
718
753
  * `--node-sizes` so spacing fits real node footprints, and defaults to the dagre
719
754
  * engine (no native dependency); `--layout-engine graphviz` opts into `dot`.
720
755
  * Lint-gated: the IR represents valid infra, so we refuse to emit for source that
721
- * does not pass lint. Non-zero on discovery errors or a layout-engine failure.
756
+ * does not pass lint, unless the project's config sets `graph.lint: "report"`
757
+ * (#919): then the graph is emitted and the findings ride on `meta.lint`.
758
+ * Non-zero on discovery errors or a layout-engine failure.
722
759
  */
723
760
  /**
724
761
  * Add what the lexicons' `graphMeta` hooks report to `ir.meta` (#2559). A
@@ -741,26 +778,6 @@ function withGraphMeta(ir: GraphIR, plugins: LexiconPlugin[], entities: Map<stri
741
778
  return Object.keys(added).length === 0 ? ir : { ...ir, meta: { ...(ir.meta ?? {}), ...added } };
742
779
  }
743
780
 
744
- /**
745
- * Add the edges the lexicons' `graphEdges` hooks resolve (#3910): CI
746
- * dependencies written as strings (`needs`, `uses:`, `trigger:`, `include:`,
747
- * `extends`), which the AttrRef walk in `buildGraphIr` cannot see. A hook that
748
- * throws costs its own edges and a warning, not the graph.
749
- */
750
- function withLexiconEdges(ir: GraphIR, plugins: LexiconPlugin[], entities: Map<string, import("../../declarable").Declarable>): GraphIR {
751
- const reports: LexiconGraphEdges[] = [];
752
- for (const p of plugins) {
753
- if (!p.graphEdges) continue;
754
- try {
755
- const report = p.graphEdges(entities);
756
- if (report) reports.push(report);
757
- } catch (err) {
758
- console.error(formatWarning({ message: `the ${p.name} lexicon's graphEdges failed (${err instanceof Error ? err.message : String(err)}); its edges are left out` }));
759
- }
760
- }
761
- return reports.length === 0 ? ir : applyLexiconGraphEdges(ir, reports);
762
- }
763
-
764
781
  /**
765
782
  * Add the outputs the lexicons' `workspaceOutputs` hooks report (#3931) to
766
783
  * the IR's `exports`: a sql table and each of its columns, which `chant
@@ -876,17 +893,16 @@ async function runGraphView(
876
893
  const buildParams = await graphBuildParams(ctx, projectPath);
877
894
  if (!buildParams) return 1;
878
895
 
879
- // Gate: only emit for lint-clean source.
896
+ // Gate: only emit for lint-clean source, unless the project reads source it
897
+ // did not author and asks for the findings instead (`graph.lint`, #919).
880
898
  const lint = await lintCommand({
881
899
  path: ctx.args.path,
882
900
  format: "stylish",
883
901
  sandbox: ctx.args.sandbox,
884
902
  buildParams,
885
903
  });
886
- if (!lint.success) {
887
- refuseLintGate(lint, projectPath);
888
- return 1;
889
- }
904
+ const lintMode = await graphLintMode(projectPath);
905
+ if (!passLintGate(lint, projectPath, lintMode)) return 1;
890
906
 
891
907
  const result = await discover(projectPath, { buildParams });
892
908
  if (result.errors.length > 0) {
@@ -913,10 +929,10 @@ async function runGraphView(
913
929
  // The declared half of the epic's delta (#2355): this path predicts the
914
930
  // file, the live path predicts the account. Same helper, so the two shapes
915
931
  // reach the engine by one route and stay comparable.
932
+ const plugins = ctx.plugins.length > 0
933
+ ? ctx.plugins
934
+ : await loadPlugins(await projectLexiconsOrNone(projectPath));
916
935
  {
917
- const plugins = ctx.plugins.length > 0
918
- ? ctx.plugins
919
- : await loadPlugins(await projectLexiconsOrNone(projectPath));
920
936
  ir = withLexiconEdges(ir, plugins as LexiconPlugin[], result.entities);
921
937
  ir = withLexiconOutputs(ir, plugins as LexiconPlugin[], result.entities);
922
938
  const predicted = await predictOntoIr(ir, plugins as LexiconPlugin[], ctx.args, {
@@ -951,6 +967,17 @@ async function runGraphView(
951
967
  if (notice) console.error(formatWarning({ message: notice }));
952
968
  }
953
969
  }
970
+ // Last, so no lens or detail pass can drop it: the findings are about the
971
+ // whole source read, not about the nodes this view kept.
972
+ if (lintMode === "report") ir = { ...ir, meta: { ...ir.meta, lint: graphLintReport(lint, projectPath) } };
973
+ // The view capability rules read (#4378), mapped by this project's own
974
+ // lexicons, so a workspace's rules read each member as its chant maps it.
975
+ if (ctx.args.capabilityView) {
976
+ const { buildView, serializeView } = await import("../../capabilities/view");
977
+ const { resolvePacks } = await import("../../capabilities/catalog");
978
+ const packs = (plugins as LexiconPlugin[]).map((p) => p.capabilityPack).filter((p): p is NonNullable<typeof p> => !!p);
979
+ ir = { ...ir, capabilityView: serializeView(buildView(ir, resolvePacks(packs))) };
980
+ }
954
981
  return emitIr(ir, ctx, format);
955
982
  }
956
983