@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.
Files changed (50) hide show
  1. package/dist/cli/commands/onboard.d.ts.map +1 -1
  2. package/dist/cli/handlers/graph.d.ts.map +1 -1
  3. package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
  4. package/dist/cli/handlers/search.d.ts +49 -1
  5. package/dist/cli/handlers/search.d.ts.map +1 -1
  6. package/dist/cli/main.d.ts.map +1 -1
  7. package/dist/cli/registry.d.ts +26 -0
  8. package/dist/cli/registry.d.ts.map +1 -1
  9. package/dist/graph-ir.d.ts +14 -0
  10. package/dist/graph-ir.d.ts.map +1 -1
  11. package/dist/graph-refs.d.ts +19 -0
  12. package/dist/graph-refs.d.ts.map +1 -1
  13. package/dist/lexicon.d.ts +141 -0
  14. package/dist/lexicon.d.ts.map +1 -1
  15. package/dist/lifecycle/deep-observe.d.ts +4 -0
  16. package/dist/lifecycle/deep-observe.d.ts.map +1 -1
  17. package/dist/lifecycle/live-diff.d.ts.map +1 -1
  18. package/dist/lifecycle/observe.d.ts +55 -1
  19. package/dist/lifecycle/observe.d.ts.map +1 -1
  20. package/dist/lifecycle/replay.d.ts +47 -0
  21. package/dist/lifecycle/replay.d.ts.map +1 -0
  22. package/dist/lifecycle/snapshot.d.ts +6 -0
  23. package/dist/lifecycle/snapshot.d.ts.map +1 -1
  24. package/dist/lifecycle/types.d.ts +46 -0
  25. package/dist/lifecycle/types.d.ts.map +1 -1
  26. package/package.json +1 -1
  27. package/src/cli/commands/onboard.ts +10 -25
  28. package/src/cli/handlers/graph.test.ts +74 -0
  29. package/src/cli/handlers/graph.ts +77 -36
  30. package/src/cli/handlers/lifecycle.test.ts +86 -0
  31. package/src/cli/handlers/lifecycle.ts +43 -10
  32. package/src/cli/handlers/search.test.ts +200 -4
  33. package/src/cli/handlers/search.ts +383 -29
  34. package/src/cli/main.ts +9 -0
  35. package/src/cli/registry.ts +27 -0
  36. package/src/codegen/lexicon-wiring.test.ts +53 -0
  37. package/src/codegen/release-wiring.test.ts +92 -0
  38. package/src/graph-ir-live.test.ts +83 -0
  39. package/src/graph-ir.ts +51 -1
  40. package/src/graph-refs.test.ts +59 -0
  41. package/src/graph-refs.ts +39 -8
  42. package/src/lexicon.ts +145 -0
  43. package/src/lifecycle/deep-observe.ts +5 -0
  44. package/src/lifecycle/live-diff.test.ts +38 -0
  45. package/src/lifecycle/live-diff.ts +45 -2
  46. package/src/lifecycle/observe.ts +186 -4
  47. package/src/lifecycle/replay.ts +141 -0
  48. package/src/lifecycle/snapshot.test.ts +179 -0
  49. package/src/lifecycle/snapshot.ts +88 -3
  50. 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?: { cwd?: string; stack?: string },
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
 
@@ -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
  }