@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
@@ -0,0 +1,88 @@
1
+ /**
2
+ * The `capabilities` document of the read contract (#4375): its schema, the
3
+ * document `chant search --capabilities --json` prints, and its failure.
4
+ */
5
+ import { readFileSync } from "node:fs";
6
+ import { join } from "node:path";
7
+ import { describe, expect, test } from "vitest";
8
+ import { contract, validSchema } from "./__fixtures__/contract-repo";
9
+ import { READ_CONTRACT_FLOOR, READ_CONTRACT_VERSION, REASONS } from "./reason-codes";
10
+ import { CAPABILITIES_OUTPUT_SCHEMA_ID, CAPABILITY_SUPPORT_CODES, capabilitiesDocument, capabilitiesFailure, formatCapabilities } from "./capabilities-contract";
11
+ import { READ_CONTRACT_COMMANDS, READ_CONTRACT_SCHEMAS, MCP_READ_TOOLS, mcpToolCall } from "./conformance";
12
+ import schema from "./capabilities.schema.json";
13
+
14
+ const { expectValid } = contract(schema);
15
+
16
+ /** The lexicons' mappings as they ship them, read as data: loading a lexicon's plugin takes seconds. */
17
+ const LEXICONS = join(import.meta.dirname, "..", "..", "..", "..", "lexicons");
18
+ const load = async (name: string) => {
19
+ if (name !== "aws" && name !== "terraform") throw new Error(`no lexicon ${name}`);
20
+ return { capabilityPack: { mapping: JSON.parse(readFileSync(join(LEXICONS, name, "src", "capabilities", "mapping.json"), "utf-8")) } };
21
+ };
22
+
23
+ describe("capabilities.schema.json", () => {
24
+ test("is a valid draft 2020-12 document at the contract version, naming the floor", () => {
25
+ expect(validSchema(schema)).toBe(true);
26
+ expect(schema.$id).toBe(CAPABILITIES_OUTPUT_SCHEMA_ID);
27
+ expect(schema.$id).toBe(`https://intentius.io/chant/schemas/workspace/capabilities/v${READ_CONTRACT_VERSION}/capabilities.schema.json`);
28
+ for (const branch of ["result", "failure"] as const) {
29
+ const props = (schema.$defs as Record<string, { properties: Record<string, unknown> }>)[branch].properties;
30
+ expect(props.contract).toEqual({ const: READ_CONTRACT_VERSION });
31
+ expect(props.$schema).toEqual({ const: schema.$id });
32
+ }
33
+ expect(schema.description).toContain(`chant ${READ_CONTRACT_FLOOR} and newer`);
34
+ });
35
+
36
+ test("names each support code in the closed list", () => {
37
+ for (const code of CAPABILITY_SUPPORT_CODES) expect(Object.keys(REASONS)).toContain(code);
38
+ });
39
+
40
+ test("is in the conformance suite as workspace search --capabilities, served by the capabilities MCP tool", () => {
41
+ expect(READ_CONTRACT_COMMANDS).toContain("search --capabilities");
42
+ expect(READ_CONTRACT_SCHEMAS["search --capabilities"]).toBe("capabilities.schema.json");
43
+ expect(MCP_READ_TOOLS["search --capabilities"]).toBe("capabilities");
44
+ expect(mcpToolCall(["workspace", "search", "--capabilities", "--json"])).toEqual({ name: "capabilities", arguments: {} });
45
+ expect(mcpToolCall(["workspace", "search", "app", "--capabilities", "--json"])).toBeUndefined();
46
+ });
47
+ });
48
+
49
+ describe("the document", () => {
50
+ test("lists the fifteen capabilities with a support statement for each lexicon asked about", async () => {
51
+ const doc = await capabilitiesDocument(["aws", "terraform", "no-such-lexicon"], load);
52
+ expectValid(doc);
53
+ expect(doc.contract).toBe(1);
54
+ expect(doc.capabilities).toHaveLength(16);
55
+ const reaches = doc.capabilities.find((c) => c.name === "reaches")!;
56
+ expect(reaches).toMatchObject({
57
+ shape: "edge",
58
+ search: "->[cap=reaches]",
59
+ depth: "declared",
60
+ params: [{ name: "action", optional: true }],
61
+ uses: ["can-assume", "grants"],
62
+ lexicons: {
63
+ aws: "supported",
64
+ terraform: "supported",
65
+ "no-such-lexicon": { code: "lexicon-not-loaded" },
66
+ },
67
+ });
68
+ expect(reaches.rule).toBe("reaches(r, x; action?) := r:Role ->{0,}can-assume ->grants($action) x");
69
+ expect(reaches.notEvaluated).toContain('Resource "*"');
70
+ expect(doc.helpers.map((h) => h.name)).toEqual(["in-sg", "ingress-of", "world-open", "allows", "denies", "pivot"]);
71
+ for (const c of doc.capabilities) {
72
+ expect(c.answers.length, c.name).toBeGreaterThan(0);
73
+ expect(c.search, c.name).toBe(c.shape === "set" ? `cap:${c.name}` : `->[cap=${c.name}]`);
74
+ }
75
+ });
76
+
77
+ test("prints as text, a line per capability with its parameters", async () => {
78
+ const lines = formatCapabilities(await capabilitiesDocument([], load));
79
+ expect(lines[0]).toMatch(/^cap:internet-facing +set +its subnet routes 0\.0\.0\.0\/0 to an internet gateway$/);
80
+ expect(lines).toContainEqual(expect.stringMatching(/^ +port\?: a port the open rule must cover, such as 22$/));
81
+ expect(lines.at(-1)).toBe("— 16 capabilities · --json prints each rule");
82
+ });
83
+
84
+ test("a refusal is a failure document with a code", async () => {
85
+ const doc = await capabilitiesFailure({ code: "usage-invalid", message: "m" });
86
+ expectValid(doc);
87
+ });
88
+ });
@@ -0,0 +1,132 @@
1
+ /**
2
+ * The `capabilities` document of the read contract (#4375, ws-017): the
3
+ * capability catalog `chant search` answers `cap:` terms with, described by
4
+ * `capabilities.schema.json` beside this file.
5
+ *
6
+ * `chant search --capabilities --json` prints it for one project, with a
7
+ * support statement for each lexicon the project loads. `chant workspace
8
+ * search --capabilities --json` prints it from the workspace root's chant,
9
+ * for every lexicon a member loads (a terraform member's reader lexicon
10
+ * included, #4380): capability rules run once, under
11
+ * the workspace's chant, so the catalog is that chant's. chant's MCP
12
+ * `capabilities` tool returns whichever of the two applies where the server
13
+ * runs, unchanged.
14
+ */
15
+
16
+ import { resolve } from "node:path";
17
+ import type { ReasonCode } from "./reason-codes";
18
+ import type { CapabilityEntry } from "../capabilities/catalog";
19
+
20
+ /** The version of the `capabilities` document this chant writes. */
21
+ export const CAPABILITIES_CONTRACT_VERSION = 1;
22
+
23
+ /** `$id` of the JSON Schema for the document, shipped beside this file. */
24
+ export const CAPABILITIES_OUTPUT_SCHEMA_ID = "https://intentius.io/chant/schemas/workspace/capabilities/v1/capabilities.schema.json";
25
+
26
+ /** Why no catalog was printed. */
27
+ export const CAPABILITIES_ERROR_CODES = ["usage-invalid"] as const satisfies readonly ReasonCode[];
28
+ export type CapabilitiesErrorCode = (typeof CAPABILITIES_ERROR_CODES)[number];
29
+
30
+ /** Why a lexicon can't supply a capability. */
31
+ export const CAPABILITY_SUPPORT_CODES = ["lexicon-has-no-mapping", "capability-not-derivable", "lexicon-not-loaded"] as const satisfies readonly ReasonCode[];
32
+
33
+ interface CapabilitiesHead {
34
+ $schema: typeof CAPABILITIES_OUTPUT_SCHEMA_ID;
35
+ contract: typeof CAPABILITIES_CONTRACT_VERSION;
36
+ /** The chant that wrote the document. */
37
+ chant: string;
38
+ }
39
+
40
+ export interface CapabilitiesResult extends CapabilitiesHead {
41
+ capabilities: CapabilityEntry[];
42
+ helpers: { name: string; rule: string }[];
43
+ }
44
+
45
+ export interface CapabilitiesFailure extends CapabilitiesHead {
46
+ error: { code: CapabilitiesErrorCode; message: string; hint?: string };
47
+ }
48
+
49
+ export type CapabilitiesDocument = CapabilitiesResult | CapabilitiesFailure;
50
+
51
+ async function head(): Promise<CapabilitiesHead> {
52
+ const { readChantPackageVersion } = await import("../package-version");
53
+ return { $schema: CAPABILITIES_OUTPUT_SCHEMA_ID, contract: CAPABILITIES_CONTRACT_VERSION, chant: readChantPackageVersion() };
54
+ }
55
+
56
+ /**
57
+ * The catalog, with a support statement for each lexicon named. Each is
58
+ * loaded on its own, so one that isn't installed is said to be, and the rest
59
+ * still read.
60
+ */
61
+ export async function capabilitiesDocument(
62
+ lexicons: string[],
63
+ load?: (name: string) => Promise<{ capabilityPack?: { mapping: unknown } }>,
64
+ ): Promise<CapabilitiesResult> {
65
+ const loadPlugin = load ?? (await import("../cli/plugins")).loadPlugin;
66
+ const { capabilityCatalog } = await import("../capabilities/catalog");
67
+ const mappings: Record<string, Parameters<typeof capabilityCatalog>[0][string]> = {};
68
+ const notLoaded: string[] = [];
69
+ for (const name of [...new Set(lexicons)].sort()) {
70
+ try {
71
+ mappings[name] = (await loadPlugin(name)).capabilityPack?.mapping as (typeof mappings)[string];
72
+ } catch {
73
+ notLoaded.push(name);
74
+ mappings[name] = undefined;
75
+ }
76
+ }
77
+ const catalog = capabilityCatalog(mappings);
78
+ for (const c of catalog.capabilities) {
79
+ for (const name of notLoaded) {
80
+ c.lexicons[name] = { code: "lexicon-not-loaded", message: `the ${name} lexicon isn't installed where this chant runs, so its mapping wasn't read` };
81
+ }
82
+ }
83
+ return { ...(await head()), ...catalog };
84
+ }
85
+
86
+ export async function capabilitiesFailure(error: CapabilitiesFailure["error"]): Promise<CapabilitiesFailure> {
87
+ return { ...(await head()), error };
88
+ }
89
+
90
+ /**
91
+ * The lexicons the members of the workspace at `cwd` load, for its catalog:
92
+ * each chant member's own, and for a member of a package's kind, such as
93
+ * `terraform`, the lexicon its reader project declares (#4380).
94
+ */
95
+ export async function workspaceLexicons(cwd: string): Promise<string[]> {
96
+ const { listWorkspace } = await import("./ls");
97
+ const { findWorkspaceRoot } = await import("../project-root");
98
+ const { resolveProjectLexicons } = await import("../cli/plugins");
99
+ const { loadKindRegistry } = await import("./kinds");
100
+ const located = findWorkspaceRoot(cwd);
101
+ const doc = listWorkspace({ cwd });
102
+ if (!located || "error" in doc) return [];
103
+ const out = new Set<string>();
104
+ const kinds = doc.members.some((m) => m.kind !== "chant" && m.readable) ? loadKindRegistry(doc.workspace.pins, located.dir).registry : undefined;
105
+ for (const m of doc.members) {
106
+ if (!m.readable) continue;
107
+ const reader = m.kind === "chant" ? undefined : kinds?.get(m.kind)?.graph?.lexicon;
108
+ if (reader) out.add(reader);
109
+ if (m.kind !== "chant") continue;
110
+ try {
111
+ for (const l of await resolveProjectLexicons(resolve(located.dir, m.dir))) out.add(l);
112
+ } catch {
113
+ // A member whose lexicons can't be resolved adds none; its own search says why.
114
+ }
115
+ }
116
+ return [...out];
117
+ }
118
+
119
+ /** The catalog as text: one capability per line, its search term and meaning, then its parameters. */
120
+ export function formatCapabilities(doc: CapabilitiesResult): string[] {
121
+ const width = Math.max(...doc.capabilities.map((c) => c.search.length));
122
+ const lines: string[] = [];
123
+ for (const c of doc.capabilities) {
124
+ lines.push(`${c.search.padEnd(width)} ${c.shape.padEnd(4)} ${c.meaning}`);
125
+ for (const p of c.params) lines.push(`${"".padEnd(width)} ${p.name}${p.optional ? "?" : ""}: ${p.meaning}`);
126
+ const unsupported = Object.entries(c.lexicons).filter(([, s]) => s !== "supported");
127
+ for (const [lex, s] of unsupported) lines.push(`${"".padEnd(width)} not in ${lex}: ${(s as { message: string }).message}`);
128
+ }
129
+ const lexicons = Object.keys(doc.capabilities[0]?.lexicons ?? {});
130
+ lines.push(`— ${doc.capabilities.length} capabilities${lexicons.length ? ` · lexicons ${lexicons.join(", ")}` : ""} · --json prints each rule`);
131
+ return lines;
132
+ }
@@ -0,0 +1,229 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://intentius.io/chant/schemas/workspace/capabilities/v1/capabilities.schema.json",
4
+ "title": "chant capabilities catalog",
5
+ "description": "What `chant search --capabilities --json` prints (#4375): the capability catalog `chant search` answers `cap:` terms and `->[cap=...]` edges with, each capability's rule, and whether each lexicon the project loads can supply it. chant's MCP `capabilities` tool returns the same document. Version 1 of the read contract. Readers ignore fields they do not know; a field is only ever added within a version. The codes are a closed list, each in the one closed list of `reason-codes.ts`. Contract version 1 of this document is written by chant 0.124.46 and newer; the read contract as a whole by chant 0.81.0 and newer.",
6
+ "oneOf": [
7
+ {
8
+ "$ref": "#/$defs/result"
9
+ },
10
+ {
11
+ "$ref": "#/$defs/failure"
12
+ }
13
+ ],
14
+ "$defs": {
15
+ "result": {
16
+ "type": "object",
17
+ "description": "Exit code 0.",
18
+ "required": [
19
+ "$schema",
20
+ "contract",
21
+ "chant",
22
+ "capabilities",
23
+ "helpers"
24
+ ],
25
+ "properties": {
26
+ "$schema": {
27
+ "const": "https://intentius.io/chant/schemas/workspace/capabilities/v1/capabilities.schema.json"
28
+ },
29
+ "contract": {
30
+ "const": 1
31
+ },
32
+ "chant": {
33
+ "type": "string"
34
+ },
35
+ "error": false,
36
+ "capabilities": {
37
+ "type": "array",
38
+ "description": "Every capability the catalog lists, in the order the rule file defines them.",
39
+ "items": {
40
+ "$ref": "#/$defs/capability"
41
+ }
42
+ },
43
+ "helpers": {
44
+ "type": "array",
45
+ "description": "Rules the capabilities use that the catalog does not list as capabilities of their own.",
46
+ "items": {
47
+ "type": "object",
48
+ "required": [
49
+ "name",
50
+ "rule"
51
+ ],
52
+ "properties": {
53
+ "name": {
54
+ "type": "string"
55
+ },
56
+ "rule": {
57
+ "type": "string"
58
+ }
59
+ }
60
+ }
61
+ }
62
+ }
63
+ },
64
+ "capability": {
65
+ "type": "object",
66
+ "required": [
67
+ "name",
68
+ "meaning",
69
+ "shape",
70
+ "params",
71
+ "search",
72
+ "answers",
73
+ "depth",
74
+ "lexicons",
75
+ "notEvaluated",
76
+ "uses",
77
+ "rule"
78
+ ],
79
+ "properties": {
80
+ "name": {
81
+ "type": "string",
82
+ "pattern": "^[a-z][a-z0-9-]*$"
83
+ },
84
+ "meaning": {
85
+ "type": "string",
86
+ "description": "What the capability says, in one line."
87
+ },
88
+ "shape": {
89
+ "enum": [
90
+ "set",
91
+ "edge"
92
+ ],
93
+ "description": "set: a set of nodes, tested with cap:<name>. edge: pairs of nodes, walked with ->[cap=<name>] or <-[cap=<name>]."
94
+ },
95
+ "params": {
96
+ "type": "array",
97
+ "description": "Arguments, in order: cap:<name>(<a>, <b>). An optional one left out drops the test that reads it.",
98
+ "items": {
99
+ "type": "object",
100
+ "required": [
101
+ "name",
102
+ "optional",
103
+ "meaning"
104
+ ],
105
+ "properties": {
106
+ "name": {
107
+ "type": "string"
108
+ },
109
+ "optional": {
110
+ "type": "boolean"
111
+ },
112
+ "meaning": {
113
+ "type": "string"
114
+ }
115
+ }
116
+ }
117
+ },
118
+ "search": {
119
+ "type": "string",
120
+ "description": "The search term that asks for it: cap:<name> for a set, ->[cap=<name>] for an edge."
121
+ },
122
+ "answers": {
123
+ "type": "array",
124
+ "description": "Questions it serves, in people's words.",
125
+ "items": {
126
+ "type": "string"
127
+ }
128
+ },
129
+ "depth": {
130
+ "enum": [
131
+ "declared",
132
+ "live",
133
+ "either"
134
+ ],
135
+ "description": "Which observation can supply its facts: declared source, a live read or snapshot, or either."
136
+ },
137
+ "lexicons": {
138
+ "type": "object",
139
+ "description": "Each lexicon the project loads: supported, or why it can't supply the capability.",
140
+ "additionalProperties": {
141
+ "oneOf": [
142
+ {
143
+ "const": "supported"
144
+ },
145
+ {
146
+ "type": "object",
147
+ "required": [
148
+ "code",
149
+ "message"
150
+ ],
151
+ "properties": {
152
+ "code": {
153
+ "enum": [
154
+ "lexicon-has-no-mapping",
155
+ "capability-not-derivable",
156
+ "lexicon-not-loaded"
157
+ ]
158
+ },
159
+ "message": {
160
+ "type": "string"
161
+ }
162
+ }
163
+ }
164
+ ]
165
+ }
166
+ },
167
+ "notEvaluated": {
168
+ "type": "array",
169
+ "description": "What the rule leaves out. chant search prints this under every answer that uses the capability.",
170
+ "items": {
171
+ "type": "string"
172
+ }
173
+ },
174
+ "uses": {
175
+ "type": "array",
176
+ "description": "The capabilities and helpers its rule refers to.",
177
+ "items": {
178
+ "type": "string"
179
+ }
180
+ },
181
+ "rule": {
182
+ "type": "string",
183
+ "description": "The rule as written, one alternative per line."
184
+ }
185
+ }
186
+ },
187
+ "failure": {
188
+ "type": "object",
189
+ "description": "Exit code 1.",
190
+ "required": [
191
+ "$schema",
192
+ "contract",
193
+ "chant",
194
+ "error"
195
+ ],
196
+ "properties": {
197
+ "$schema": {
198
+ "const": "https://intentius.io/chant/schemas/workspace/capabilities/v1/capabilities.schema.json"
199
+ },
200
+ "contract": {
201
+ "const": 1
202
+ },
203
+ "chant": {
204
+ "type": "string"
205
+ },
206
+ "error": {
207
+ "type": "object",
208
+ "required": [
209
+ "code",
210
+ "message"
211
+ ],
212
+ "properties": {
213
+ "code": {
214
+ "enum": [
215
+ "usage-invalid"
216
+ ]
217
+ },
218
+ "message": {
219
+ "type": "string"
220
+ },
221
+ "hint": {
222
+ "type": "string"
223
+ }
224
+ }
225
+ }
226
+ }
227
+ }
228
+ }
229
+ }
@@ -41,6 +41,7 @@ import type { KindLinkRead } from "./kind-link-readers";
41
41
  import type { RecordLinkRow } from "./record-assets";
42
42
  import type { ReasonCode } from "./reason-codes";
43
43
  import { GRAPH_IR_VERSION, type GraphIR, type IRExport, type IRDanglingRef, type IRGroups, type IRImport, type IREdge, type IRNode } from "../graph-ir";
44
+ import type { ViewDocument } from "../capabilities/view";
44
45
 
45
46
  /** The version of the composed document `chant workspace graph` writes. */
46
47
  export const WORKSPACE_GRAPH_VERSION = 1;
@@ -105,6 +106,12 @@ export interface ComposedMember {
105
106
  * `chant workspace graph` on every member; null when the box declares neither.
106
107
  */
107
108
  box?: { factory: FactoryView | null; listing: ListingView | null } | null;
109
+ /**
110
+ * With `--capability-view` (#4378): the lexicon-neutral view the member's
111
+ * own chant mapped through its own lexicons, with `<member>/<id>` ids. null
112
+ * when the member's chant predates the view and printed none.
113
+ */
114
+ capabilityView?: ViewDocument | null;
108
115
  /** Whole-read facts the member's IR carried (`meta`, `pipeline`), kept apart from the composed sections. */
109
116
  meta?: Record<string, unknown>;
110
117
  pipeline?: unknown;
@@ -249,7 +256,9 @@ export interface ComposeInput {
249
256
  ir?: GraphIR;
250
257
  }
251
258
 
252
- const prefix = (member: string, id: string): string => `${member}/${id}`;
259
+ /** A member's node id as the composed workspace documents give it: `<member>/<id>`. `workspace search` uses the same rule (#4374). */
260
+ export const composedId = (member: string, id: string): string => `${member}/${id}`;
261
+ const prefix = composedId;
253
262
 
254
263
  const strings = (v: unknown): string[] => (Array.isArray(v) ? v.filter((x): x is string => typeof x === "string") : []);
255
264
  const records = (v: unknown): Record<string, unknown>[] =>
@@ -39,6 +39,8 @@ const JSON_FLAG: Record<ReadContractCommand, string[]> = {
39
39
  "graph --composites": ["--json"],
40
40
  runs: ["--json"],
41
41
  wip: ["--json"],
42
+ search: ["--json"],
43
+ "search --capabilities": ["--json"],
42
44
  };
43
45
 
44
46
  /** The smallest reader that conforms, with a hook to misbehave. */
@@ -65,6 +67,17 @@ describe("the generated conformance workspace (#2679)", () => {
65
67
  expect(report.skipped).toEqual([]);
66
68
  }, 300_000);
67
69
 
70
+ test("search answers over the chant member with workspace graph's ids, and lists the others (#4374)", async () => {
71
+ const docs: Record<string, unknown>[] = [];
72
+ const report = await runWorkspaceReaderConformance(reader((doc) => (docs.push(doc), doc)), { workspaceDir: ws.dir, commands: ["search"] });
73
+ expect(report.problems).toEqual([]);
74
+ const doc = docs[0] as { at: null; matches: { id: string; member: string }[]; members: { name: string; status: string; alone?: boolean }[] };
75
+ expect(doc.at).toBeNull();
76
+ expect(doc.matches.length).toBeGreaterThan(0);
77
+ for (const m of doc.matches) expect(m.id.startsWith(`${m.member}/`), m.id).toBe(true);
78
+ expect(doc.members.find((m) => m.name === "delivery")).toMatchObject({ status: "searched", alone: true });
79
+ }, 300_000);
80
+
68
81
  test("graph --composites joins the fixture's composite to its component", async () => {
69
82
  const docs: Record<string, unknown>[] = [];
70
83
  await runWorkspaceReaderConformance(reader((doc) => (docs.push(doc), doc)), { workspaceDir: ws.dir, commands: ["graph --composites"] });
@@ -120,7 +133,7 @@ describe("commands (#2679)", () => {
120
133
  expect(report.problems).toEqual([]);
121
134
  expect(read).toEqual(["ls", "status", "graph --composites"]);
122
135
  expect(report.checked).toEqual(["ls", "status", "graph --composites"]);
123
- expect(report.skipped).toEqual(["graph", "check", "records", "records --uncommitted", "graph --intent", "runs", "wip"]);
136
+ expect(report.skipped).toEqual(["graph", "check", "records", "records --uncommitted", "graph --intent", "runs", "wip", "search", "search --capabilities"]);
124
137
  }, 300_000);
125
138
 
126
139
  test("a name that is not a contract command, or an empty list, is refused", () => {
@@ -56,7 +56,7 @@ import { isDeepStrictEqual } from "node:util";
56
56
  import { READ_CONTRACT_VERSION } from "../reason-codes";
57
57
 
58
58
  /** The read-contract commands, as a reader names them. */
59
- export const READ_CONTRACT_COMMANDS = ["ls", "graph", "check", "status", "records", "records --uncommitted", "graph --intent", "graph --composites", "runs", "wip"] as const;
59
+ export const READ_CONTRACT_COMMANDS = ["ls", "graph", "check", "status", "records", "records --uncommitted", "graph --intent", "graph --composites", "runs", "wip", "search", "search --capabilities"] as const;
60
60
  export type ReadContractCommand = (typeof READ_CONTRACT_COMMANDS)[number];
61
61
 
62
62
  /**
@@ -75,6 +75,10 @@ export const READ_CONTRACT_SCHEMAS: Record<ReadContractCommand, string> = {
75
75
  "graph --composites": "composites.schema.json",
76
76
  runs: "runs.schema.json",
77
77
  wip: "wip.schema.json",
78
+ // `workspace search` (#4374). Plain `chant search --json` prints the same document for one project.
79
+ search: "search.schema.json",
80
+ // The capability catalog (#4375), from the workspace's chant.
81
+ "search --capabilities": "capabilities.schema.json",
78
82
  };
79
83
 
80
84
  /** The flags that ask each command for its JSON document. A reader adds one of these and nothing else. */
@@ -89,6 +93,8 @@ export const READ_CONTRACT_JSON_FLAGS: Record<ReadContractCommand, readonly (rea
89
93
  "graph --composites": [[], ["--json"]],
90
94
  runs: [["--json"]],
91
95
  wip: [["--json"]],
96
+ search: [["--json"]],
97
+ "search --capabilities": [["--json"]],
92
98
  };
93
99
 
94
100
  /** The arguments the suite passes each read, on the reference workspace or the generated one. */
@@ -103,6 +109,9 @@ export const REFERENCE_READS: Record<ReadContractCommand, string[]> = {
103
109
  "graph --composites": [],
104
110
  runs: [],
105
111
  wip: [],
112
+ // A bare word: on either workspace it searches the chant member, delivery, and lists the others.
113
+ search: ["app"],
114
+ "search --capabilities": [],
106
115
  };
107
116
 
108
117
  /** One chant run: the arguments after `chant`, what it was given on stdin, and what it printed. */
@@ -344,6 +353,8 @@ export const MCP_READ_TOOLS: Partial<Record<ReadContractCommand, string>> = {
344
353
  "graph --intent": "workspace-graph",
345
354
  "graph --composites": "workspace-graph",
346
355
  runs: "workspace-runs",
356
+ search: "workspace-search",
357
+ "search --capabilities": "capabilities",
347
358
  };
348
359
 
349
360
  /**
@@ -359,9 +370,14 @@ export function mcpToolCall(argv: readonly string[]): { name: string; arguments:
359
370
  const args: Record<string, unknown> = {};
360
371
  const positional: string[] = [];
361
372
  const kinds: string[] = [];
373
+ const members: string[] = [];
362
374
  for (let i = 0; i < rest.length; i++) {
363
375
  const a = rest[i];
364
376
  const value = (): string | undefined => rest[++i];
377
+ if (a === "--") {
378
+ positional.push(...rest.slice(i + 1));
379
+ break;
380
+ }
365
381
  if (a === "--json") continue;
366
382
  if (a === "--format" && rest[i + 1] === "json") {
367
383
  i++;
@@ -381,7 +397,14 @@ export function mcpToolCall(argv: readonly string[]): { name: string; arguments:
381
397
  else if (a === "--by") args.by = value();
382
398
  else if (a === "--uncommitted") args.uncommitted = true;
383
399
  else if (a === "--composites") args.composites = true;
384
- else if (a.startsWith("-")) return undefined;
400
+ else if (verb === "search" && a === "--member") members.push(value() ?? "");
401
+ else if (verb === "search" && a === "--env") args.env = value();
402
+ else if (verb === "search" && a === "--snapshot") args.snapshot = value();
403
+ else if (verb === "search" && a === "--count-by") args.countBy = value();
404
+ else if (verb === "search" && a === "--show") args.show = value();
405
+ else if (verb === "search" && a === "--explain") args.explain = true;
406
+ else if (verb === "search" && a === "--capabilities") args.capabilities = true;
407
+ else if (/^-[A-Za-z-]/.test(a)) return undefined;
385
408
  else positional.push(a);
386
409
  }
387
410
  switch (verb) {
@@ -397,6 +420,12 @@ export function mcpToolCall(argv: readonly string[]): { name: string; arguments:
397
420
  return positional.length === 0 ? { name: "workspace-graph", arguments: { ...args, ...(kinds.length ? { kind: kinds } : {}) } } : undefined;
398
421
  case "runs":
399
422
  return positional.length === 0 && kinds.length === 0 ? { name: "workspace-runs", arguments: args } : undefined;
423
+ case "search":
424
+ // The catalog (#4375) is its own tool, and takes nothing.
425
+ if (args.capabilities) return positional.length === 0 && Object.keys(args).length === 1 && kinds.length === 0 && members.length === 0 ? { name: "capabilities", arguments: {} } : undefined;
426
+ return positional.length <= 1 && kinds.length === 0
427
+ ? { name: "workspace-search", arguments: { ...args, query: positional[0] ?? "", ...(members.length ? { member: members } : {}) } }
428
+ : undefined;
400
429
  default:
401
430
  return undefined;
402
431
  }
@@ -368,9 +368,7 @@ export function splitCached(plan: MemberPlan, args: ParsedArgs, cache: GraphCach
368
368
  // A member read through a generated reader project (#2874) is also keyed on
369
369
  // the kind's graph block and the version of the package that reads it.
370
370
  const reader = unit.reader ? readerStamp(unit.reader) : "";
371
- const abs = unit.dir === "." ? plan.workspace.root : join(plan.workspace.root, ...unit.dir.split("/"));
372
- const revision = at ? { commit: at, dir: unit.dir } : undefined;
373
- const stamp = memberStamp(abs, unit.exclude, revision);
371
+ const { abs, revision, stamp } = unitStamp(plan, unit, at);
374
372
  if (!stamp) continue;
375
373
  stamps.set(unit.id, stamp.value);
376
374
  const key = cacheKey({ member: unit.member, dir: unit.dir, stamp: stamp.value, toolchain: `${tool}\0${reader}`, argv, env });
@@ -385,6 +383,32 @@ export function splitCached(plan: MemberPlan, args: ParsedArgs, cache: GraphCach
385
383
  return { hits, stamps, pending, started };
386
384
  }
387
385
 
386
+ /** One unit's stamp, and what it was taken over: the member's directory on disk, and the commit for `--at`. */
387
+ function unitStamp(plan: MemberPlan, unit: RunUnit, at: string | null): { abs: string; revision?: { commit: string; dir: string }; stamp: Stamp | undefined } {
388
+ const abs = unit.dir === "." ? plan.workspace.root : join(plan.workspace.root, ...unit.dir.split("/"));
389
+ const revision = at ? { commit: at, dir: unit.dir } : undefined;
390
+ return { abs, ...(revision ? { revision } : {}), stamp: memberStamp(abs, unit.exclude, revision) };
391
+ }
392
+
393
+ /**
394
+ * Every member unit's stamp, by unit id, by the rule {@link splitCached} keys
395
+ * the cache on (ws-059), with no cache read. A read that is never cached, such
396
+ * as `chant workspace search` (#4374), still stamps its members this way, so a
397
+ * reader compares stamps across documents with one meaning. A unit whose stamp
398
+ * can't be taken is left out.
399
+ */
400
+ export function planStamps(plan: MemberPlan, at: string | null): Map<string, string> {
401
+ const stamps = new Map<string, string>();
402
+ for (const group of plan.groups) {
403
+ for (const unit of group.units) {
404
+ if (unit.group) continue;
405
+ const { stamp } = unitStamp(plan, unit, at);
406
+ if (stamp) stamps.set(unit.id, stamp.value);
407
+ }
408
+ }
409
+ return stamps;
410
+ }
411
+
388
412
  /** The plan without the units in `ids`; a toolchain left with no units is dropped. */
389
413
  export function withoutUnits(plan: MemberPlan, ids: ReadonlySet<string>): MemberPlan {
390
414
  if (ids.size === 0) return plan;