@intentius/chant 0.33.1 → 0.34.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/commands/onboard.d.ts.map +1 -1
- package/dist/cli/handlers/graph.d.ts.map +1 -1
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/handlers/search.d.ts +49 -1
- package/dist/cli/handlers/search.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +26 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/graph-ir.d.ts +14 -0
- package/dist/graph-ir.d.ts.map +1 -1
- package/dist/graph-refs.d.ts +19 -0
- package/dist/graph-refs.d.ts.map +1 -1
- package/dist/lexicon.d.ts +141 -0
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/deep-observe.d.ts +4 -0
- package/dist/lifecycle/deep-observe.d.ts.map +1 -1
- package/dist/lifecycle/live-diff.d.ts.map +1 -1
- package/dist/lifecycle/observe.d.ts +55 -1
- package/dist/lifecycle/observe.d.ts.map +1 -1
- package/dist/lifecycle/replay.d.ts +47 -0
- package/dist/lifecycle/replay.d.ts.map +1 -0
- package/dist/lifecycle/snapshot.d.ts +6 -0
- package/dist/lifecycle/snapshot.d.ts.map +1 -1
- package/dist/lifecycle/types.d.ts +46 -0
- package/dist/lifecycle/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/onboard.ts +10 -25
- package/src/cli/handlers/graph.test.ts +74 -0
- package/src/cli/handlers/graph.ts +77 -36
- package/src/cli/handlers/lifecycle.test.ts +86 -0
- package/src/cli/handlers/lifecycle.ts +43 -10
- package/src/cli/handlers/search.test.ts +200 -4
- package/src/cli/handlers/search.ts +383 -29
- package/src/cli/main.ts +9 -0
- package/src/cli/registry.ts +27 -0
- package/src/codegen/lexicon-wiring.test.ts +53 -0
- package/src/codegen/release-wiring.test.ts +92 -0
- package/src/graph-ir-live.test.ts +83 -0
- package/src/graph-ir.ts +51 -1
- package/src/graph-refs.test.ts +59 -0
- package/src/graph-refs.ts +39 -8
- package/src/lexicon.ts +145 -0
- package/src/lifecycle/deep-observe.ts +5 -0
- package/src/lifecycle/live-diff.test.ts +38 -0
- package/src/lifecycle/live-diff.ts +45 -2
- package/src/lifecycle/observe.ts +186 -4
- package/src/lifecycle/replay.ts +141 -0
- package/src/lifecycle/snapshot.test.ts +179 -0
- package/src/lifecycle/snapshot.ts +88 -3
- package/src/lifecycle/types.ts +47 -0
|
@@ -5,8 +5,11 @@
|
|
|
5
5
|
import type { ObservationLexicon, ResourceMetadata, ArtifactMetadata } from "../lexicon";
|
|
6
6
|
import type { BuildResult } from "../build";
|
|
7
7
|
import type { SerializerResult } from "../serializer";
|
|
8
|
-
import type { LifecycleSnapshot } from "./types";
|
|
8
|
+
import type { LifecycleSnapshot, ObservationDepth } from "./types";
|
|
9
|
+
import { observeDeep } from "./deep-observe";
|
|
10
|
+
import type { DeepResourceObservation } from "../deep-observation";
|
|
9
11
|
import { computeBuildDigest } from "./digest";
|
|
12
|
+
import { collectDependencies, collectAmbient } from "./observe";
|
|
10
13
|
import { writeSnapshot, snapshotStorageKey, getHeadCommit, pushLifecycle } from "./git";
|
|
11
14
|
import { sortedJsonReplacer } from "../utils";
|
|
12
15
|
import { formatUnobserved, normalizeObservation, unobservedAll, type UnobservedEntity } from "../observation";
|
|
@@ -80,9 +83,24 @@ export async function takeSnapshot(
|
|
|
80
83
|
environment: string,
|
|
81
84
|
plugins: ObservationLexicon[],
|
|
82
85
|
buildResult: BuildResult,
|
|
83
|
-
opts?: {
|
|
86
|
+
opts?: {
|
|
87
|
+
cwd?: string;
|
|
88
|
+
stack?: string;
|
|
89
|
+
region?: string;
|
|
90
|
+
deep?: boolean;
|
|
91
|
+
ambient?: boolean;
|
|
92
|
+
/** Kinds the PROJECT manages, not just this stack — a region whose stack
|
|
93
|
+
* declares no security group still has a default one (#1278). */
|
|
94
|
+
ambientKinds?: string[];
|
|
95
|
+
},
|
|
84
96
|
): Promise<TakeSnapshotResult> {
|
|
85
97
|
const stack = opts?.stack;
|
|
98
|
+
const depth: ObservationDepth = opts?.deep ? "deep" : "identity";
|
|
99
|
+
// A stack declares the region it deploys to (#1261). Passing it through is
|
|
100
|
+
// what lets a multi-region estate be observed at all: the reader targets the
|
|
101
|
+
// stack's own region rather than whichever one the shell happens to be set
|
|
102
|
+
// to, so out-of-region stacks stop coming back empty.
|
|
103
|
+
const region = opts?.region;
|
|
86
104
|
const warnings: string[] = [];
|
|
87
105
|
const errors: string[] = [];
|
|
88
106
|
const snapshots: LifecycleSnapshot[] = [];
|
|
@@ -131,6 +149,7 @@ export async function takeSnapshot(
|
|
|
131
149
|
entityNames,
|
|
132
150
|
entities,
|
|
133
151
|
stack,
|
|
152
|
+
region,
|
|
134
153
|
}),
|
|
135
154
|
);
|
|
136
155
|
const { valid, dropped, warnings: validationWarnings } = validateResources(observed.resources);
|
|
@@ -149,6 +168,8 @@ export async function takeSnapshot(
|
|
|
149
168
|
}
|
|
150
169
|
|
|
151
170
|
if (plugin.listArtifacts) {
|
|
171
|
+
// No region: artifacts are registry/chart objects (docker, helm), not
|
|
172
|
+
// regional cloud resources, and `listArtifacts` takes no region.
|
|
152
173
|
const raw = await plugin.listArtifacts({ environment, entities, stack });
|
|
153
174
|
const { valid, dropped, warnings: validationWarnings } = validateResources(raw);
|
|
154
175
|
warnings.push(...validationWarnings);
|
|
@@ -168,15 +189,79 @@ export async function takeSnapshot(
|
|
|
168
189
|
continue;
|
|
169
190
|
}
|
|
170
191
|
|
|
192
|
+
// The deep read is a second pass, after identity is known to be readable
|
|
193
|
+
// (#1267). Keeping it separate means a lexicon with no deep reader still
|
|
194
|
+
// snapshots exactly as before, and a deep read that comes back empty
|
|
195
|
+
// downgrades the record rather than discarding an identity snapshot that
|
|
196
|
+
// was already good.
|
|
197
|
+
let properties: Record<string, DeepResourceObservation> | undefined;
|
|
198
|
+
let recordedDepth: ObservationDepth = "identity";
|
|
199
|
+
if (depth === "deep") {
|
|
200
|
+
if (!plugin.observeResourcesDeep) {
|
|
201
|
+
warnings.push(
|
|
202
|
+
`${plugin.name}: no deep reader — recording an identity snapshot; property questions cannot be answered from it`,
|
|
203
|
+
);
|
|
204
|
+
} else {
|
|
205
|
+
const observed = await observeDeep(plugin, {
|
|
206
|
+
environment,
|
|
207
|
+
buildOutput,
|
|
208
|
+
entities,
|
|
209
|
+
...(stack ? { stack } : {}),
|
|
210
|
+
...(region ? { region } : {}),
|
|
211
|
+
});
|
|
212
|
+
for (const [name, entry] of Object.entries(observed.unobserved)) {
|
|
213
|
+
warnings.push(`${plugin.name}: not observed deeply — ${formatUnobserved(name, entry)}`);
|
|
214
|
+
}
|
|
215
|
+
if (Object.keys(observed.resources).length > 0) {
|
|
216
|
+
properties = observed.resources;
|
|
217
|
+
recordedDepth = "deep";
|
|
218
|
+
} else {
|
|
219
|
+
warnings.push(
|
|
220
|
+
`${plugin.name}: deep read returned no properties — recording an identity snapshot`,
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
// What this estate depends on but does not manage (#1273), recorded so a
|
|
226
|
+
// replayed snapshot can answer the same questions a live read can (#1266).
|
|
227
|
+
// Without them a snapshot holds the managed resources and no route to the
|
|
228
|
+
// account's default VPC, so `internetFacing` is unanswerable from it —
|
|
229
|
+
// which would make `search --at` quietly weaker than `search --live`.
|
|
230
|
+
const dependencies = await collectDependencies(plugin, {
|
|
231
|
+
environment,
|
|
232
|
+
entities,
|
|
233
|
+
observed: resources,
|
|
234
|
+
stacks: stack ? [{ name: stack, ...(region ? { region } : {}) }] : [],
|
|
235
|
+
});
|
|
236
|
+
for (const message of dependencies.warnings) warnings.push(message);
|
|
237
|
+
// Ambient resources (#1278) are recorded too when asked for, so a replayed
|
|
238
|
+
// snapshot can answer "which of these are unused" without a live read.
|
|
239
|
+
// Without this `search --at --ambient` filters a set that was never
|
|
240
|
+
// recorded and silently returns nothing.
|
|
241
|
+
const ambient = opts?.ambient
|
|
242
|
+
? await collectAmbient(plugin, {
|
|
243
|
+
environment,
|
|
244
|
+
kinds: opts?.ambientKinds ?? [...new Set([...entities.values()].map((e) => e.entityType))],
|
|
245
|
+
observed: resources,
|
|
246
|
+
stacks: stack ? [{ name: stack, ...(region ? { region } : {}) }] : [],
|
|
247
|
+
warnings,
|
|
248
|
+
})
|
|
249
|
+
: {};
|
|
250
|
+
const withDependencies = { ...resources, ...dependencies.resources, ...ambient };
|
|
251
|
+
|
|
171
252
|
const snapshot: LifecycleSnapshot = {
|
|
172
253
|
lexicon: plugin.name,
|
|
173
254
|
environment,
|
|
174
255
|
...(stack ? { stack } : {}),
|
|
175
256
|
commit: headCommit,
|
|
176
257
|
timestamp,
|
|
177
|
-
resources,
|
|
258
|
+
resources: withDependencies,
|
|
259
|
+
...(dependencies.edges.length > 0 ? { edges: dependencies.edges } : {}),
|
|
178
260
|
...(Object.keys(unobserved).length > 0 && { unobserved }),
|
|
179
261
|
...(Object.keys(artifacts).length > 0 && { artifacts }),
|
|
262
|
+
// Only written when deep. An absent field means identity, which is what
|
|
263
|
+
// every snapshot taken before #1267 was.
|
|
264
|
+
...(recordedDepth === "deep" && { depth: recordedDepth, properties }),
|
|
180
265
|
digest,
|
|
181
266
|
};
|
|
182
267
|
|
package/src/lifecycle/types.ts
CHANGED
|
@@ -1,8 +1,25 @@
|
|
|
1
1
|
import type { ResourceMetadata, ArtifactMetadata } from "../lexicon";
|
|
2
2
|
import type { UnobservedEntity } from "../observation";
|
|
3
|
+
import type { DeepResourceObservation } from "../deep-observation";
|
|
4
|
+
import type { IREdge } from "../graph-ir";
|
|
3
5
|
|
|
4
6
|
export type { ResourceMetadata, ArtifactMetadata } from "../lexicon";
|
|
5
7
|
|
|
8
|
+
/**
|
|
9
|
+
* How much of each resource an observation actually read (#1267).
|
|
10
|
+
*
|
|
11
|
+
* `identity` is the thin path: logical name, type, physical id, status. It is
|
|
12
|
+
* what a snapshot recorded before deep reads existed, and it stays the default
|
|
13
|
+
* because a deep read costs more provider calls and a larger record.
|
|
14
|
+
*
|
|
15
|
+
* `deep` additionally carries each resource's normalized property tree, which
|
|
16
|
+
* is what a fold over topology needs — a subnet's route-table association, a
|
|
17
|
+
* security group's rules. A consumer must branch on this rather than assume:
|
|
18
|
+
* asking an `identity` snapshot a property question has no answer, and
|
|
19
|
+
* silently returning nothing would read as "no such resources".
|
|
20
|
+
*/
|
|
21
|
+
export type ObservationDepth = "identity" | "deep";
|
|
22
|
+
|
|
6
23
|
/**
|
|
7
24
|
* State snapshot for a single lexicon in an environment.
|
|
8
25
|
*/
|
|
@@ -28,6 +45,36 @@ export interface LifecycleSnapshot {
|
|
|
28
45
|
unobserved?: Record<string, UnobservedEntity>;
|
|
29
46
|
/** Artifact metadata keyed by server-side identifier (lexicon-specific). */
|
|
30
47
|
artifacts?: Record<string, ArtifactMetadata>;
|
|
48
|
+
/**
|
|
49
|
+
* How much of each resource this snapshot read (#1267). Absent means
|
|
50
|
+
* `identity` — every snapshot written before deep reads existed was thin, and
|
|
51
|
+
* treating a missing field as unknown rather than as thin would invalidate
|
|
52
|
+
* them all.
|
|
53
|
+
*/
|
|
54
|
+
depth?: ObservationDepth;
|
|
55
|
+
/**
|
|
56
|
+
* Normalized per-resource property trees, present only at `deep` depth and
|
|
57
|
+
* keyed by the same logical names as `resources`.
|
|
58
|
+
*
|
|
59
|
+
* Kept beside `resources` rather than merged into it so the thin record stays
|
|
60
|
+
* exactly what it always was: a reader that only wants identity does not have
|
|
61
|
+
* to learn a new shape, and an old snapshot and a new one parse the same way.
|
|
62
|
+
*/
|
|
63
|
+
properties?: Record<string, DeepResourceObservation>;
|
|
64
|
+
/**
|
|
65
|
+
* Relationships observed between the recorded resources (#1266).
|
|
66
|
+
*
|
|
67
|
+
* `resources` says what existed; without this a snapshot cannot say how any
|
|
68
|
+
* of it connected, so a fold over topology has nothing to traverse when the
|
|
69
|
+
* snapshot is replayed. That is the difference between a snapshot answering
|
|
70
|
+
* "which instances exist" and answering "which are reachable from the
|
|
71
|
+
* internet" — and the second is the whole reason the graph is worth
|
|
72
|
+
* recording.
|
|
73
|
+
*
|
|
74
|
+
* Absent on every snapshot written before this, which is read as "no
|
|
75
|
+
* relationships recorded" rather than "no relationships existed".
|
|
76
|
+
*/
|
|
77
|
+
edges?: IREdge[];
|
|
31
78
|
/** Build digest at snapshot time — what was declared when this snapshot was taken */
|
|
32
79
|
digest?: BuildDigest;
|
|
33
80
|
}
|