@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.
- package/dist/capabilities/aws.paths +148 -0
- package/dist/capabilities/base-mappings.json +122 -0
- package/dist/capabilities/catalog.d.ts +89 -0
- package/dist/capabilities/catalog.d.ts.map +1 -0
- package/dist/capabilities/catalog.js +204 -0
- package/dist/capabilities/language.d.ts +103 -0
- package/dist/capabilities/language.d.ts.map +1 -0
- package/dist/capabilities/language.js +335 -0
- package/dist/capabilities/matcher.d.ts +65 -0
- package/dist/capabilities/matcher.d.ts.map +1 -0
- package/dist/capabilities/matcher.js +299 -0
- package/dist/capabilities/view.d.ts +207 -0
- package/dist/capabilities/view.d.ts.map +1 -0
- package/dist/capabilities/view.js +484 -0
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/commands/lint.js +1 -0
- package/dist/cli/handlers/declared-graph.d.ts +44 -0
- package/dist/cli/handlers/declared-graph.d.ts.map +1 -0
- package/dist/cli/handlers/declared-graph.js +87 -0
- package/dist/cli/handlers/graph.d.ts +25 -0
- package/dist/cli/handlers/graph.d.ts.map +1 -1
- package/dist/cli/handlers/graph.js +93 -87
- package/dist/cli/handlers/pr-env.d.ts.map +1 -1
- package/dist/cli/handlers/pr-env.js +2 -1
- package/dist/cli/handlers/search-count.d.ts +89 -0
- package/dist/cli/handlers/search-count.d.ts.map +1 -0
- package/dist/cli/handlers/search-count.js +132 -0
- package/dist/cli/handlers/search.d.ts +358 -6
- package/dist/cli/handlers/search.d.ts.map +1 -1
- package/dist/cli/handlers/search.js +1065 -155
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +23 -3
- package/dist/cli/mcp/server.d.ts.map +1 -1
- package/dist/cli/mcp/server.js +3 -0
- package/dist/cli/mcp/tools/capabilities.d.ts +7 -0
- package/dist/cli/mcp/tools/capabilities.d.ts.map +1 -0
- package/dist/cli/mcp/tools/capabilities.js +27 -0
- package/dist/cli/mcp/workspace-tools.d.ts +3 -1
- package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
- package/dist/cli/mcp/workspace-tools.js +47 -2
- package/dist/cli/parse-args.d.ts.map +1 -1
- package/dist/cli/parse-args.js +64 -17
- package/dist/cli/registry.d.ts +22 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/components/pr-env-ports.d.ts +29 -0
- package/dist/components/pr-env-ports.d.ts.map +1 -0
- package/dist/components/pr-env-ports.js +77 -0
- package/dist/components/pr-env-run.d.ts +5 -21
- package/dist/components/pr-env-run.d.ts.map +1 -1
- package/dist/components/pr-env-run.js +6 -70
- package/dist/components/pr-environment.d.ts +2 -1
- package/dist/components/pr-environment.d.ts.map +1 -1
- package/dist/components/pr-environment.js +1 -1
- package/dist/config-environments.d.ts +92 -0
- package/dist/config-environments.d.ts.map +1 -0
- package/dist/config-environments.js +102 -0
- package/dist/config.d.ts +24 -84
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +9 -96
- package/dist/graph-detail.d.ts.map +1 -1
- package/dist/graph-detail.js +15 -3
- package/dist/graph-effective.d.ts +33 -2
- package/dist/graph-effective.d.ts.map +1 -1
- package/dist/graph-effective.js +215 -68
- package/dist/graph-ir.d.ts +23 -0
- package/dist/graph-ir.d.ts.map +1 -1
- package/dist/graph-ir.js +130 -0
- package/dist/graph-sub-template.d.ts +11 -0
- package/dist/graph-sub-template.d.ts.map +1 -0
- package/dist/graph-sub-template.js +19 -0
- package/dist/lexicon.d.ts +9 -0
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/observe.d.ts +28 -0
- package/dist/lifecycle/observe.d.ts.map +1 -1
- package/dist/lifecycle/observe.js +23 -1
- package/dist/lint/rule.d.ts +6 -0
- package/dist/lint/rule.d.ts.map +1 -1
- package/dist/live-endpoint.d.ts +1 -1
- package/dist/live-endpoint.d.ts.map +1 -1
- package/dist/live-endpoint.js +1 -1
- package/dist/observation.d.ts +48 -1
- package/dist/observation.d.ts.map +1 -1
- package/dist/observation.js +21 -5
- package/dist/op/activities/activity-contracts.d.ts +4 -4
- package/dist/wave-runtime.mjs +250 -249
- package/dist/workspace/capabilities-contract.d.ts +66 -0
- package/dist/workspace/capabilities-contract.d.ts.map +1 -0
- package/dist/workspace/capabilities-contract.js +107 -0
- package/dist/workspace/capabilities.schema.json +229 -0
- package/dist/workspace/compose-graph.d.ts +9 -0
- package/dist/workspace/compose-graph.d.ts.map +1 -1
- package/dist/workspace/compose-graph.js +3 -1
- package/dist/workspace/conformance/index.d.ts +1 -1
- package/dist/workspace/conformance/index.d.ts.map +1 -1
- package/dist/workspace/conformance/index.js +39 -2
- package/dist/workspace/graph-cache.d.ts +8 -0
- package/dist/workspace/graph-cache.d.ts.map +1 -1
- package/dist/workspace/graph-cache.js +27 -3
- package/dist/workspace/graph-cli.d.ts +39 -2
- package/dist/workspace/graph-cli.d.ts.map +1 -1
- package/dist/workspace/graph-cli.js +66 -24
- package/dist/workspace/graph.schema.json +148 -0
- package/dist/workspace/kind-readers.d.ts +3 -0
- package/dist/workspace/kind-readers.d.ts.map +1 -1
- package/dist/workspace/kind-readers.js +7 -1
- package/dist/workspace/member-commands.d.ts +10 -1
- package/dist/workspace/member-commands.d.ts.map +1 -1
- package/dist/workspace/member-commands.js +17 -1
- package/dist/workspace/nested-graph.d.ts +2 -0
- package/dist/workspace/nested-graph.d.ts.map +1 -1
- package/dist/workspace/nested-graph.js +32 -1
- package/dist/workspace/reason-codes.d.ts +10 -0
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/reason-codes.js +12 -0
- package/dist/workspace/search-cli.d.ts +204 -0
- package/dist/workspace/search-cli.d.ts.map +1 -0
- package/dist/workspace/search-cli.js +466 -0
- package/dist/workspace/search-contract.d.ts +56 -0
- package/dist/workspace/search-contract.d.ts.map +1 -0
- package/dist/workspace/search-contract.js +70 -0
- package/dist/workspace/search.schema.json +822 -0
- package/package.json +1 -1
- package/src/__snapshots__/okf.test.ts.snap +10 -0
- package/src/capabilities/aws.paths +148 -0
- package/src/capabilities/base-mappings.json +122 -0
- package/src/capabilities/catalog.ts +230 -0
- package/src/capabilities/language.test.ts +191 -0
- package/src/capabilities/language.ts +373 -0
- package/src/capabilities/matcher.ts +315 -0
- package/src/capabilities/view-compose.test.ts +76 -0
- package/src/capabilities/view.ts +631 -0
- package/src/cli/commands/lint.ts +1 -0
- package/src/cli/handlers/declared-graph.ts +99 -0
- package/src/cli/handlers/graph.test.ts +94 -3
- package/src/cli/handlers/graph.ts +123 -96
- package/src/cli/handlers/pr-env.ts +1 -2
- package/src/cli/handlers/search-build-roots.test.ts +144 -0
- package/src/cli/handlers/search-count.test.ts +191 -0
- package/src/cli/handlers/search-count.ts +165 -0
- package/src/cli/handlers/search-json.test.ts +333 -0
- package/src/cli/handlers/search-live.test.ts +387 -0
- package/src/cli/handlers/search-paths.test.ts +235 -0
- package/src/cli/handlers/search.ts +1312 -166
- package/src/cli/main.test.ts +28 -3
- package/src/cli/main.ts +23 -3
- package/src/cli/mcp/server.test.ts +3 -3
- package/src/cli/mcp/server.ts +3 -0
- package/src/cli/mcp/tools/capabilities.ts +32 -0
- package/src/cli/mcp/workspace-tools.e2e.test.ts +21 -1
- package/src/cli/mcp/workspace-tools.ts +47 -2
- package/src/cli/parse-args.ts +52 -13
- package/src/cli/registry.ts +22 -0
- package/src/components/pr-env-ports.ts +89 -0
- package/src/components/pr-env-run-bundle.test.ts +54 -0
- package/src/components/pr-env-run.ts +5 -76
- package/src/components/pr-environment.ts +2 -1
- package/src/config-environments.ts +133 -0
- package/src/config.ts +37 -124
- package/src/graph-detail.ts +15 -3
- package/src/graph-effective.ts +226 -75
- package/src/graph-ir.test.ts +163 -0
- package/src/graph-ir.ts +147 -0
- package/src/graph-sub-template.ts +22 -0
- package/src/lexicon.ts +10 -0
- package/src/lifecycle/observe.test.ts +27 -1
- package/src/lifecycle/observe.ts +43 -1
- package/src/lint/rule.ts +6 -0
- package/src/live-endpoint.ts +1 -1
- package/src/observation.test.ts +25 -0
- package/src/observation.ts +69 -5
- package/src/workspace/capabilities-contract.test.ts +88 -0
- package/src/workspace/capabilities-contract.ts +132 -0
- package/src/workspace/capabilities.schema.json +229 -0
- package/src/workspace/compose-graph.ts +10 -1
- package/src/workspace/conformance/conformance.e2e.test.ts +14 -1
- package/src/workspace/conformance/index.ts +31 -2
- package/src/workspace/graph-cache.ts +27 -3
- package/src/workspace/graph-cli.ts +96 -24
- package/src/workspace/graph.schema.json +148 -0
- package/src/workspace/kind-readers.e2e.test.ts +28 -0
- package/src/workspace/kind-readers.test.ts +1 -1
- package/src/workspace/kind-readers.ts +7 -1
- package/src/workspace/member-commands.ts +22 -1
- package/src/workspace/nested-graph.ts +32 -1
- package/src/workspace/reason-codes.test.ts +7 -0
- package/src/workspace/reason-codes.ts +12 -0
- package/src/workspace/search-cli.e2e.test.ts +305 -0
- package/src/workspace/search-cli.ts +591 -0
- package/src/workspace/search-contract.ts +95 -0
- 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
|
-
|
|
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
|
|
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
|
|
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;
|