@intentius/chant 0.32.0 → 0.33.0
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/cli/handlers/graph.d.ts.map +1 -1
- package/dist/cli/handlers/search.d.ts +58 -0
- package/dist/cli/handlers/search.d.ts.map +1 -0
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +4 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/config.d.ts +3 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/graph-declared.d.ts +20 -0
- package/dist/graph-declared.d.ts.map +1 -0
- package/dist/graph-effective.d.ts +25 -0
- package/dist/graph-effective.d.ts.map +1 -0
- package/dist/lexicon.d.ts +9 -0
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/observe.d.ts +14 -6
- package/dist/lifecycle/observe.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli/handlers/graph.test.ts +1 -1
- package/src/cli/handlers/graph.ts +33 -11
- package/src/cli/handlers/search.test.ts +113 -0
- package/src/cli/handlers/search.ts +263 -0
- package/src/cli/main.ts +7 -0
- package/src/cli/registry.ts +4 -0
- package/src/config.ts +3 -0
- package/src/graph-declared.ts +33 -0
- package/src/graph-effective.test.ts +97 -0
- package/src/graph-effective.ts +110 -0
- package/src/lexicon.ts +6 -1
- package/src/lifecycle/observe.test.ts +66 -2
- package/src/lifecycle/observe.ts +79 -18
|
@@ -1,9 +1,25 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
2
|
import { observeResources } from "./observe";
|
|
3
3
|
import { observation } from "../observation";
|
|
4
4
|
import type { ObservationLexicon, ResourceMetadata } from "../lexicon";
|
|
5
5
|
import type { BuildResult } from "../build";
|
|
6
6
|
|
|
7
|
+
// The per-stack scoped build (#1162) resolves each stack's `src` through the
|
|
8
|
+
// real `build`. Mock it so the test controls what each src yields — a stack
|
|
9
|
+
// whose src is scoped reports BARE entity names, matching the deployed
|
|
10
|
+
// LogicalResourceIds, not the whole-project build's disambiguated names.
|
|
11
|
+
const scopedBuilds: Record<string, string[]> = {};
|
|
12
|
+
vi.mock("../build", () => ({
|
|
13
|
+
build: async (src: string): Promise<BuildResult> => {
|
|
14
|
+
const names = scopedBuilds[src] ?? [];
|
|
15
|
+
return {
|
|
16
|
+
outputs: new Map<string, string>([["aws", "{}"]]),
|
|
17
|
+
entities: new Map(names.map((n) => [n, { lexicon: "aws", entityType: "AWS::EC2::Instance", props: {} }])),
|
|
18
|
+
errors: [],
|
|
19
|
+
} as unknown as BuildResult;
|
|
20
|
+
},
|
|
21
|
+
}));
|
|
22
|
+
|
|
7
23
|
function mockBuild(): BuildResult {
|
|
8
24
|
return {
|
|
9
25
|
outputs: new Map<string, string>([["aws", "{}"]]),
|
|
@@ -154,7 +170,11 @@ describe("observeResources", () => {
|
|
|
154
170
|
awsPlugin((opts) => {
|
|
155
171
|
const stack = (opts as { stack?: string }).stack;
|
|
156
172
|
calls.push(stack);
|
|
157
|
-
// Different resources per stack — the multi-stack, per-component case
|
|
173
|
+
// Different resources per stack — the multi-stack, per-component case
|
|
174
|
+
// (#57 loomster). Bare-string stacks keep BARE ids (no `src` scope), so
|
|
175
|
+
// the union is `db-a`+`db-b`, not stack-qualified: per-component ids are
|
|
176
|
+
// already unique and behold reads them bare. Qualification is a scoped
|
|
177
|
+
// (`src`) feature — see the per-stack src test below.
|
|
158
178
|
const resources: Record<string, ResourceMetadata> =
|
|
159
179
|
stack === "s1"
|
|
160
180
|
? { "db-a": { type: "AWS::RDS::DBInstance", status: "AVAILABLE" } }
|
|
@@ -169,6 +189,50 @@ describe("observeResources", () => {
|
|
|
169
189
|
expect(Object.keys(observations[0].resources).sort()).toEqual(["db-a", "db-b"]);
|
|
170
190
|
});
|
|
171
191
|
|
|
192
|
+
it("with per-stack src (#1162): describeResources gets each stack's SCOPED bare names, not the whole-project build's names", async () => {
|
|
193
|
+
const { resolve } = await import("node:path");
|
|
194
|
+
// The whole-project build disambiguates colliding names by module path;
|
|
195
|
+
// each stack's scoped src reports the bare names it actually deploys.
|
|
196
|
+
scopedBuilds[resolve("east/src")] = ["server", "vpc"];
|
|
197
|
+
scopedBuilds[resolve("west/src")] = ["server", "vpc"];
|
|
198
|
+
|
|
199
|
+
const seen: Record<string, string[]> = {};
|
|
200
|
+
const plugins = [
|
|
201
|
+
awsPlugin((opts) => {
|
|
202
|
+
const o = opts as { stack?: string; entityNames: string[] };
|
|
203
|
+
seen[o.stack ?? "?"] = o.entityNames;
|
|
204
|
+
// Echo one resource keyed by a bare name the deployed stack owns.
|
|
205
|
+
return { server: { type: "AWS::EC2::Instance", status: "AVAILABLE", physicalId: `i-${o.stack}` } };
|
|
206
|
+
}),
|
|
207
|
+
];
|
|
208
|
+
// The whole-project buildResult carries DISAMBIGUATED names — proving the
|
|
209
|
+
// scoped path overrides them rather than falling through to these.
|
|
210
|
+
const wholeProject = {
|
|
211
|
+
outputs: new Map<string, string>([["aws", "{}"]]),
|
|
212
|
+
entities: new Map([
|
|
213
|
+
["EastServer", { lexicon: "aws", entityType: "AWS::EC2::Instance", props: {} }],
|
|
214
|
+
["WestServer", { lexicon: "aws", entityType: "AWS::EC2::Instance", props: {} }],
|
|
215
|
+
]),
|
|
216
|
+
errors: [],
|
|
217
|
+
} as unknown as BuildResult;
|
|
218
|
+
|
|
219
|
+
const { observations } = await observeResources("floci", plugins, wholeProject, {
|
|
220
|
+
stacks: [
|
|
221
|
+
{ name: "east", src: "east/src" },
|
|
222
|
+
{ name: "west", src: "west/src" },
|
|
223
|
+
],
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// Each stack saw its own scoped bare names (matching deployed ids).
|
|
227
|
+
expect(seen["east"]).toEqual(["server", "vpc"]);
|
|
228
|
+
expect(seen["west"]).toEqual(["server", "vpc"]);
|
|
229
|
+
// Observed nodes are stack-qualified, so the colliding `server` id is
|
|
230
|
+
// distinct per stack and each carries its own physical id.
|
|
231
|
+
expect(Object.keys(observations[0].resources).sort()).toEqual(["east::server", "west::server"]);
|
|
232
|
+
expect(observations[0].resources["east::server"].physicalId).toBe("i-east");
|
|
233
|
+
expect(observations[0].resources["west::server"].physicalId).toBe("i-west");
|
|
234
|
+
});
|
|
235
|
+
|
|
172
236
|
it("with an empty stacks array — falls back to the single unstacked call", async () => {
|
|
173
237
|
const calls: Array<{ stack?: string }> = [];
|
|
174
238
|
const plugins = [
|
package/src/lifecycle/observe.ts
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import type { ObservationLexicon } from "../lexicon";
|
|
13
13
|
import type { BuildResult } from "../build";
|
|
14
|
+
import { build as buildProject } from "../build";
|
|
15
|
+
import { resolve as resolvePath } from "node:path";
|
|
14
16
|
import type { SerializerResult } from "../serializer";
|
|
15
17
|
import type { LiveObservation } from "../graph-ir";
|
|
16
18
|
import {
|
|
@@ -28,6 +30,19 @@ export interface ObserveResult {
|
|
|
28
30
|
errors: string[];
|
|
29
31
|
}
|
|
30
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Re-key a normalized observation's entities by `${stack}::${id}` (#1162) so a
|
|
35
|
+
* bare LogicalResourceId shared across stacks (e.g. `vpc`) stays unambiguous
|
|
36
|
+
* once the per-stack results are merged. The declared canvas qualifies the same
|
|
37
|
+
* way (`buildDeclaredPerStack`), so the overlay join lines up. Applies to both
|
|
38
|
+
* the OBSERVED-PRESENT and NOT-OBSERVED maps of the tri-state (#1089).
|
|
39
|
+
*/
|
|
40
|
+
function qualifyObservation(obs: NormalizedObservation, stackName: string): NormalizedObservation {
|
|
41
|
+
const q = <T>(m: Record<string, T>): Record<string, T> =>
|
|
42
|
+
Object.fromEntries(Object.entries(m).map(([k, v]) => [`${stackName}::${k}`, v]));
|
|
43
|
+
return { resources: q(obs.resources), unobserved: q(obs.unobserved) };
|
|
44
|
+
}
|
|
45
|
+
|
|
31
46
|
/**
|
|
32
47
|
* Query every plugin that implements `describeResources` for its resources in
|
|
33
48
|
* `environment`. `owned` (default true for the managed-only diagram, epic #776)
|
|
@@ -44,20 +59,39 @@ export interface ObserveResult {
|
|
|
44
59
|
* absent an explicit `stack`) queries a stack that simply doesn't exist there,
|
|
45
60
|
* so the single-call path always observes zero nodes. When `stacks` is
|
|
46
61
|
* present and non-empty, each observing plugin's `describeResources` is
|
|
47
|
-
* called once per stack
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
62
|
+
* called once per stack and the returned observations are merged. A stack entry
|
|
63
|
+
* may be a bare name or `{ name, region?, src? }` (#1162): `src` is built
|
|
64
|
+
* SCOPED so the deployed BARE LogicalResourceIds match (the whole-project build
|
|
65
|
+
* disambiguates colliding names to `UsWest1Src…`, which the live ids never
|
|
66
|
+
* carry), and a scoped stack's observed ids are qualified `${stack}::${id}` so
|
|
67
|
+
* the same bare id in two stacks stays distinct. A bare-string stack keeps its
|
|
68
|
+
* bare ids and the tri-state merge (#57). When `stacks` is absent or empty, behavior is
|
|
69
|
+
* exactly the single call of before (no `stack` key at all), so a single-stack
|
|
70
|
+
* project is unaffected.
|
|
52
71
|
*/
|
|
53
72
|
export async function observeResources(
|
|
54
73
|
environment: string,
|
|
55
74
|
plugins: ObservationLexicon[],
|
|
56
75
|
buildResult: BuildResult,
|
|
57
|
-
opts?: { owned?: boolean; stacks?: string
|
|
76
|
+
opts?: { owned?: boolean; stacks?: Array<string | { name: string; region?: string; src?: string }> },
|
|
58
77
|
): Promise<ObserveResult> {
|
|
59
78
|
const owned = opts?.owned ?? true;
|
|
60
|
-
const stacks = opts?.stacks ?? [];
|
|
79
|
+
const stacks = (opts?.stacks ?? []).map((st) => (typeof st === "string" ? { name: st } : st));
|
|
80
|
+
// A stack's `src` (multi-stack, #1162) is built SCOPED to recover that stack's
|
|
81
|
+
// BARE entity names — the names it actually deploys. Matching deployed bare
|
|
82
|
+
// LogicalResourceIds against the whole-project build's DISAMBIGUATED names
|
|
83
|
+
// (UsWest1Src…) misses every colliding resource. Cached per src.
|
|
84
|
+
const serializers = plugins.map((p) => p.serializer);
|
|
85
|
+
const scopedBuildCache = new Map<string, BuildResult>();
|
|
86
|
+
const scopedBuild = async (src: string): Promise<BuildResult> => {
|
|
87
|
+
const key = resolvePath(src);
|
|
88
|
+
let r = scopedBuildCache.get(key);
|
|
89
|
+
if (!r) {
|
|
90
|
+
r = await buildProject(key, serializers);
|
|
91
|
+
scopedBuildCache.set(key, r);
|
|
92
|
+
}
|
|
93
|
+
return r;
|
|
94
|
+
};
|
|
61
95
|
const observations: LiveObservation[] = [];
|
|
62
96
|
const warnings: string[] = [];
|
|
63
97
|
const errors: string[] = [];
|
|
@@ -91,18 +125,45 @@ export async function observeResources(
|
|
|
91
125
|
if (stacks.length > 0) {
|
|
92
126
|
const parts: NormalizedObservation[] = [];
|
|
93
127
|
for (const stack of stacks) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
128
|
+
// Use this stack's scoped build (bare entity names) when it has a src,
|
|
129
|
+
// so describeResources matches the deployed bare LogicalResourceIds.
|
|
130
|
+
let stackEntityNames = entityNames;
|
|
131
|
+
let stackBuildOutput = buildOutput;
|
|
132
|
+
let stackEntities = entities;
|
|
133
|
+
if (stack.src) {
|
|
134
|
+
const sb = await scopedBuild(stack.src);
|
|
135
|
+
stackEntityNames = [];
|
|
136
|
+
stackEntities = new Map();
|
|
137
|
+
for (const [name, entity] of sb.entities) {
|
|
138
|
+
if (entity.lexicon !== plugin.name) continue;
|
|
139
|
+
stackEntityNames.push(name);
|
|
140
|
+
stackEntities.set(name, {
|
|
141
|
+
entityType: entity.entityType,
|
|
142
|
+
props: ("props" in entity && entity.props != null ? entity.props : {}) as Record<string, unknown>,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
const raw = sb.outputs.get(plugin.name);
|
|
146
|
+
stackBuildOutput = raw === undefined ? "" : typeof raw === "string" ? raw : (raw as SerializerResult).primary;
|
|
147
|
+
}
|
|
148
|
+
const norm = normalizeObservation(
|
|
149
|
+
await plugin.describeResources({
|
|
150
|
+
environment,
|
|
151
|
+
buildOutput: stackBuildOutput,
|
|
152
|
+
entityNames: stackEntityNames,
|
|
153
|
+
entities: stackEntities,
|
|
154
|
+
owned,
|
|
155
|
+
stack: stack.name,
|
|
156
|
+
region: stack.region,
|
|
157
|
+
}),
|
|
105
158
|
);
|
|
159
|
+
// Qualify ids by stack ONLY for a scoped (`src`) stack (#1162): that
|
|
160
|
+
// is the multi-region case where the SAME bare LogicalResourceId
|
|
161
|
+
// (e.g. `vpc`) exists in every stack, so a bare union would collide.
|
|
162
|
+
// A bare-string stack (#57 loomster) has unique per-component ids and
|
|
163
|
+
// is asked the whole-project entity set, so it keeps the bare-id
|
|
164
|
+
// tri-state merge (present > not-observed > absent) that behold and
|
|
165
|
+
// other consumers read.
|
|
166
|
+
parts.push(stack.src ? qualifyObservation(norm, stack.name) : norm);
|
|
106
167
|
}
|
|
107
168
|
observed = mergeObservations(parts);
|
|
108
169
|
} else {
|