@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
@@ -51,6 +51,18 @@ export const REASONS = {
51
51
  "command-failed": "The member's own command exited with a failure, or the lexicon that reads the member through its kind's graph block isn't installed where the workspace resolves its pin.",
52
52
  "output-unreadable": "The member's command printed something that isn't the document asked for.",
53
53
  "ir-version-unsupported": "The member's IR has a version this chant can't read.",
54
+ // search and workspace search (#4374): why no answer was printed, and why a member has none.
55
+ "query-invalid": "search was given no query, a term its grammar can't parse, or a --count-by that is not a grouping.",
56
+ "usage-invalid": "search was given a flag that needs another one it lacks, or two that don't go together, such as --check-live without --snapshot.",
57
+ "environment-unknown": "--env names an environment the project's chant config does not declare.",
58
+ "snapshot-unavailable": "--snapshot names a snapshot chant can't replay: none is recorded for the environment, or the ref is one replay does not take.",
59
+ "build-failed": "The project's build, or one of its build roots, failed, so the graph search would answer over is incomplete.",
60
+ "snapshot-at-revision": "workspace search was given --snapshot and --at: a snapshot replays the estate as recorded over the source in the working tree, not at a revision.",
61
+ "member-chant-too-old": "The member's chant predates the document asked for: it has no such command, or prints it without the read contract.",
62
+ // the capabilities catalog (#4375): why a lexicon can't supply a capability.
63
+ "lexicon-has-no-mapping": "The lexicon ships no capability mapping, so no capability rule can read its resources.",
64
+ "capability-not-derivable": "The lexicon's capability mapping labels none of the kinds the capability's rule needs.",
65
+ "lexicon-not-loaded": "The lexicon isn't installed where the chant that wrote the catalog runs, so its mapping wasn't read.",
54
66
  // A ledger status can't fully read.
55
67
  "ledger-unreadable": "Reading the ledger failed, so nothing from it is listed.",
56
68
  "ledger-malformed": "Some lines of the ledger aren't release records; the rest are listed.",
@@ -0,0 +1,305 @@
1
+ /**
2
+ * `chant workspace search` (#4374): `chant search` in every member, under the
3
+ * member's own chant, composed into one `search` document of the read
4
+ * contract with the ids `chant workspace graph` gives the same nodes.
5
+ *
6
+ * The workspace here has two chant members, `east` and `west`, each a copy of
7
+ * the reader conformance fixture's `delivery` member, and both run this
8
+ * checkout's real chant. A member that runs a fake chant shows the reason
9
+ * codes for a chant too old to print the document and for a read that fails.
10
+ */
11
+
12
+ import { cpSync, mkdirSync, readdirSync, readFileSync, symlinkSync, writeFileSync } from "node:fs";
13
+ import { join, resolve } from "node:path";
14
+ import { pathToFileURL } from "node:url";
15
+ import { realpathSync } from "node:fs";
16
+ import { afterAll, describe, expect, test } from "vitest";
17
+ import { writeSnapshot } from "../lifecycle/git";
18
+ import { cleanScratch, commitAll, contract, FAKE_GRAPH_CHANT, git, REPO, scratchDir } from "./__fixtures__/contract-repo";
19
+ import { CONFORMANCE_FIXTURE_DIR } from "./conformance";
20
+ import { workspaceGraph } from "./graph-cli";
21
+ import { readerBin, type Toolchain } from "./member-commands";
22
+ import { SEARCH_DOCUMENT_ERROR_CODES, SEARCH_MEMBER_REASON_CODES, SEARCH_OUTPUT_SCHEMA_ID } from "./search-contract";
23
+ import { workspaceSearch, type WorkspaceSearchDocument } from "./search-cli";
24
+ import schema from "./search.schema.json";
25
+
26
+ afterAll(cleanScratch);
27
+ // workspace graph's member cache, kept out of the user's cache directory.
28
+ process.env.CHANT_CACHE_DIR = scratchDir("chant-ws-search-cache-");
29
+
30
+ const TIMEOUT = 240_000;
31
+ const { expectValid } = contract(schema);
32
+
33
+ /** This checkout's chant, started the way the CLI starts it. */
34
+ const reader: Toolchain = {
35
+ command: [process.execPath, "--import", pathToFileURL(join(REPO, "node_modules", "tsx", "dist", "loader.mjs")).href, join(REPO, "packages", "core", "src", "cli", "main.ts")],
36
+ identity: realpathSync(readerBin()),
37
+ source: "reader",
38
+ };
39
+
40
+ /** A git repository with `east` and `west`, two chant members, and whatever else `extra` declares, committed. */
41
+ function twoMembers(extra: { name: string; dir: string; kind: string }[] = []): string {
42
+ const root = scratchDir("chant-ws-search-");
43
+ for (const m of ["east", "west"]) cpSync(join(CONFORMANCE_FIXTURE_DIR, "delivery"), join(root, m), { recursive: true });
44
+ mkdirSync(join(root, "node_modules", "@intentius"), { recursive: true });
45
+ symlinkSync(resolve(REPO, "packages", "core"), join(root, "node_modules", "@intentius", "chant"), "dir");
46
+ writeFileSync(join(root, ".gitignore"), "node_modules/\n");
47
+ writeFileSync(
48
+ join(root, "chant.workspace.json"),
49
+ JSON.stringify({ name: "two", schema: 1, members: [{ name: "east", dir: "east", kind: "chant" }, { name: "west", dir: "west", kind: "chant" }, ...extra] }, null, 2),
50
+ );
51
+ git(root, "init", "-q");
52
+ commitAll(root, "two members");
53
+ return root;
54
+ }
55
+
56
+ function result(doc: WorkspaceSearchDocument): Extract<WorkspaceSearchDocument, { matches: unknown }> {
57
+ if ("error" in doc) throw new Error(`${doc.error.code}: ${doc.error.message}`);
58
+ if (!("matches" in doc)) throw new Error("expected rows, got groups");
59
+ return doc;
60
+ }
61
+
62
+ const search = (cwd: string, query: string, extra: Partial<Parameters<typeof workspaceSearch>[0]> = {}) => workspaceSearch({ cwd, query, reader, ...extra });
63
+
64
+ describe("the search schema (#4374)", () => {
65
+ test("lists exactly the codes the code can return", () => {
66
+ expect(schema.$id).toBe(SEARCH_OUTPUT_SCHEMA_ID);
67
+ expect(schema.$defs.failure.properties.error.properties.code.enum).toEqual(SEARCH_DOCUMENT_ERROR_CODES);
68
+ expect(schema.$defs.member.properties.reason.oneOf[1].properties!.code.enum).toEqual([...SEARCH_MEMBER_REASON_CODES]);
69
+ });
70
+ });
71
+
72
+ describe("chant workspace search on two members", () => {
73
+ test("ids are the <member>/<id> ids chant workspace graph prints for the same nodes", async () => {
74
+ const root = twoMembers();
75
+ const [found, graph] = await Promise.all([search(root, "kind:Fixture::Service"), workspaceGraph({ cwd: root, reader })]);
76
+ const doc = result(found.doc);
77
+ expectValid(doc);
78
+ expect(found.failed).toBe(false);
79
+ if ("error" in graph.doc) throw new Error(graph.doc.error.message);
80
+ const graphIds = new Set(graph.doc.nodes.filter((n) => n.kind === "Fixture::Service").map((n) => n.id));
81
+ expect(doc.matches.map((m) => m.id)).toEqual([...graphIds].sort());
82
+ expect(doc.matches.map((m) => m.id)).toEqual(["east/appService", "west/appService"]);
83
+ expect(doc.matches.map((m) => m.member)).toEqual(["east", "west"]);
84
+ // Grouped by member, each searched alone, stamped by the graph cache's rule.
85
+ expect(doc.members.map((m) => [m.name, m.status, m.alone, m.matches])).toEqual([
86
+ ["east", "searched", true, ["east/appService"]],
87
+ ["west", "searched", true, ["west/appService"]],
88
+ ]);
89
+ const graphStamps = Object.fromEntries(graph.doc.members.map((m) => [m.name, m.stamp]));
90
+ for (const m of doc.members) expect(m.stamp, m.name).toBe(graphStamps[m.name]);
91
+ expect(doc).toMatchObject({ contract: 1, workspace: { name: "two", root: "." }, at: null, source: { kind: "declared" } });
92
+ }, TIMEOUT);
93
+
94
+ test("--count-by kind adds the members' groups up, and validates", async () => {
95
+ const root = twoMembers();
96
+ const { doc } = await search(root, "", { flags: { countBy: "kind" } });
97
+ expectValid(doc);
98
+ expect(doc).toMatchObject({ countBy: "kind", total: 2 });
99
+ if (!("groups" in doc)) throw new Error("no groups");
100
+ const service = doc.groups.find((g) => g.group === "Fixture::Service");
101
+ expect(service).toMatchObject({ count: 2 });
102
+ // #4382: each group's ids, as `<member>/<id>`, one per member here.
103
+ expect(service?.ids).toHaveLength(2);
104
+ expect(service?.ids?.map((id) => id.split("/")[0]).sort()).toEqual(doc.members.map((m) => m.name).sort());
105
+ expect(doc.members.map((m) => m.total)).toEqual([1, 1]);
106
+ }, TIMEOUT);
107
+
108
+ test("a refused query is a failure document with a code, before any member runs", async () => {
109
+ const root = twoMembers();
110
+ const { doc, failed } = await search(root, "effectiveIngress:tcp:22");
111
+ expectValid(doc);
112
+ expect(failed).toBe(true);
113
+ expect(doc).toMatchObject({ error: { code: "query-invalid" } });
114
+ }, TIMEOUT);
115
+ });
116
+
117
+ describe("two times: --at is the git revision, --snapshot the recorded estate", () => {
118
+ test("--at <rev> searches the source at that commit; --snapshot latest replays the snapshot; the two are not confused", async () => {
119
+ const root = twoMembers();
120
+ const head = git(root, "rev-parse", "HEAD");
121
+ // The working tree renames east's app; the commit still has the old name.
122
+ const app = join(root, "east", "src", "app.ts");
123
+ writeFileSync(app, readFileSync(app, "utf-8").replace("export const app =", "export const site ="));
124
+
125
+ const now = result((await search(root, "kind:Fixture::Service", { members: ["east"] })).doc);
126
+ expect(now.matches.map((m) => m.id)).toEqual(["east/siteService"]);
127
+ expect(now.at).toBeNull();
128
+
129
+ const then = result((await search(root, "kind:Fixture::Service", { members: ["east"], at: head })).doc);
130
+ expectValid(then);
131
+ expect(then.at).toBe(head);
132
+ expect(then.source).toEqual({ kind: "declared" });
133
+ expect(then.matches.map((m) => m.id)).toEqual(["east/appService"]);
134
+
135
+ // A snapshot recorded for east only: east replays it, west has none and says so.
136
+ await writeSnapshot(
137
+ "dev",
138
+ "fixture",
139
+ JSON.stringify({ lexicon: "fixture", environment: "dev", commit: "c0ffee", timestamp: "2026-10-23T00:00:00.000Z", resources: { siteService: { type: "Fixture::Service", physicalId: "svc-east", status: "OK" } } }),
140
+ { cwd: join(root, "east") },
141
+ );
142
+ const replayed = await search(root, "kind:Fixture::Service", { flags: { snapshot: "latest", env: "dev" } });
143
+ const snap = result(replayed.doc);
144
+ expectValid(snap);
145
+ expect(snap.at).toBeNull();
146
+ expect(snap.source).toEqual({ kind: "snapshot", env: "dev", snapshot: "latest" });
147
+ expect(snap.matches).toEqual([expect.objectContaining({ id: "east/siteService", physicalId: "svc-east" })]);
148
+ expect(snap.members[0]).toMatchObject({ name: "east", status: "searched", source: { kind: "snapshot", snapshot: "latest", commit: "c0ffee" } });
149
+ expect(snap.members[1]).toMatchObject({ name: "west", status: "failed", reason: { code: "snapshot-unavailable" } });
150
+ expect(replayed.failed).toBe(true);
151
+
152
+ // --at given a snapshot ref names no commit, and the hint names --snapshot.
153
+ const misused = await search(root, "kind:Fixture::Service", { at: "latest" });
154
+ expectValid(misused.doc);
155
+ expect(misused.doc).toMatchObject({ error: { code: "revision-unknown", hint: expect.stringContaining("--snapshot latest") } });
156
+ // Both at once is refused.
157
+ const both = await search(root, "kind:Fixture::Service", { at: head, flags: { snapshot: "latest", env: "dev" } });
158
+ expectValid(both.doc);
159
+ expect(both.doc).toMatchObject({ error: { code: "snapshot-at-revision" } });
160
+ }, TIMEOUT);
161
+ });
162
+
163
+ describe("a member that can't be searched has a reason code, never zero matches", () => {
164
+ test("a member whose chant is too old: member-chant-too-old", async () => {
165
+ const root = twoMembers([{ name: "old", dir: "old", kind: "chant" }]);
166
+ mkdirSync(join(root, "old", "node_modules", ".bin"), { recursive: true });
167
+ writeFileSync(join(root, "old", "chant.config.ts"), "export default {};\n");
168
+ writeFileSync(join(root, "old", "ids.txt"), "Thing\n");
169
+ writeFileSync(join(root, "old", "node_modules", ".bin", "chant"), FAKE_GRAPH_CHANT, { mode: 0o755 });
170
+ const { doc, failed } = await search(root, "kind:Fixture::Service");
171
+ expectValid(doc);
172
+ expect(failed).toBe(true);
173
+ const r = result(doc);
174
+ expect(r.members.find((m) => m.name === "old")).toMatchObject({ status: "failed", chant: null, reason: { code: "member-chant-too-old" } });
175
+ expect(r.members.find((m) => m.name === "old")).not.toHaveProperty("matches");
176
+ expect(r.matches.map((m) => m.id)).toEqual(["east/appService", "west/appService"]);
177
+ }, TIMEOUT);
178
+
179
+ test("a member whose read fails: command-failed, and a missing directory: dir-missing", async () => {
180
+ const root = twoMembers([{ name: "gone", dir: "gone", kind: "chant" }]);
181
+ writeFileSync(join(root, "west", "chant.config.ts"), 'throw new Error("this config does not load");\n');
182
+ const { doc, failed } = await search(root, "kind:Fixture::Service");
183
+ expectValid(doc);
184
+ expect(failed).toBe(true);
185
+ const r = result(doc);
186
+ expect(r.members.find((m) => m.name === "west")).toMatchObject({ status: "failed", reason: { code: "command-failed" } });
187
+ expect(r.members.find((m) => m.name === "gone")).toMatchObject({ status: "failed", reason: { code: "dir-missing" } });
188
+ expect(r.matches.map((m) => m.id)).toEqual(["east/appService"]);
189
+ }, TIMEOUT);
190
+ });
191
+
192
+ // #4380: a member of kind terraform is searched through the reader project
193
+ // `workspace graph` writes for it (#2874), so its rows carry the ids the graph
194
+ // gives the same nodes: `<member>/<root>/<address>`, the root named for the member.
195
+ describe("terraform members (#4380)", () => {
196
+ const LEXICON = "@intentius/chant-lexicon-terraform";
197
+ const VERSION = (JSON.parse(readFileSync(join(REPO, "lexicons", "terraform", "package.json"), "utf-8")) as { version: string }).version;
198
+ const NET_TF = 'resource "aws_vpc" "main" {\n cidr_block = "10.0.0.0/16"\n}\n\nresource "aws_subnet" "b" {\n vpc_id = aws_vpc.main.id\n cidr_block = "10.0.2.0/24"\n}\n\nresource "aws_s3_bucket" "logs" {\n bucket = "logs"\n}\n\nmodule "subnets" {\n source = "../modules/subnets"\n vpc_id = aws_vpc.main.id\n}\n';
199
+ const SUBNETS_TF = 'variable "vpc_id" {}\n\nresource "aws_subnet" "a" {\n vpc_id = var.vpc_id\n cidr_block = "10.0.1.0/24"\n}\n';
200
+
201
+ /** A committed workspace with chant member `east` and terraform member `net`, the lexicon pinned at `version`. */
202
+ function mixed(version = VERSION): string {
203
+ const root = scratchDir("chant-ws-search-tf-");
204
+ cpSync(join(CONFORMANCE_FIXTURE_DIR, "delivery"), join(root, "east"), { recursive: true });
205
+ mkdirSync(join(root, "estates", "net"), { recursive: true });
206
+ mkdirSync(join(root, "estates", "modules", "subnets"), { recursive: true });
207
+ writeFileSync(join(root, "estates", "net", "main.tf"), NET_TF);
208
+ writeFileSync(join(root, "estates", "modules", "subnets", "main.tf"), SUBNETS_TF);
209
+ // This checkout's node_modules: @intentius/chant for east, the lexicon for net's reader project.
210
+ symlinkSync(join(REPO, "node_modules"), join(root, "node_modules"), "dir");
211
+ writeFileSync(join(root, ".gitignore"), "node_modules\n");
212
+ writeFileSync(
213
+ join(root, "chant.workspace.json"),
214
+ JSON.stringify({ name: "mixed", schema: 1, pins: [{ package: LEXICON, version }], members: [{ name: "east", dir: "east", kind: "chant" }, { name: "net", dir: "estates/net", kind: "terraform" }] }, null, 2),
215
+ );
216
+ git(root, "init", "-q");
217
+ commitAll(root, "mixed");
218
+ return root;
219
+ }
220
+
221
+ test("a terraform and a chant member: rows, hops and capability matches carry workspace graph's ids", async () => {
222
+ const root = mixed();
223
+ const [found, graph] = await Promise.all([search(root, "kind:aws"), workspaceGraph({ cwd: root, reader })]);
224
+ const doc = result(found.doc);
225
+ expectValid(doc);
226
+ expect(found.failed).toBe(false);
227
+ if ("error" in graph.doc) throw new Error(graph.doc.error.message);
228
+ expect(doc.members.map((m) => [m.name, m.kind, m.status, m.reason])).toEqual([
229
+ ["east", "chant", "searched", null],
230
+ ["net", "terraform", "searched", null],
231
+ ]);
232
+ expect(doc.matches.map((m) => m.id)).toEqual(["net/net/aws_s3_bucket.logs", "net/net/aws_subnet.b", "net/net/aws_vpc.main", "net/net/module.subnets/aws_subnet.a"]);
233
+ expect(doc.members.find((m) => m.name === "east")!.matches).toEqual([]);
234
+ const graphIds = new Set(graph.doc.nodes.map((n) => n.id));
235
+ for (const m of doc.matches) expect(graphIds, m.id).toContain(m.id);
236
+ // The member's stamp is the one workspace graph gives it.
237
+ expect(doc.members.find((m) => m.name === "net")!.stamp).toBe(graph.doc.members.find((m) => m.name === "net")!.stamp);
238
+
239
+ // A path's hops are prefixed too, and each end is a node of the graph.
240
+ const walked = result((await search(root, "kind:aws_subnet ->kind:aws_vpc")).doc);
241
+ expectValid(walked);
242
+ expect(walked.matches.map((m) => m.id)).toEqual(["net/net/aws_subnet.b"]);
243
+ const hops = walked.matches[0].paths.flatMap((p) => p.hops);
244
+ expect(hops.length).toBeGreaterThan(0);
245
+ for (const h of hops) {
246
+ expect(graphIds, h.from).toContain(h.from);
247
+ expect(graphIds, h.to).toContain(h.to);
248
+ }
249
+ expect(hops.at(-1)!.to).toBe("net/net/aws_vpc.main");
250
+
251
+ // A capability term (#4375) maps the terraform member's nodes as well.
252
+ const caps = result((await search(root, "cap:stores-data")).doc);
253
+ expect(caps.matches.map((m) => m.id)).toEqual(["net/net/aws_s3_bucket.logs"]);
254
+ expect(caps.members.find((m) => m.name === "net")).toMatchObject({ status: "searched", matches: ["net/net/aws_s3_bucket.logs"] });
255
+ }, TIMEOUT);
256
+
257
+ test("a pin at another version than the installed lexicon: unknown-kind naming the pin, in search and graph, never kind-not-run", async () => {
258
+ const root = mixed("0.0.1");
259
+ const [found, graph] = await Promise.all([search(root, "kind:aws_vpc"), workspaceGraph({ cwd: root, reader })]);
260
+ const doc = result(found.doc);
261
+ expectValid(doc);
262
+ expect(found.failed).toBe(true);
263
+ const net = doc.members.find((m) => m.name === "net")!;
264
+ expect(net).toMatchObject({ status: "failed", reason: { code: "unknown-kind" } });
265
+ expect(net.reason!.message).toContain(`${LEXICON} is pinned at 0.0.1, and ${VERSION} is installed`);
266
+ expect(net).not.toHaveProperty("matches");
267
+ if ("error" in graph.doc) throw new Error(graph.doc.error.message);
268
+ expect(graph.doc.members.find((m) => m.name === "net")).toMatchObject({ status: "failed", reason: { code: "unknown-kind" } });
269
+ expect(graph.failed).toBe(true);
270
+ }, TIMEOUT);
271
+
272
+ // The shape behold's scratch project takes for a bare Terraform folder
273
+ // (behold#925): a directory outside the folder, not a git repository, with
274
+ // the declaration, a node_modules link, and a link to the folder as the one
275
+ // member's directory. Nothing is written in the folder.
276
+ test("a one-member workspace whose member is a bare Terraform folder, linked in: ids equal workspace graph's, the folder untouched", async () => {
277
+ const estate = scratchDir("chant-ws-search-bare-");
278
+ writeFileSync(join(estate, "main.tf"), 'resource "aws_vpc" "main" {\n cidr_block = "10.0.0.0/16"\n}\n\nresource "aws_s3_bucket" "logs" {\n bucket = "logs"\n}\n\nmodule "subnets" {\n source = "./modules/subnets"\n vpc_id = aws_vpc.main.id\n}\n');
279
+ mkdirSync(join(estate, "modules", "subnets"), { recursive: true });
280
+ writeFileSync(join(estate, "modules", "subnets", "main.tf"), SUBNETS_TF);
281
+ const before = readdirSync(estate, { recursive: true }).sort();
282
+
283
+ const project = scratchDir("chant-ws-search-scratch-");
284
+ symlinkSync(join(REPO, "node_modules"), join(project, "node_modules"), "dir");
285
+ symlinkSync(estate, join(project, "estate"), "dir");
286
+ writeFileSync(
287
+ join(project, "chant.workspace.json"),
288
+ JSON.stringify({ name: "estate", schema: 1, pins: [{ package: LEXICON, version: VERSION }], members: [{ name: "estate", dir: "estate", kind: "terraform" }] }, null, 2),
289
+ );
290
+
291
+ const [found, graph] = await Promise.all([search(project, "kind:aws"), workspaceGraph({ cwd: project, reader })]);
292
+ const doc = result(found.doc);
293
+ expectValid(doc);
294
+ expect(found.failed).toBe(false);
295
+ if ("error" in graph.doc) throw new Error(graph.doc.error.message);
296
+ expect(doc.at).toBeNull();
297
+ expect(doc.members.map((m) => [m.name, m.kind, m.status])).toEqual([["estate", "terraform", "searched"]]);
298
+ expect(doc.matches.map((m) => m.id)).toEqual(["estate/estate/aws_s3_bucket.logs", "estate/estate/aws_vpc.main", "estate/estate/module.subnets/aws_subnet.a"]);
299
+ const graphIds = new Set(graph.doc.nodes.map((n) => n.id));
300
+ for (const m of doc.matches) expect(graphIds, m.id).toContain(m.id);
301
+ const caps = result((await search(project, "cap:stores-data")).doc);
302
+ expect(caps.matches.map((m) => m.id)).toEqual(["estate/estate/aws_s3_bucket.logs"]);
303
+ expect(readdirSync(estate, { recursive: true }).sort()).toEqual(before);
304
+ }, TIMEOUT);
305
+ });