@intentius/chant 0.124.44 → 0.124.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/ci-identity.d.ts +183 -12
- package/dist/ci-identity.d.ts.map +1 -1
- package/dist/ci-identity.js +362 -53
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/commands/lint.js +1 -0
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/build.js +17 -1
- 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 +8 -2
- package/dist/cli/handlers/pr-env.d.ts.map +1 -1
- package/dist/cli/handlers/pr-env.js +127 -225
- package/dist/cli/handlers/pr.d.ts +3 -0
- package/dist/cli/handlers/pr.d.ts.map +1 -1
- package/dist/cli/handlers/pr.js +1 -0
- 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/cli-support.d.ts +2 -0
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/cli-support.js +4 -2
- package/dist/components/fan-out-support.d.ts +3 -0
- package/dist/components/fan-out-support.d.ts.map +1 -1
- package/dist/components/fan-out-support.js +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 +254 -0
- package/dist/components/pr-env-run.d.ts.map +1 -0
- package/dist/components/pr-env-run.js +376 -0
- package/dist/components/pr-environment.d.ts +91 -15
- package/dist/components/pr-environment.d.ts.map +1 -1
- package/dist/components/pr-environment.js +95 -9
- package/dist/components/pr-pipeline.d.ts +10 -1
- package/dist/components/pr-pipeline.d.ts.map +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/op/op-waves.d.ts +17 -0
- package/dist/op/op-waves.d.ts.map +1 -1
- package/dist/op/op-waves.js +23 -1
- package/dist/pr-forge.d.ts +15 -1
- package/dist/pr-forge.d.ts.map +1 -1
- package/dist/pr-forge.js +28 -16
- package/dist/pr-requires.d.ts +5 -1
- package/dist/pr-requires.d.ts.map +1 -1
- package/dist/wave-runtime.mjs +251 -250
- 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/ci-cloud-identity.d.ts +3 -1
- package/dist/workspace/ci-cloud-identity.d.ts.map +1 -1
- package/dist/workspace/ci-cloud-identity.js +16 -1
- package/dist/workspace/ci-run.d.ts +35 -8
- package/dist/workspace/ci-run.d.ts.map +1 -1
- package/dist/workspace/ci-run.js +95 -21
- package/dist/workspace/ci-secrets.d.ts.map +1 -1
- package/dist/workspace/ci-secrets.js +4 -3
- 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/declaration.d.ts +10 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.js +17 -1
- package/dist/workspace/declaration.schema.json +36 -1
- 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/ci-identity-targets.test.ts +275 -0
- package/src/ci-identity.ts +394 -61
- package/src/cli/commands/lint.ts +1 -0
- package/src/cli/handlers/build.ts +15 -1
- 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 +135 -227
- package/src/cli/handlers/pr.ts +4 -0
- 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/cli-support.ts +6 -1
- package/src/components/fan-out-support.ts +4 -1
- 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.test.ts +290 -0
- package/src/components/pr-env-run.ts +563 -0
- package/src/components/pr-environment.ts +161 -13
- package/src/components/pr-pipeline.ts +10 -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/op/op-waves.ts +38 -1
- package/src/pr-forge-bundle.test.ts +1 -1
- package/src/pr-forge.ts +44 -17
- package/src/pr-requires.test.ts +43 -6
- package/src/pr-requires.ts +5 -1
- 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/ci-cloud-identity.test.ts +20 -0
- package/src/workspace/ci-cloud-identity.ts +14 -1
- package/src/workspace/ci-run.test.ts +74 -1
- package/src/workspace/ci-run.ts +122 -29
- package/src/workspace/ci-secrets.test.ts +1 -1
- package/src/workspace/ci-secrets.ts +4 -3
- 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/declaration.schema.json +36 -1
- package/src/workspace/declaration.test.ts +7 -0
- package/src/workspace/declaration.ts +24 -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,191 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import type { GraphIR, IRNode } from "../../graph-ir";
|
|
3
|
+
import {
|
|
4
|
+
countGroups,
|
|
5
|
+
formatCounts,
|
|
6
|
+
groupMembers,
|
|
7
|
+
groupOf,
|
|
8
|
+
hasManyTerraformRoots,
|
|
9
|
+
parseCountBy,
|
|
10
|
+
terraformType,
|
|
11
|
+
type CountBy,
|
|
12
|
+
} from "./search-count";
|
|
13
|
+
|
|
14
|
+
/** A Terraform block as `buildGraphIr` projects one: key `<root>/[module.x/]<address>`. */
|
|
15
|
+
function tf(root: string, address: string, kind: string, callers: string[] = [], extra: Record<string, unknown> = {}): IRNode {
|
|
16
|
+
const id = [root, ...callers, address].join("/");
|
|
17
|
+
return {
|
|
18
|
+
id,
|
|
19
|
+
kind,
|
|
20
|
+
lexicon: "terraform",
|
|
21
|
+
attrs: { address, root, ...(callers.length > 0 ? { callers } : {}), ...extra },
|
|
22
|
+
} as IRNode;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const app = [
|
|
26
|
+
tf("app", "aws_instance.web", "Terraform::Resource", [], { expansion: "count" }),
|
|
27
|
+
tf("app", "aws_instance.worker", "Terraform::Resource"),
|
|
28
|
+
tf("app", "aws_s3_bucket.assets", "Terraform::Resource"),
|
|
29
|
+
tf("app", "data.aws_iam_policy.boundary", "Terraform::Data"),
|
|
30
|
+
tf("app", "var.region", "Terraform::Variable"),
|
|
31
|
+
tf("app", "aws_cloudfront_distribution.edge", "Terraform::Resource", ["module.cdn"]),
|
|
32
|
+
tf("app", "aws_s3_bucket.logs", "Terraform::Resource", ["module.cdn", "module.bucket"]),
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
const sum = (groups: Array<{ count: number }>) => groups.reduce((s, g) => s + g.count, 0);
|
|
36
|
+
|
|
37
|
+
describe("--count-by parsing", () => {
|
|
38
|
+
it("accepts kind, module and attr:<name>", () => {
|
|
39
|
+
expect(parseCountBy("kind")).toEqual({ by: "kind" });
|
|
40
|
+
expect(parseCountBy("module")).toEqual({ by: "module" });
|
|
41
|
+
expect(parseCountBy("attr:region")).toEqual({ by: "attr", name: "region" });
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("refuses anything else and names the spellings", () => {
|
|
45
|
+
for (const bad of ["type", "attr:", "attr", "Kind"]) {
|
|
46
|
+
const r = parseCountBy(bad);
|
|
47
|
+
expect("error" in r).toBe(true);
|
|
48
|
+
if ("error" in r) expect(r.hint).toContain("--count-by attr:<name>");
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe("--count-by kind", () => {
|
|
54
|
+
it("groups a Terraform resource by its resource type, not by Terraform::Resource", () => {
|
|
55
|
+
expect(terraformType(app[0])).toBe("aws_instance");
|
|
56
|
+
expect(terraformType(app[3])).toBe("data.aws_iam_policy");
|
|
57
|
+
expect(terraformType(app[4])).toBeUndefined();
|
|
58
|
+
const groups = countGroups(app, { by: "kind" });
|
|
59
|
+
expect(groups).toEqual([
|
|
60
|
+
{ group: "aws_instance", count: 2 },
|
|
61
|
+
{ group: "aws_s3_bucket", count: 2 },
|
|
62
|
+
{ group: "Terraform::Variable", count: 1 },
|
|
63
|
+
{ group: "aws_cloudfront_distribution", count: 1 },
|
|
64
|
+
{ group: "data.aws_iam_policy", count: 1 },
|
|
65
|
+
]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("keeps the kind of every other lexicon", () => {
|
|
69
|
+
const n = { id: "web", kind: "AWS::EC2::Instance", lexicon: "aws", attrs: {} } as IRNode;
|
|
70
|
+
expect(groupOf(n, { by: "kind" })).toBe("AWS::EC2::Instance");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("reads the type off the id when the address attribute is absent", () => {
|
|
74
|
+
const n = { id: "app/module.cdn/aws_s3_bucket.logs~2", kind: "Terraform::Resource", lexicon: "terraform", attrs: {} } as IRNode;
|
|
75
|
+
expect(terraformType(n)).toBe("aws_s3_bucket");
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("--count-by module", () => {
|
|
80
|
+
it("takes the module from the address, and a root-module block is (root)", () => {
|
|
81
|
+
expect(countGroups(app, { by: "module" })).toEqual([
|
|
82
|
+
{ group: "(root)", count: 5 },
|
|
83
|
+
{ group: "module.cdn", count: 1 },
|
|
84
|
+
{ group: "module.cdn.module.bucket", count: 1 },
|
|
85
|
+
]);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("reads the call chain off the entity key when callers is not recorded", () => {
|
|
89
|
+
const n = { id: "app/module.cdn/aws_s3_bucket.logs", kind: "Terraform::Resource", lexicon: "terraform", attrs: {} } as IRNode;
|
|
90
|
+
expect(groupOf(n, { by: "module" })).toBe("module.cdn");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("qualifies the module with its root when the graph holds more than one root", () => {
|
|
94
|
+
const nodes = [...app, tf("network", "aws_vpc.main", "Terraform::Resource"), tf("network", "aws_subnet.a", "Terraform::Resource", ["module.cdn"])];
|
|
95
|
+
const ir = { nodes, edges: [] } as unknown as GraphIR;
|
|
96
|
+
expect(hasManyTerraformRoots(ir)).toBe(true);
|
|
97
|
+
expect(hasManyTerraformRoots({ nodes: app, edges: [] } as unknown as GraphIR)).toBe(false);
|
|
98
|
+
const groups = countGroups(nodes, { by: "module" }, true);
|
|
99
|
+
expect(groups.map((g) => g.group)).toEqual([
|
|
100
|
+
"app/(root)",
|
|
101
|
+
"app/module.cdn",
|
|
102
|
+
"app/module.cdn.module.bucket",
|
|
103
|
+
"network/(root)",
|
|
104
|
+
"network/module.cdn",
|
|
105
|
+
]);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("groups a node of another lexicon by its composite, or (none)", () => {
|
|
109
|
+
const inComposite = { id: "a", kind: "AWS::S3::Bucket", lexicon: "aws", attrs: {}, compositeParent: "Site" } as IRNode;
|
|
110
|
+
const bare = { id: "b", kind: "AWS::S3::Bucket", lexicon: "aws", attrs: {} } as IRNode;
|
|
111
|
+
expect(groupOf(inComposite, { by: "module" })).toBe("Site");
|
|
112
|
+
expect(groupOf(bare, { by: "module" })).toBe("(none)");
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe("--count-by attr:<name>", () => {
|
|
117
|
+
it("counts a node lacking the attribute under (none), and the groups add up to the total", () => {
|
|
118
|
+
const by: CountBy = { by: "attr", name: "expansion" };
|
|
119
|
+
const groups = countGroups(app, by);
|
|
120
|
+
expect(groups).toEqual([
|
|
121
|
+
{ group: "(none)", count: 6 },
|
|
122
|
+
{ group: "count", count: 1 },
|
|
123
|
+
]);
|
|
124
|
+
// Never dropped: every matched node is in exactly one group.
|
|
125
|
+
expect(sum(groups)).toBe(app.length);
|
|
126
|
+
const lines = formatCounts(groups, by);
|
|
127
|
+
expect(lines.at(-1)).toBe(`— ${app.length} total · 2 groups by attr:expansion`);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("adds up for every grouping, on a mixed graph with gaps", () => {
|
|
131
|
+
const mixed = [
|
|
132
|
+
...app,
|
|
133
|
+
{ id: "web", kind: "AWS::EC2::Instance", lexicon: "aws", attrs: { region: "us-east-1" } } as IRNode,
|
|
134
|
+
{ id: "db", kind: "AWS::RDS::DBInstance", lexicon: "aws", attrs: { region: null } } as IRNode,
|
|
135
|
+
{ id: "odd", kind: "", lexicon: "aws", attrs: { region: "" } } as unknown as IRNode,
|
|
136
|
+
];
|
|
137
|
+
for (const by of [{ by: "kind" }, { by: "module" }, { by: "attr", name: "region" }, { by: "attr", name: "nope" }] as CountBy[]) {
|
|
138
|
+
expect(sum(countGroups(mixed, by))).toBe(mixed.length);
|
|
139
|
+
}
|
|
140
|
+
expect(countGroups(mixed, { by: "attr", name: "region" })).toEqual([
|
|
141
|
+
{ group: "(none)", count: 9 },
|
|
142
|
+
{ group: "us-east-1", count: 1 },
|
|
143
|
+
]);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("matches the attribute name case-insensitively, as --show does", () => {
|
|
147
|
+
const n = { id: "web", kind: "AWS::EC2::Instance", lexicon: "aws", attrs: { Region: "eu-west-1" } } as IRNode;
|
|
148
|
+
expect(groupOf(n, { by: "attr", name: "region" })).toBe("eu-west-1");
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe("grouped output", () => {
|
|
153
|
+
it("prints one line per group, sorted by count then name, and a total line", () => {
|
|
154
|
+
const groups = countGroups(
|
|
155
|
+
[
|
|
156
|
+
{ id: "1", kind: "B", lexicon: "x", attrs: {} },
|
|
157
|
+
{ id: "2", kind: "A", lexicon: "x", attrs: {} },
|
|
158
|
+
{ id: "3", kind: "C", lexicon: "x", attrs: {} },
|
|
159
|
+
{ id: "4", kind: "C", lexicon: "x", attrs: {} },
|
|
160
|
+
] as IRNode[],
|
|
161
|
+
{ by: "kind" },
|
|
162
|
+
);
|
|
163
|
+
expect(formatCounts(groups, { by: "kind" })).toEqual(["2 C", "1 A", "1 B", "— 4 total · 3 groups by kind"]);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("right-aligns the counts", () => {
|
|
167
|
+
const nodes = Array.from({ length: 11 }, (_, i) => ({ id: `n${i}`, kind: i === 0 ? "Z" : "Y", lexicon: "x", attrs: {} })) as IRNode[];
|
|
168
|
+
expect(formatCounts(countGroups(nodes, { by: "kind" }), { by: "kind" })).toEqual([
|
|
169
|
+
"10 Y",
|
|
170
|
+
" 1 Z",
|
|
171
|
+
"— 11 total · 2 groups by kind",
|
|
172
|
+
]);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe("--count-by --json lists each group's ids (#4382)", () => {
|
|
177
|
+
it("lists the matches of each group in match order, the count is their number, and the groups are countGroups'", () => {
|
|
178
|
+
for (const by of [{ by: "kind" }, { by: "module" }, { by: "attr", name: "expansion" }] as CountBy[]) {
|
|
179
|
+
const groups = groupMembers(app, by);
|
|
180
|
+
expect(groups.map(({ group, count }) => ({ group, count }))).toEqual(countGroups(app, by));
|
|
181
|
+
for (const g of groups) expect(g.ids).toHaveLength(g.count);
|
|
182
|
+
// Every match is in exactly one group.
|
|
183
|
+
expect(groups.flatMap((g) => g.ids).sort()).toEqual(app.map((n) => n.id).sort());
|
|
184
|
+
}
|
|
185
|
+
expect(groupMembers(app, { by: "module" })[0]).toEqual({
|
|
186
|
+
group: "(root)",
|
|
187
|
+
count: 5,
|
|
188
|
+
ids: ["app/aws_instance.web", "app/aws_instance.worker", "app/aws_s3_bucket.assets", "app/data.aws_iam_policy.boundary", "app/var.region"],
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
});
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import type { GraphIR, IRNode } from "../../graph-ir";
|
|
2
|
+
import { terraformType } from "../../graph-effective";
|
|
3
|
+
|
|
4
|
+
// The one reading of a terraform node's type lives with the folds that also
|
|
5
|
+
// select on it, so `--count-by kind` and `kind:` cannot disagree (#4366).
|
|
6
|
+
export { terraformType };
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* `chant search --count-by <kind|module|attr:<name>>` (#4360): grouped counts
|
|
10
|
+
* over the matched set, one line per group, instead of one row per match.
|
|
11
|
+
*
|
|
12
|
+
* An agent asking "how many resources of each type does each module own" used
|
|
13
|
+
* to run one search per value, or read every row and count them itself. The
|
|
14
|
+
* second is the token cost `search` exists to avoid.
|
|
15
|
+
*
|
|
16
|
+
* Every matched node lands in exactly one group. A node with no value for the
|
|
17
|
+
* grouped dimension is counted under `(none)`, never dropped, so the groups
|
|
18
|
+
* always add up to the total line.
|
|
19
|
+
*/
|
|
20
|
+
export type CountBy = { by: "kind" } | { by: "module" } | { by: "attr"; name: string };
|
|
21
|
+
|
|
22
|
+
/** The group a node with no value for the grouped dimension is counted under. */
|
|
23
|
+
export const NONE_GROUP = "(none)";
|
|
24
|
+
/** The module group of a Terraform block declared in the root module itself. */
|
|
25
|
+
export const ROOT_MODULE_GROUP = "(root)";
|
|
26
|
+
|
|
27
|
+
/** Parse a `--count-by` value, or say what the accepted spellings are. */
|
|
28
|
+
export function parseCountBy(raw: string): CountBy | { error: string; hint: string } {
|
|
29
|
+
const value = raw.trim();
|
|
30
|
+
if (value === "kind") return { by: "kind" };
|
|
31
|
+
if (value === "module") return { by: "module" };
|
|
32
|
+
const attr = /^attr:(.+)$/.exec(value);
|
|
33
|
+
if (attr && attr[1].trim()) return { by: "attr", name: attr[1].trim() };
|
|
34
|
+
return {
|
|
35
|
+
error: `--count-by "${raw}" is not a grouping`,
|
|
36
|
+
hint: "group by one of: --count-by kind, --count-by module, --count-by attr:<name>",
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** A Terraform node's root name, from its attrs or the first segment of its id. */
|
|
41
|
+
function terraformRoot(n: IRNode): string {
|
|
42
|
+
const attrs = (n.attrs ?? {}) as Record<string, unknown>;
|
|
43
|
+
if (typeof attrs.root === "string") return attrs.root;
|
|
44
|
+
const slash = n.id.indexOf("/");
|
|
45
|
+
return slash === -1 ? "" : n.id.slice(0, slash);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The module a Terraform node sits in: its call chain as Terraform writes it
|
|
50
|
+
* (`module.cdn`, `module.cdn.module.bucket`), or `(root)` for a block of the
|
|
51
|
+
* root module itself. The chain is on `attrs.callers`; an entity key
|
|
52
|
+
* `<root>/module.cdn/<address>` carries the same chain when it is absent.
|
|
53
|
+
*/
|
|
54
|
+
function terraformModule(n: IRNode): string {
|
|
55
|
+
const attrs = (n.attrs ?? {}) as Record<string, unknown>;
|
|
56
|
+
const callers = Array.isArray(attrs.callers)
|
|
57
|
+
? attrs.callers.filter((c): c is string => typeof c === "string")
|
|
58
|
+
: n.id.split("/").slice(1, -1);
|
|
59
|
+
return callers.length === 0 ? ROOT_MODULE_GROUP : callers.join(".");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** An attribute's value as a group name, matched by exact name and then case-insensitively, as `--show` does. */
|
|
63
|
+
function attrGroup(n: IRNode, name: string): string {
|
|
64
|
+
const attrs = (n.attrs ?? {}) as Record<string, unknown>;
|
|
65
|
+
const key = name in attrs ? name : Object.keys(attrs).find((k) => k.toLowerCase() === name.toLowerCase());
|
|
66
|
+
const v = key === undefined ? undefined : attrs[key];
|
|
67
|
+
if (v === undefined || v === null || v === "") return NONE_GROUP;
|
|
68
|
+
return typeof v === "object" ? JSON.stringify(v) : String(v);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The group one node belongs to.
|
|
73
|
+
*
|
|
74
|
+
* `module` on a Terraform node is its call chain. When the graph holds more
|
|
75
|
+
* than one Terraform root, the chain is prefixed with the root (`app/(root)`,
|
|
76
|
+
* `net/module.vpc`): `module.vpc` in two roots is two modules, applied
|
|
77
|
+
* separately, and counting them as one would add up resources that never sit
|
|
78
|
+
* in the same state. A node of another lexicon groups under the composite that
|
|
79
|
+
* expanded it, chant's own unit of reuse, or `(none)`.
|
|
80
|
+
*/
|
|
81
|
+
export function groupOf(n: IRNode, countBy: CountBy, multiRoot = false): string {
|
|
82
|
+
if (countBy.by === "kind") return terraformType(n) ?? (n.kind || NONE_GROUP);
|
|
83
|
+
if (countBy.by === "attr") return attrGroup(n, countBy.name);
|
|
84
|
+
if (n.lexicon === "terraform") {
|
|
85
|
+
const module = terraformModule(n);
|
|
86
|
+
return multiRoot ? `${terraformRoot(n) || NONE_GROUP}/${module}` : module;
|
|
87
|
+
}
|
|
88
|
+
return n.compositeParent || NONE_GROUP;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Whether the graph holds blocks from more than one Terraform root. */
|
|
92
|
+
export function hasManyTerraformRoots(ir: GraphIR): boolean {
|
|
93
|
+
const roots = new Set<string>();
|
|
94
|
+
for (const n of ir.nodes) if (n.lexicon === "terraform") roots.add(terraformRoot(n));
|
|
95
|
+
return roots.size > 1;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Count `matches` per group, sorted by count (highest first) and then by name. */
|
|
99
|
+
export function countGroups(matches: IRNode[], countBy: CountBy, multiRoot = false): Array<{ group: string; count: number }> {
|
|
100
|
+
return groupMembers(matches, countBy, multiRoot).map(({ group, count }) => ({ group, count }));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The groups with the ids of the matches in each, in match order (#4382), in
|
|
105
|
+
* the order {@link countGroups} gives. `count` is always `ids.length`. A
|
|
106
|
+
* reader that draws the groups (behold#922) lights each group's nodes from
|
|
107
|
+
* `ids`, so it never has to read a node's group itself.
|
|
108
|
+
*/
|
|
109
|
+
export function groupMembers(matches: IRNode[], countBy: CountBy, multiRoot = false): Array<{ group: string; count: number; ids: string[] }> {
|
|
110
|
+
const ids = new Map<string, string[]>();
|
|
111
|
+
for (const n of matches) {
|
|
112
|
+
const g = groupOf(n, countBy, multiRoot);
|
|
113
|
+
const list = ids.get(g);
|
|
114
|
+
if (list) list.push(n.id);
|
|
115
|
+
else ids.set(g, [n.id]);
|
|
116
|
+
}
|
|
117
|
+
return [...ids]
|
|
118
|
+
.map(([group, members]) => ({ group, count: members.length, ids: members }))
|
|
119
|
+
.sort((a, b) => b.count - a.count || (a.group < b.group ? -1 : a.group > b.group ? 1 : 0));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** `--count-by` with `--json`: the groups, in printed order, each with its matched ids, and the total they add up to. */
|
|
123
|
+
export interface CountsJson {
|
|
124
|
+
query: string;
|
|
125
|
+
source: unknown;
|
|
126
|
+
countBy: string;
|
|
127
|
+
groups: Array<{ group: string; count: number; ids: string[] }>;
|
|
128
|
+
total: number;
|
|
129
|
+
liveFailures?: string[];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function countsJson(
|
|
133
|
+
query: string,
|
|
134
|
+
source: unknown,
|
|
135
|
+
groups: Array<{ group: string; count: number; ids: string[] }>,
|
|
136
|
+
countBy: CountBy,
|
|
137
|
+
liveFailures: string[] = [],
|
|
138
|
+
): CountsJson {
|
|
139
|
+
return {
|
|
140
|
+
query,
|
|
141
|
+
source,
|
|
142
|
+
countBy: countByLabel(countBy),
|
|
143
|
+
groups,
|
|
144
|
+
total: groups.reduce((sum, g) => sum + g.count, 0),
|
|
145
|
+
...(liveFailures.length > 0 ? { liveFailures } : {}),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** The label a `--count-by` value is printed with on the total line. */
|
|
150
|
+
function countByLabel(countBy: CountBy): string {
|
|
151
|
+
return countBy.by === "attr" ? `attr:${countBy.name}` : countBy.by;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Render grouped counts: one `<count> <group>` line per group, counts right
|
|
156
|
+
* aligned, then the total. The total is the number of matches, which is the
|
|
157
|
+
* sum of the group counts by construction.
|
|
158
|
+
*/
|
|
159
|
+
export function formatCounts(groups: Array<{ group: string; count: number }>, countBy: CountBy): string[] {
|
|
160
|
+
const total = groups.reduce((sum, g) => sum + g.count, 0);
|
|
161
|
+
const width = String(groups[0]?.count ?? 0).length;
|
|
162
|
+
const lines = groups.map((g) => `${String(g.count).padStart(width)} ${g.group}`);
|
|
163
|
+
lines.push(`— ${total} total · ${groups.length} group${groups.length === 1 ? "" : "s"} by ${countByLabel(countBy)}`);
|
|
164
|
+
return lines;
|
|
165
|
+
}
|