@intentius/chant 0.124.45 → 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 (190) 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/cli/commands/lint.d.ts.map +1 -1
  16. package/dist/cli/commands/lint.js +1 -0
  17. package/dist/cli/handlers/declared-graph.d.ts +44 -0
  18. package/dist/cli/handlers/declared-graph.d.ts.map +1 -0
  19. package/dist/cli/handlers/declared-graph.js +87 -0
  20. package/dist/cli/handlers/graph.d.ts +25 -0
  21. package/dist/cli/handlers/graph.d.ts.map +1 -1
  22. package/dist/cli/handlers/graph.js +93 -87
  23. package/dist/cli/handlers/pr-env.d.ts.map +1 -1
  24. package/dist/cli/handlers/pr-env.js +2 -1
  25. package/dist/cli/handlers/search-count.d.ts +89 -0
  26. package/dist/cli/handlers/search-count.d.ts.map +1 -0
  27. package/dist/cli/handlers/search-count.js +132 -0
  28. package/dist/cli/handlers/search.d.ts +358 -6
  29. package/dist/cli/handlers/search.d.ts.map +1 -1
  30. package/dist/cli/handlers/search.js +1065 -155
  31. package/dist/cli/main.d.ts.map +1 -1
  32. package/dist/cli/main.js +23 -3
  33. package/dist/cli/mcp/server.d.ts.map +1 -1
  34. package/dist/cli/mcp/server.js +3 -0
  35. package/dist/cli/mcp/tools/capabilities.d.ts +7 -0
  36. package/dist/cli/mcp/tools/capabilities.d.ts.map +1 -0
  37. package/dist/cli/mcp/tools/capabilities.js +27 -0
  38. package/dist/cli/mcp/workspace-tools.d.ts +3 -1
  39. package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
  40. package/dist/cli/mcp/workspace-tools.js +47 -2
  41. package/dist/cli/parse-args.d.ts.map +1 -1
  42. package/dist/cli/parse-args.js +64 -17
  43. package/dist/cli/registry.d.ts +22 -0
  44. package/dist/cli/registry.d.ts.map +1 -1
  45. package/dist/components/pr-env-ports.d.ts +29 -0
  46. package/dist/components/pr-env-ports.d.ts.map +1 -0
  47. package/dist/components/pr-env-ports.js +77 -0
  48. package/dist/components/pr-env-run.d.ts +5 -21
  49. package/dist/components/pr-env-run.d.ts.map +1 -1
  50. package/dist/components/pr-env-run.js +6 -70
  51. package/dist/components/pr-environment.d.ts +2 -1
  52. package/dist/components/pr-environment.d.ts.map +1 -1
  53. package/dist/components/pr-environment.js +1 -1
  54. package/dist/config-environments.d.ts +92 -0
  55. package/dist/config-environments.d.ts.map +1 -0
  56. package/dist/config-environments.js +102 -0
  57. package/dist/config.d.ts +24 -84
  58. package/dist/config.d.ts.map +1 -1
  59. package/dist/config.js +9 -96
  60. package/dist/graph-detail.d.ts.map +1 -1
  61. package/dist/graph-detail.js +15 -3
  62. package/dist/graph-effective.d.ts +33 -2
  63. package/dist/graph-effective.d.ts.map +1 -1
  64. package/dist/graph-effective.js +215 -68
  65. package/dist/graph-ir.d.ts +23 -0
  66. package/dist/graph-ir.d.ts.map +1 -1
  67. package/dist/graph-ir.js +130 -0
  68. package/dist/graph-sub-template.d.ts +11 -0
  69. package/dist/graph-sub-template.d.ts.map +1 -0
  70. package/dist/graph-sub-template.js +19 -0
  71. package/dist/lexicon.d.ts +9 -0
  72. package/dist/lexicon.d.ts.map +1 -1
  73. package/dist/lifecycle/observe.d.ts +28 -0
  74. package/dist/lifecycle/observe.d.ts.map +1 -1
  75. package/dist/lifecycle/observe.js +23 -1
  76. package/dist/lint/rule.d.ts +6 -0
  77. package/dist/lint/rule.d.ts.map +1 -1
  78. package/dist/live-endpoint.d.ts +1 -1
  79. package/dist/live-endpoint.d.ts.map +1 -1
  80. package/dist/live-endpoint.js +1 -1
  81. package/dist/observation.d.ts +48 -1
  82. package/dist/observation.d.ts.map +1 -1
  83. package/dist/observation.js +21 -5
  84. package/dist/op/activities/activity-contracts.d.ts +4 -4
  85. package/dist/wave-runtime.mjs +250 -249
  86. package/dist/workspace/capabilities-contract.d.ts +66 -0
  87. package/dist/workspace/capabilities-contract.d.ts.map +1 -0
  88. package/dist/workspace/capabilities-contract.js +107 -0
  89. package/dist/workspace/capabilities.schema.json +229 -0
  90. package/dist/workspace/compose-graph.d.ts +9 -0
  91. package/dist/workspace/compose-graph.d.ts.map +1 -1
  92. package/dist/workspace/compose-graph.js +3 -1
  93. package/dist/workspace/conformance/index.d.ts +1 -1
  94. package/dist/workspace/conformance/index.d.ts.map +1 -1
  95. package/dist/workspace/conformance/index.js +39 -2
  96. package/dist/workspace/graph-cache.d.ts +8 -0
  97. package/dist/workspace/graph-cache.d.ts.map +1 -1
  98. package/dist/workspace/graph-cache.js +27 -3
  99. package/dist/workspace/graph-cli.d.ts +39 -2
  100. package/dist/workspace/graph-cli.d.ts.map +1 -1
  101. package/dist/workspace/graph-cli.js +66 -24
  102. package/dist/workspace/graph.schema.json +148 -0
  103. package/dist/workspace/kind-readers.d.ts +3 -0
  104. package/dist/workspace/kind-readers.d.ts.map +1 -1
  105. package/dist/workspace/kind-readers.js +7 -1
  106. package/dist/workspace/member-commands.d.ts +10 -1
  107. package/dist/workspace/member-commands.d.ts.map +1 -1
  108. package/dist/workspace/member-commands.js +17 -1
  109. package/dist/workspace/nested-graph.d.ts +2 -0
  110. package/dist/workspace/nested-graph.d.ts.map +1 -1
  111. package/dist/workspace/nested-graph.js +32 -1
  112. package/dist/workspace/reason-codes.d.ts +10 -0
  113. package/dist/workspace/reason-codes.d.ts.map +1 -1
  114. package/dist/workspace/reason-codes.js +12 -0
  115. package/dist/workspace/search-cli.d.ts +204 -0
  116. package/dist/workspace/search-cli.d.ts.map +1 -0
  117. package/dist/workspace/search-cli.js +466 -0
  118. package/dist/workspace/search-contract.d.ts +56 -0
  119. package/dist/workspace/search-contract.d.ts.map +1 -0
  120. package/dist/workspace/search-contract.js +70 -0
  121. package/dist/workspace/search.schema.json +822 -0
  122. package/package.json +1 -1
  123. package/src/__snapshots__/okf.test.ts.snap +10 -0
  124. package/src/capabilities/aws.paths +148 -0
  125. package/src/capabilities/base-mappings.json +122 -0
  126. package/src/capabilities/catalog.ts +230 -0
  127. package/src/capabilities/language.test.ts +191 -0
  128. package/src/capabilities/language.ts +373 -0
  129. package/src/capabilities/matcher.ts +315 -0
  130. package/src/capabilities/view-compose.test.ts +76 -0
  131. package/src/capabilities/view.ts +631 -0
  132. package/src/cli/commands/lint.ts +1 -0
  133. package/src/cli/handlers/declared-graph.ts +99 -0
  134. package/src/cli/handlers/graph.test.ts +94 -3
  135. package/src/cli/handlers/graph.ts +123 -96
  136. package/src/cli/handlers/pr-env.ts +1 -2
  137. package/src/cli/handlers/search-build-roots.test.ts +144 -0
  138. package/src/cli/handlers/search-count.test.ts +191 -0
  139. package/src/cli/handlers/search-count.ts +165 -0
  140. package/src/cli/handlers/search-json.test.ts +333 -0
  141. package/src/cli/handlers/search-live.test.ts +387 -0
  142. package/src/cli/handlers/search-paths.test.ts +235 -0
  143. package/src/cli/handlers/search.ts +1312 -166
  144. package/src/cli/main.test.ts +28 -3
  145. package/src/cli/main.ts +23 -3
  146. package/src/cli/mcp/server.test.ts +3 -3
  147. package/src/cli/mcp/server.ts +3 -0
  148. package/src/cli/mcp/tools/capabilities.ts +32 -0
  149. package/src/cli/mcp/workspace-tools.e2e.test.ts +21 -1
  150. package/src/cli/mcp/workspace-tools.ts +47 -2
  151. package/src/cli/parse-args.ts +52 -13
  152. package/src/cli/registry.ts +22 -0
  153. package/src/components/pr-env-ports.ts +89 -0
  154. package/src/components/pr-env-run-bundle.test.ts +54 -0
  155. package/src/components/pr-env-run.ts +5 -76
  156. package/src/components/pr-environment.ts +2 -1
  157. package/src/config-environments.ts +133 -0
  158. package/src/config.ts +37 -124
  159. package/src/graph-detail.ts +15 -3
  160. package/src/graph-effective.ts +226 -75
  161. package/src/graph-ir.test.ts +163 -0
  162. package/src/graph-ir.ts +147 -0
  163. package/src/graph-sub-template.ts +22 -0
  164. package/src/lexicon.ts +10 -0
  165. package/src/lifecycle/observe.test.ts +27 -1
  166. package/src/lifecycle/observe.ts +43 -1
  167. package/src/lint/rule.ts +6 -0
  168. package/src/live-endpoint.ts +1 -1
  169. package/src/observation.test.ts +25 -0
  170. package/src/observation.ts +69 -5
  171. package/src/workspace/capabilities-contract.test.ts +88 -0
  172. package/src/workspace/capabilities-contract.ts +132 -0
  173. package/src/workspace/capabilities.schema.json +229 -0
  174. package/src/workspace/compose-graph.ts +10 -1
  175. package/src/workspace/conformance/conformance.e2e.test.ts +14 -1
  176. package/src/workspace/conformance/index.ts +31 -2
  177. package/src/workspace/graph-cache.ts +27 -3
  178. package/src/workspace/graph-cli.ts +96 -24
  179. package/src/workspace/graph.schema.json +148 -0
  180. package/src/workspace/kind-readers.e2e.test.ts +28 -0
  181. package/src/workspace/kind-readers.test.ts +1 -1
  182. package/src/workspace/kind-readers.ts +7 -1
  183. package/src/workspace/member-commands.ts +22 -1
  184. package/src/workspace/nested-graph.ts +32 -1
  185. package/src/workspace/reason-codes.test.ts +7 -0
  186. package/src/workspace/reason-codes.ts +12 -0
  187. package/src/workspace/search-cli.e2e.test.ts +305 -0
  188. package/src/workspace/search-cli.ts +591 -0
  189. package/src/workspace/search-contract.ts +95 -0
  190. package/src/workspace/search.schema.json +822 -0
@@ -208,9 +208,33 @@ describe("parseArgs", () => {
208
208
  expect(() => parseArgs(["build", "--unknown=value"])).toThrow(/Unknown flag: --unknown/);
209
209
  });
210
210
 
211
- test("unknown short flags are still silently ignored (unchanged; out of #1127 scope)", () => {
212
- const result = parseArgs(["build", "-x", "value"]);
213
- expect(result.command).toBe("build");
211
+ test("an unknown short flag is refused like an unknown long one (#4369)", () => {
212
+ // Was "unknown short flags are still silently ignored": `-x` vanished and
213
+ // the next token became the path. #4369 refuses it with the #1127 message.
214
+ expect(() => parseArgs(["build", "-x", "value"])).toThrow(/Unknown flag: -x\nRun "chant --help"/);
215
+ expect(() => parseArgs(["search", "kind:X", "-q"])).toThrow(/Unknown flag: -q/);
216
+ });
217
+
218
+ test("a search query that opens with -> is the positional, not a dropped flag (#4369)", () => {
219
+ expect(parseArgs(["search", "->kind:Bucket"]).path).toBe("->kind:Bucket");
220
+ const explained = parseArgs(["search", "->kind:Bucket", "--explain"]);
221
+ expect(explained.path).toBe("->kind:Bucket");
222
+ expect(explained.explain).toBe(true);
223
+ expect(parseArgs(["search", "--explain", "->*[via=subnet_id]kind:aws_vpc"]).path).toBe("->*[via=subnet_id]kind:aws_vpc");
224
+ // Unchanged: the in-edge spelling and a leading space always worked.
225
+ expect(parseArgs(["search", "<-kind:Bucket"]).path).toBe("<-kind:Bucket");
226
+ expect(parseArgs(["search", " ->kind:Bucket"]).path).toBe(" ->kind:Bucket");
227
+ // A known short flag still parses as one.
228
+ expect(parseArgs(["search", "->kind:Bucket", "-v"]).verbose).toBe(true);
229
+ });
230
+
231
+ test("-- ends flag parsing: what follows is positional, unsplit (#4369)", () => {
232
+ const r = parseArgs(["search", "--json", "--", "-kind:X", "--explain"]);
233
+ expect(r.path).toBe("-kind:X");
234
+ expect(r.extraPositional).toBe("--explain");
235
+ expect(r.explain).toBeUndefined();
236
+ expect(r.json).toBe(true);
237
+ expect(parseArgs(["search", "--", "--x=y"]).path).toBe("--x=y");
214
238
  });
215
239
 
216
240
  test("parses --watch flag", () => {
@@ -846,6 +870,7 @@ describe("workspace init and ls (#2534)", () => {
846
870
  "workspace points",
847
871
  "workspace records",
848
872
  "workspace runs",
873
+ "workspace search",
849
874
  "workspace signers",
850
875
  "workspace status",
851
876
  "workspace trust",
package/src/cli/main.ts CHANGED
@@ -170,9 +170,15 @@ Commands:
170
170
  bundle tree, otherwise JSON path→content on stdout)
171
171
  search "<query>" Answer an estate question with a few matching rows
172
172
  instead of a graph dump (kind:, tag:, attr:, ->/<- edge
173
- terms; --live or --at <ref> with --env <name> reads the
174
- estate or a recorded snapshot; --show <attr>,
175
- --check-live, --check-snapshot, --fail-on-drift)
173
+ terms, ->* / ->{1,3} for depth, ->[via=attr] / [to=attr]
174
+ by attribute; --live or --snapshot <ref> (also --at
175
+ <ref>) with --env <name> reads the estate or a
176
+ recorded snapshot; --show <attr>,
177
+ --count-by kind|module|attr:<name>, --explain prints
178
+ paths, --json, --check-live,
179
+ --check-snapshot, --fail-on-drift; cap:<name> and
180
+ ->[cap=<name>] ask with a capability, and
181
+ --capabilities prints the catalog)
176
182
  query "<sql>" [dir] Run one read-only SQL statement over chant/lifecycle:
177
183
  the resources and snapshots each lexicon observed, Op
178
184
  runs, gates, deploys, releases and cuts, loaded into
@@ -580,6 +586,9 @@ Ops:
580
586
  --layout-engine graphviz to use dot instead;
581
587
  --detail 0..3: stacks|composites|declarables|attributes;
582
588
  --lens lexicon:<n>|stack:<n>|blast:<node> (--up/--down))
589
+ --format ir --capability-view: add the lexicon-neutral
590
+ view capability rules read, as this project's lexicons
591
+ map it, to the IR as capabilityView (#4378)
583
592
  --components --format ir --projection gitlab|github|forgejo:
584
593
  add the CI/pipeline projection (stages/jobs/needs) to
585
594
  the component-graph IR, from the same generator
@@ -934,6 +943,15 @@ Workspace (level 1, #2524):
934
943
  source and toolchain are unchanged is served from
935
944
  the cache in $CHANT_CACHE_DIR or ~/.cache/chant;
936
945
  --no-cache reads every member
946
+ --capability-view: each member's chant adds its
947
+ capability view, mapped by its own lexicons
948
+ workspace search "<query>" [--member <name>] [--env <name>] [--snapshot <ref> | --live] [--at <rev>] [--explain] [--count-by ...] [--json]
949
+ chant search in every chant member, each under its own
950
+ chant, as one read-contract document: ids are
951
+ <member>/<id> as workspace graph prints them, each
952
+ member is searched alone, and a member that can't be
953
+ searched has a reason code. --at <rev> is the git
954
+ revision; --snapshot <ref> replays a recorded snapshot
937
955
  workspace graph --composites [--at <rev>] [--member <name>] [-o <file>]
938
956
  Each composite instance the members declare, with the
939
957
  components whose contract can deploy it; an instance
@@ -1610,6 +1628,8 @@ export const commandRegistry: CommandDef[] = [
1610
1628
  // chant#2618 — audit runs no project code, at the workspace level too.
1611
1629
  { name: "workspace audit", runsNoConfig: true, handler: async (ctx) => (await import("../workspace/member-commands")).runWorkspaceMembers(ctx, "audit") },
1612
1630
  { name: "workspace graph", handler: async (ctx) => (await import("../workspace/member-commands")).runWorkspaceMembers(ctx, "graph") },
1631
+ // #4374 — chant search in every member, under its own chant, composed with workspace graph's ids: the search document.
1632
+ { name: "workspace search", runsNoConfig: true, handler: async (ctx) => (await import("../workspace/search-cli")).runWorkspaceSearch(ctx) },
1613
1633
  // Each unit decides for itself whether its config is loaded (runCommandInProcess).
1614
1634
  { name: "workspace member-run", runsNoConfig: true, handler: async (ctx) => (await import("../workspace/member-run")).runWorkspaceMemberRun(ctx, runCommandInProcess) },
1615
1635
  // #3308 — a box's listing, written through chant so a tool never edits the declaration itself (ws-074).
@@ -1630,9 +1630,9 @@ describe("McpServer", () => {
1630
1630
 
1631
1631
  const toolsRes = await s.handleRequest({ jsonrpc: "2.0", id: 2, method: "tools/list" });
1632
1632
  const tools = (toolsRes.result as { tools: Array<{ name: string }> }).tools;
1633
- expect(tools).toHaveLength(14);
1633
+ expect(tools).toHaveLength(15);
1634
1634
  expect(tools.map((t) => t.name).sort()).toEqual([
1635
- "build", "composites", "explain", "import", "lifecycle-diff", "lifecycle-snapshot", "lint",
1635
+ "build", "capabilities", "composites", "explain", "import", "lifecycle-diff", "lifecycle-snapshot", "lint",
1636
1636
  "op-approve", "op-list", "op-report", "op-run", "op-status",
1637
1637
  "scaffold", "search",
1638
1638
  ]);
@@ -1646,7 +1646,7 @@ describe("McpServer", () => {
1646
1646
  const s = new McpServer([]);
1647
1647
  const toolsRes = await s.handleRequest({ jsonrpc: "2.0", id: 1, method: "tools/list" });
1648
1648
  const tools = (toolsRes.result as { tools: Array<{ name: string }> }).tools;
1649
- expect(tools).toHaveLength(14);
1649
+ expect(tools).toHaveLength(15);
1650
1650
  });
1651
1651
  });
1652
1652
  });
@@ -7,6 +7,7 @@ import { explainTool, handleExplain } from "./tools/explain";
7
7
  import { scaffoldTool, createScaffoldHandler } from "./tools/scaffold";
8
8
  import { searchTool, createSearchHandler } from "./tools/search";
9
9
  import { compositesTool, createCompositesHandler } from "./tools/composites";
10
+ import { capabilitiesTool, createCapabilitiesHandler } from "./tools/capabilities";
10
11
  import type { LexiconPlugin } from "../../lexicon";
11
12
  import type { McpClientInfo, McpRequest, McpResponse, McpRequestMeta, ToolDefinition, ToolHandler, ResourceDefinition } from "./types";
12
13
  import { findWorkspaceRoot } from "../../project-root";
@@ -126,6 +127,8 @@ export class McpServer {
126
127
  this.registerTool(scaffoldTool, createScaffoldHandler(plugins ?? []));
127
128
  this.registerTool(searchTool, createSearchHandler(plugins ?? []));
128
129
  this.registerTool(compositesTool, createCompositesHandler(plugins ?? []));
130
+ // The capability catalog (#4375), the read contract's capabilities document.
131
+ this.registerTool(capabilitiesTool, createCapabilitiesHandler(options.workspace?.cwd ?? process.cwd(), options.workspace?.chantCommand));
129
132
 
130
133
  // Register state tools
131
134
  const snapshot = createSnapshotTool(plugins ?? []);
@@ -0,0 +1,32 @@
1
+ /**
2
+ * The `capabilities` tool of `chant serve mcp` (#4375): the capability
3
+ * catalog `chant search` answers `cap:` terms with, as the read contract's
4
+ * `capabilities` document (capabilities.schema.json).
5
+ *
6
+ * Like the workspace reads, it runs the command and returns the document it
7
+ * printed, unchanged: `chant workspace search --capabilities --json` inside a
8
+ * declared workspace, where the workspace's chant runs the rules, and `chant
9
+ * search --capabilities --json` anywhere else. It is served everywhere, since
10
+ * a plain project's agent asks the same questions.
11
+ */
12
+ import { findWorkspaceRoot } from "../../../project-root";
13
+ import type { ToolDefinition, ToolHandler } from "../types";
14
+ import { ownChantCommand, runRead, type ChantCommand } from "../workspace-tools";
15
+
16
+ export const capabilitiesTool: ToolDefinition = {
17
+ name: "capabilities",
18
+ description:
19
+ "The capability catalog for chant search (capabilities.schema.json, #4375): each capability's name, meaning, parameters, the search term that asks for it (cap:<name> for a set, ->[cap=<name>] for an edge), the questions it answers, whether each lexicon can supply it, what it does not evaluate, and its rule. " +
20
+ "Read it once per session, then ask estate questions with those terms, such as cap:internet-reachable(22) or cap:stores-data <-[cap=reaches]<role>. Not the resource-type catalog: that is the search tool. " +
21
+ "Returns the document chant search --capabilities --json prints (chant workspace search --capabilities --json inside a declared workspace), unchanged.",
22
+ inputSchema: { type: "object", properties: {} },
23
+ };
24
+
25
+ /** The argv the tool runs in `cwd`: the workspace form inside a declared workspace. */
26
+ export function capabilitiesArgv(cwd: string): string[] {
27
+ return findWorkspaceRoot(cwd) ? ["workspace", "search", "--capabilities", "--json"] : ["search", "--capabilities", "--json"];
28
+ }
29
+
30
+ export function createCapabilitiesHandler(cwd: string, command: ChantCommand = ownChantCommand()): ToolHandler {
31
+ return async () => runRead(command, capabilitiesArgv(cwd), cwd);
32
+ }
@@ -85,7 +85,7 @@ describe("which servers have the workspace tools", () => {
85
85
  const { tools } = (await rpc(server(cwd), "tools/list")) as { tools: { name: string }[] };
86
86
  expect(tools.map((t) => t.name)).toEqual(expect.arrayContaining(names));
87
87
  }
88
- expect(names).toEqual(["workspace-ls", "workspace-status", "workspace-graph", "workspace-changes", "workspace-records", "workspace-points", "workspace-agent", "workspace-work-history", "workspace-runs", "records-new", "records-amend", "records-review", "records-close", "points-answer", "points-retract", "points-withdraw"]);
88
+ expect(names).toEqual(["workspace-ls", "workspace-status", "workspace-graph", "workspace-search", "workspace-changes", "workspace-records", "workspace-points", "workspace-agent", "workspace-work-history", "workspace-runs", "records-new", "records-amend", "records-review", "records-close", "points-answer", "points-retract", "points-withdraw"]);
89
89
  });
90
90
 
91
91
  test("a server outside any workspace, or given none, does not", async () => {
@@ -159,7 +159,27 @@ describe("reads", () => {
159
159
  expect(flag.content[0].text).toMatch(/at may not start with -/);
160
160
  }, 120_000);
161
161
 
162
+ test("workspace-search returns exactly the document chant workspace search --json prints (#4374)", async () => {
163
+ const s = server();
164
+ const res = await call(s, "workspace-search", { query: "app" });
165
+ expect(res.isError).toBeUndefined();
166
+ expect(res.structuredContent).toEqual(cli(["workspace", "search", "app", "--json"]));
167
+ expect(res.structuredContent).toMatchObject({ contract: 1, query: "app", at: null, source: { kind: "declared" } });
168
+ // A query that opens with an edge term is passed after --, and a refusal is a document.
169
+ const edge = await call(s, "workspace-search", { query: "->kind:Nothing", countBy: "kind" });
170
+ expect(edge.structuredContent).toEqual(cli(["workspace", "search", "--count-by", "kind", "--json", "--", "->kind:Nothing"]));
171
+ const refused = await call(s, "workspace-search", { query: "app", at: "latest" });
172
+ expect(refused.structuredContent).toMatchObject({ error: { code: "revision-unknown", hint: expect.stringContaining("--snapshot latest") } });
173
+ }, 240_000);
174
+
162
175
  test("the conformance suite's argv maps to the tool that answers it", () => {
176
+ expect(mcpToolCall(["workspace", "search", "app", "--member", "delivery", "--snapshot", "latest", "--env", "dev", "--json"])).toEqual({
177
+ name: "workspace-search",
178
+ arguments: { query: "app", member: ["delivery"], snapshot: "latest", env: "dev" },
179
+ });
180
+ expect(readArgv("workspace-search", { query: "->kind:X", member: ["delivery"], snapshot: "latest", env: "dev" })).toEqual([
181
+ "workspace", "search", "--member", "delivery", "--env", "dev", "--snapshot", "latest", "--json", "--", "->kind:X",
182
+ ]);
163
183
  expect(mcpToolCall(["workspace", "records", "--kind", KIND, "--json"])).toEqual({ name: "workspace-records", arguments: { kind: KIND } });
164
184
  expect(mcpToolCall(["workspace", "records", "--uncommitted", "--kind", KIND, "--json"])).toEqual({ name: "workspace-records", arguments: { uncommitted: true, kind: KIND } });
165
185
  expect(readArgv("workspace-records", { kind: KIND, uncommitted: true })).toEqual(["workspace", "records", "--kind", KIND, "--uncommitted", "--json"]);
@@ -5,7 +5,7 @@
5
5
  * Served when the server starts at or inside a declared workspace. Each tool
6
6
  * is a thin call into the code the CLI runs:
7
7
  *
8
- * - The reads (`workspace-ls`, `workspace-status`, `workspace-graph`,
8
+ * - The reads (`workspace-ls`, `workspace-status`, `workspace-graph`, `workspace-search`,
9
9
  * `workspace-changes`, `workspace-records`, `workspace-points`,
10
10
  * `workspace-work-history`, `workspace-runs`) run `chant workspace <command>
11
11
  * ... --json` with the chant this server runs as, in the server's
@@ -123,6 +123,27 @@ export const workspaceReadTools: ToolDefinition[] = [
123
123
  },
124
124
  },
125
125
  },
126
+ {
127
+ name: "workspace-search",
128
+ description:
129
+ "Answer an estate question over every chant member of the workspace with a few rows instead of the whole graph: chant workspace search <query> --json (search.schema.json, #4374). Returns that document unchanged. Each member is searched under its own chant and alone, ids are <member>/<id> as workspace-graph names the same nodes, and a member that can't be searched is listed with a reason code, never as zero matches. " +
130
+ "The query is space-separated terms that must all match: a bare word (substring of id, kind or attributes), kind:<substr> (a Terraform resource matches its type, kind:aws_instance), attr:<name>=<value>, tag:<key>=<value>, and edge terms ->X (this node references a node matching X) and <-X (referenced by one); ->*X is any depth, ->{2}X or ->{1,3}X a number of hops, ->[via=attr]X and ->[to=attr]X pick the edge by attribute, and !term negates. " +
131
+ "at is the git revision of the source; snapshot (with env) answers from a recorded lifecycle snapshot such as latest. They are two different times, and at refuses a snapshot ref.",
132
+ inputSchema: {
133
+ type: "object",
134
+ properties: {
135
+ query: { type: "string", description: "The search terms. May start with -> or <-." },
136
+ member: { type: "array", items: { type: "string" }, description: "Only these members (--member)." },
137
+ env: { type: "string", description: "The environment a snapshot was recorded for (--env)." },
138
+ snapshot: { type: "string", description: "Answer from this recorded lifecycle snapshot, such as latest (--snapshot). Needs env." },
139
+ countBy: { type: "string", description: "Group the matches: kind, module or attr:<name> (--count-by). The query may then be empty." },
140
+ show: { type: "string", description: "Comma-separated attributes to carry on each row (--show)." },
141
+ explain: { type: "boolean", description: "Add each member's universe and near misses (--explain)." },
142
+ at: atProp,
143
+ },
144
+ required: ["query"],
145
+ },
146
+ },
126
147
  {
127
148
  name: "workspace-changes",
128
149
  description:
@@ -417,6 +438,30 @@ export function readArgv(tool: string, params: Record<string, unknown>): string[
417
438
  "--json",
418
439
  ];
419
440
  }
441
+ case "workspace-search": {
442
+ // The query is one argument after `--`, so one that opens with -> stays a query.
443
+ const query = params.query;
444
+ if (typeof query !== "string") throw new ToolInputError("query must be a string");
445
+ const members = params.member === undefined || params.member === null ? [] : Array.isArray(params.member) ? params.member : [params.member];
446
+ const memberArgs = members.flatMap((m, i) => ["--member", str({ [`member[${i}]`]: m }, `member[${i}]`, true)!]);
447
+ const flags = (["env", "snapshot", "show"] as const).flatMap((f) => {
448
+ const v = str(params, f);
449
+ return v !== undefined ? [`--${f}`, v] : [];
450
+ });
451
+ const countBy = str(params, "countBy");
452
+ return [
453
+ "workspace",
454
+ "search",
455
+ ...memberArgs,
456
+ ...flags,
457
+ ...(countBy !== undefined ? ["--count-by", countBy] : []),
458
+ ...(bool(params, "explain") ? ["--explain"] : []),
459
+ ...atArgs,
460
+ "--json",
461
+ "--",
462
+ query,
463
+ ];
464
+ }
420
465
  case "workspace-agent": {
421
466
  const name = str(params, "name", true)!;
422
467
  return ["workspace", "agent", name, "--json"];
@@ -458,7 +503,7 @@ export function readArgv(tool: string, params: Record<string, unknown>): string[
458
503
  }
459
504
 
460
505
  /** Run chant and parse the one JSON document it printed. The exit code does not matter: an error document is a document. */
461
- function runRead(command: ChantCommand, argv: string[], cwd: string): Promise<unknown> {
506
+ export function runRead(command: ChantCommand, argv: string[], cwd: string): Promise<unknown> {
462
507
  return new Promise((settle, fail) => {
463
508
  const child = spawn(command[0], [...command.slice(1), ...argv], { cwd, env: { ...process.env, NO_COLOR: "1" }, stdio: ["ignore", "pipe", "pipe"] });
464
509
  let stdout = "";
@@ -153,13 +153,36 @@ export function parseArgs(args: string[]): ParsedArgs {
153
153
  // whose own value legitimately contains `=` (`--param=tier=production`
154
154
  // splits to flag `--param`, value `tier=production`, not further split on
155
155
  // the second `=`).
156
- args = splitJoinedFlags(args, BOOLEAN_FLAGS);
156
+ //
157
+ // Tokens after a bare `--` are positionals (#4369), so they are not split.
158
+ const end = args.indexOf("--");
159
+ args = end < 0 ? splitJoinedFlags(args, BOOLEAN_FLAGS) : [...splitJoinedFlags(args.slice(0, end), BOOLEAN_FLAGS), ...args.slice(end)];
160
+
161
+ // chant #4369 — every token after a bare `--` is a positional, whatever it
162
+ // starts with: `chant search -- "-kind:X"` is the spelling that always works.
163
+ let onlyPositionals = false;
164
+ const positional = (arg: string): void => {
165
+ if (result.command) (result.positionals ??= []).push(arg);
166
+ if (!result.command) {
167
+ result.command = arg;
168
+ } else if (result.path === ".") {
169
+ result.path = arg;
170
+ } else if (!result.extraPositional) {
171
+ result.extraPositional = arg;
172
+ } else {
173
+ result.extraPositional2 = arg;
174
+ }
175
+ };
157
176
 
158
177
  let i = 0;
159
178
  while (i < args.length) {
160
179
  const arg = args[i];
161
180
 
162
- if (arg === "--help" || arg === "-h") {
181
+ if (onlyPositionals) {
182
+ positional(arg);
183
+ } else if (arg === "--") {
184
+ onlyPositionals = true;
185
+ } else if (arg === "--help" || arg === "-h") {
163
186
  result.help = true;
164
187
  } else if (arg === "--version" || arg === "-V") {
165
188
  result.version = true;
@@ -339,6 +362,10 @@ export function parseArgs(args: string[]): ParsedArgs {
339
362
  result.explain = true;
340
363
  } else if (arg === "--show") {
341
364
  result.show = args[++i];
365
+ } else if (arg === "--count-by") {
366
+ // `chant search --count-by kind|module|attr:<name>` (#4360)
367
+ result.countBy = args[++i];
368
+ if (!result.countBy || result.countBy.startsWith("-")) throw new Error("--count-by needs a grouping: --count-by kind, --count-by module or --count-by attr:<name>");
342
369
  } else if (arg === "--up") {
343
370
  result.up = true;
344
371
  } else if (arg === "--down") {
@@ -508,6 +535,16 @@ export function parseArgs(args: string[]): ParsedArgs {
508
535
  result.deep = true;
509
536
  } else if (arg === "--at") {
510
537
  result.at = args[++i];
538
+ } else if (arg === "--capability-view") {
539
+ // `chant graph --format ir --capability-view` and `chant workspace graph --capability-view` (#4378)
540
+ result.capabilityView = true;
541
+ } else if (arg === "--capabilities") {
542
+ // `chant search --capabilities` and `chant workspace search --capabilities` (#4375)
543
+ result.capabilities = true;
544
+ } else if (arg === "--snapshot") {
545
+ // `chant search` and `chant workspace search --snapshot <ref>` (#4374)
546
+ result.snapshot = args[++i];
547
+ if (!result.snapshot || result.snapshot.startsWith("-")) throw new Error("--snapshot needs a snapshot ref: --snapshot latest");
511
548
  } else if (arg === "--kind") {
512
549
  // `chant workspace records|graph|check --kind <kind file>` (#2546, #2549)
513
550
  result.kind = args[++i];
@@ -972,17 +1009,19 @@ export function parseArgs(args: string[]): ParsedArgs {
972
1009
  // every command, not scoped per-command, so "the command's known
973
1010
  // flags" isn't something this loop can name in isolation.
974
1011
  throw new Error(`Unknown flag: ${arg}\nRun "chant --help" to see supported flags.`);
975
- } else if (!arg.startsWith("-")) {
976
- if (result.command) (result.positionals ??= []).push(arg);
977
- if (!result.command) {
978
- result.command = arg;
979
- } else if (result.path === ".") {
980
- result.path = arg;
981
- } else if (!result.extraPositional) {
982
- result.extraPositional = arg;
983
- } else {
984
- result.extraPositional2 = arg;
985
- }
1012
+ } else if (/^-[A-Za-z]/.test(arg)) {
1013
+ // chant #4369 — an unrecognised short flag (`-x`) is refused like an
1014
+ // unrecognised `--flag` (#1127). It used to be dropped without a word,
1015
+ // the same silent misparse #1127 closed for long flags. A lexicon's own
1016
+ // mounted command still gets its short flags: `main` tries the plugin
1017
+ // commands before rethrowing a parse error.
1018
+ throw new Error(`Unknown flag: ${arg}\nRun "chant --help" to see supported flags.`);
1019
+ } else {
1020
+ // Anything else is a positional, including a token that starts with `-`
1021
+ // but is not flag-shaped (#4369): `->kind:Bucket` is a search query that
1022
+ // opens with an outgoing edge term, and dropping it made `chant search`
1023
+ // answer for "." instead. A bare `-` is a positional too.
1024
+ positional(arg);
986
1025
  }
987
1026
 
988
1027
  i++;
@@ -246,6 +246,8 @@ export interface ParsedArgs {
246
246
  show?: string;
247
247
  /** `chant search --explain` — append a footer: universe count + why non-matches were excluded (#1139). */
248
248
  explain?: boolean;
249
+ /** `chant search --count-by kind|module|attr:<name>`: print one count per group instead of one row per match (#4360). */
250
+ countBy?: string;
249
251
  /** `chant graph --lens blast:<node> --up` — include upstream producers */
250
252
  up?: boolean;
251
253
  /** `chant graph --lens blast:<node> --down` — include downstream dependents */
@@ -446,6 +448,26 @@ export interface ParsedArgs {
446
448
  * should win.
447
449
  */
448
450
  at?: string;
451
+ /**
452
+ * `chant search "<q>" --snapshot <ref> --env <name>` and `chant workspace
453
+ * search "<q>" --snapshot <ref>` (#4374): the recorded snapshot to answer
454
+ * from. Plain search also takes it as `--at <ref>`, for compatibility;
455
+ * everywhere else in the read contract `--at` is a git revision.
456
+ */
457
+ snapshot?: string;
458
+ /**
459
+ * `chant search --capabilities` and `chant workspace search --capabilities`
460
+ * (#4375): print the capability catalog, the `capabilities` document of the
461
+ * read contract, instead of answering a query.
462
+ */
463
+ capabilities?: boolean;
464
+ /**
465
+ * `chant graph --format ir --capability-view` (#4378): add the lexicon-neutral
466
+ * view capability rules read, mapped by this chant's lexicons, to the IR as
467
+ * `capabilityView`. `chant workspace graph --capability-view` asks each
468
+ * member's chant for it and carries it on the member.
469
+ */
470
+ capabilityView?: boolean;
449
471
  /** `chant workspace records --kind <path>` (#2546): the record kind file to read records through. */
450
472
  kind?: string;
451
473
  /** Every `--kind` given, in order: `chant workspace graph --intent` reads each (#2651). */
@@ -0,0 +1,89 @@
1
+ /**
2
+ * The ports `chant components pr-env` runs ./pr-env-run.ts with: the records
3
+ * on chant/lifecycle (`_pr-envs/records.jsonl`), the gate ledger there, and
4
+ * the note and status on the forge. They sit apart from ./pr-env-run.ts
5
+ * because the gate port loads ./pr-run.ts and ../op/gate.ts, which bring
6
+ * Ajv, js-yaml and the component driver. A product with its own ports
7
+ * imports `@intentius/chant/components/pr-env-run` and never loads these.
8
+ */
9
+
10
+ import type { PrForge } from "../pr-forge";
11
+ import type { PrEnvPorts } from "./pr-env-run";
12
+ import {
13
+ PR_ENV_LEDGER_DIR,
14
+ PR_ENV_LEDGER_FILE,
15
+ parsePrEnvRecords,
16
+ prEnvNoteMarker,
17
+ prEnvStatusContext,
18
+ prEnvStatusDescription,
19
+ prEnvStatusState,
20
+ renderPrEnvNote,
21
+ type PrEnvReport,
22
+ } from "./pr-environment";
23
+
24
+ /**
25
+ * The note and the `chant/pr-env` status on the forge. A refusal from the
26
+ * forge warns and never fails the run.
27
+ */
28
+ export function forgePrEnvPublisher(forge: PrForge, warn: (message: string) => void = () => {}): (report: PrEnvReport) => Promise<void> {
29
+ return async (report) => {
30
+ const { forgePrincipalOf } = await import("../pr-forge");
31
+ const note = renderPrEnvNote(report, forgePrincipalOf(forge.kind, forge.host, "<your-login>"));
32
+ if (report.head) {
33
+ try {
34
+ await forge.setStatus(report.head, { context: prEnvStatusContext(report.member), state: prEnvStatusState(report), description: prEnvStatusDescription(report), ...(forge.runUrl ? { url: forge.runUrl } : {}) });
35
+ } catch (err) {
36
+ warn(`could not set the ${prEnvStatusContext(report.member)} status: ${messageOf(err)}`);
37
+ }
38
+ }
39
+ try {
40
+ await forge.upsertNote(report.pr, prEnvNoteMarker(report.env, report.member), note);
41
+ } catch (err) {
42
+ warn(`could not post the note on #${report.pr}: ${messageOf(err)}`);
43
+ }
44
+ };
45
+ }
46
+
47
+ /**
48
+ * The ports over chant/lifecycle in `cwd` (default the working directory):
49
+ * the records at `_pr-envs/records.jsonl`, appended and pushed, and the gate
50
+ * ledger read without writing a pending fact. With `forge`, the note and
51
+ * status are posted and the sweep asks it whether a pull request is open.
52
+ */
53
+ export function lifecyclePrEnvPorts(opts: { cwd?: string; forge?: PrForge; log?: (line: string) => void; warn?: (message: string) => void } = {}): PrEnvPorts {
54
+ const at = opts.cwd ? { cwd: opts.cwd } : undefined;
55
+ const warn = opts.warn ?? (() => {});
56
+ const forge = opts.forge;
57
+ return {
58
+ async readRecords() {
59
+ const { fetchLifecycle, readBlobFromPath } = await import("../lifecycle/git");
60
+ await fetchLifecycle(at).catch(() => false);
61
+ return parsePrEnvRecords(await readBlobFromPath(PR_ENV_LEDGER_DIR, PR_ENV_LEDGER_FILE, at));
62
+ },
63
+ async appendRecord(record) {
64
+ const { appendLedgerLine, fetchLifecycle, pushLifecycleReport } = await import("../lifecycle/git");
65
+ try {
66
+ await fetchLifecycle(at).catch(() => false);
67
+ await appendLedgerLine(PR_ENV_LEDGER_DIR, PR_ENV_LEDGER_FILE, JSON.stringify(record), `Pull request environment ${record.env} ${record.kind} (${record.result})`, at);
68
+ const pushed = await pushLifecycleReport(at);
69
+ if (pushed.pushWarning) warn(pushed.pushWarning);
70
+ } catch (err) {
71
+ warn(`could not record ${record.env}'s ${record.kind} on chant/lifecycle: ${messageOf(err)}`);
72
+ }
73
+ },
74
+ async decideGate(input) {
75
+ const { approvalOf, decidePrGate, readOnlyLedger } = await import("./pr-run");
76
+ const { gitGateLedgerPort } = await import("../op/gate");
77
+ const approval = approvalOf(await decidePrGate(readOnlyLedger(gitGateLedgerPort(at)), input));
78
+ return { status: approval.status, ...(approval.approvedBy ? { approvedBy: approval.approvedBy } : {}) };
79
+ },
80
+ ...(forge ? { publish: forgePrEnvPublisher(forge, warn) } : {}),
81
+ ...(forge?.pullRequestState ? { pullRequestState: (pr: number) => forge.pullRequestState!(pr) } : {}),
82
+ ...(opts.log ? { log: opts.log } : {}),
83
+ warn,
84
+ };
85
+ }
86
+
87
+ function messageOf(err: unknown): string {
88
+ return err instanceof Error ? err.message : String(err);
89
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * The pull request environment runs bundle alone (#4330 follow-up): a
3
+ * product that brings its own adapter and ports imports
4
+ * `@intentius/chant/components/pr-env-run` into one bundled file, without the
5
+ * gate, Ajv, js-yaml or the config loader. The ports chant uses live in
6
+ * ./pr-env-ports.ts. This bundles the module and reads esbuild's list of what
7
+ * went in.
8
+ */
9
+
10
+ import { join } from "node:path";
11
+ import { build } from "esbuild";
12
+ import { describe, expect, test } from "vitest";
13
+ import { REPO } from "../workspace/__fixtures__/contract-repo";
14
+
15
+ describe("the pr-env-run bundle", () => {
16
+ test("holds the runs and the records, and no gate, schema checks, YAML or config loader", async () => {
17
+ const result = await build({
18
+ entryPoints: [join(REPO, "packages", "core", "src", "components", "pr-env-run.ts")],
19
+ bundle: true,
20
+ platform: "node",
21
+ format: "esm",
22
+ write: false,
23
+ metafile: true,
24
+ outdir: "out",
25
+ logLevel: "silent",
26
+ });
27
+ const inputs = Object.keys(result.metafile.inputs).map((i) => i.replace(/^.*?packages\//, "packages/")).sort();
28
+ expect(inputs).toEqual([
29
+ "packages/core/src/change-set.ts",
30
+ "packages/core/src/ci-handover.ts",
31
+ "packages/core/src/ci-jobs.ts",
32
+ "packages/core/src/components/changed-files.ts",
33
+ "packages/core/src/components/pr-env-run.ts",
34
+ "packages/core/src/components/pr-environment.ts",
35
+ "packages/core/src/components/pr-pipeline.ts",
36
+ "packages/core/src/config-environments.ts",
37
+ "packages/core/src/content-digest.ts",
38
+ "packages/core/src/declarable.ts",
39
+ "packages/core/src/effect-receipt.ts",
40
+ "packages/core/src/gated-waves.ts",
41
+ "packages/core/src/intrinsic.ts",
42
+ "packages/core/src/lifecycle/plan-digest.ts",
43
+ "packages/core/src/lifecycle/teardown.ts",
44
+ "packages/core/src/live-endpoint.ts",
45
+ "packages/core/src/observation.ts",
46
+ "packages/core/src/op/activity-runtime.ts",
47
+ "packages/core/src/op/emulator-lifecycle.ts",
48
+ "packages/core/src/plan-summary.ts",
49
+ "packages/core/src/pr-loop.ts",
50
+ ]);
51
+ expect(inputs.some((i) => /node_modules\/(ajv|js-yaml)\//.test(i) || i.endsWith("/op/gate.ts") || i.endsWith("/src/config.ts") || i.endsWith("/ts-loader.ts"))).toBe(false);
52
+ expect(result.outputFiles.reduce((n, f) => n + f.contents.byteLength, 0)).toBeLessThan(40 * 1024);
53
+ });
54
+ });