@intentius/chant 0.29.0 → 0.31.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 (60) hide show
  1. package/dist/cli/handlers/graph.d.ts.map +1 -1
  2. package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
  3. package/dist/cli/main.d.ts.map +1 -1
  4. package/dist/cli/registry.d.ts +14 -0
  5. package/dist/cli/registry.d.ts.map +1 -1
  6. package/dist/codegen/generate.d.ts +16 -0
  7. package/dist/codegen/generate.d.ts.map +1 -1
  8. package/dist/deep-observation.d.ts +257 -0
  9. package/dist/deep-observation.d.ts.map +1 -0
  10. package/dist/discovery/fold-import.d.ts.map +1 -1
  11. package/dist/fold/fold.d.ts +23 -3
  12. package/dist/fold/fold.d.ts.map +1 -1
  13. package/dist/fold/subset.d.ts +9 -0
  14. package/dist/fold/subset.d.ts.map +1 -1
  15. package/dist/graph-ir.d.ts +44 -0
  16. package/dist/graph-ir.d.ts.map +1 -1
  17. package/dist/index.d.ts +1 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/kubectl-context.d.ts +18 -1
  20. package/dist/kubectl-context.d.ts.map +1 -1
  21. package/dist/lexicon.d.ts +47 -0
  22. package/dist/lexicon.d.ts.map +1 -1
  23. package/dist/lifecycle/deep-diff.d.ts +103 -0
  24. package/dist/lifecycle/deep-diff.d.ts.map +1 -0
  25. package/dist/lifecycle/deep-observe.d.ts +62 -0
  26. package/dist/lifecycle/deep-observe.d.ts.map +1 -0
  27. package/dist/lifecycle/index.d.ts +3 -0
  28. package/dist/lifecycle/index.d.ts.map +1 -1
  29. package/dist/lifecycle/observation-baseline.d.ts +118 -0
  30. package/dist/lifecycle/observation-baseline.d.ts.map +1 -0
  31. package/dist/lifecycle/snapshot.d.ts.map +1 -1
  32. package/package.json +1 -1
  33. package/src/cli/handlers/graph.test.ts +86 -0
  34. package/src/cli/handlers/graph.ts +64 -3
  35. package/src/cli/handlers/lifecycle.test.ts +126 -1
  36. package/src/cli/handlers/lifecycle.ts +184 -3
  37. package/src/cli/main.test.ts +6 -0
  38. package/src/cli/main.ts +12 -0
  39. package/src/cli/registry.ts +14 -0
  40. package/src/codegen/generate.ts +25 -0
  41. package/src/deep-observation.test.ts +234 -0
  42. package/src/deep-observation.ts +489 -0
  43. package/src/discovery/fold-import.test.ts +372 -1
  44. package/src/discovery/fold-import.ts +235 -79
  45. package/src/fold/fold.test.ts +105 -0
  46. package/src/fold/fold.ts +88 -18
  47. package/src/fold/subset.test.ts +38 -7
  48. package/src/fold/subset.ts +9 -0
  49. package/src/graph-ir.ts +47 -0
  50. package/src/index.ts +1 -0
  51. package/src/kubectl-context.ts +22 -2
  52. package/src/lexicon.ts +59 -0
  53. package/src/lifecycle/deep-diff.test.ts +157 -0
  54. package/src/lifecycle/deep-diff.ts +213 -0
  55. package/src/lifecycle/deep-observe.test.ts +174 -0
  56. package/src/lifecycle/deep-observe.ts +173 -0
  57. package/src/lifecycle/index.ts +3 -0
  58. package/src/lifecycle/observation-baseline.test.ts +99 -0
  59. package/src/lifecycle/observation-baseline.ts +217 -0
  60. package/src/lifecycle/snapshot.ts +6 -11
@@ -0,0 +1,234 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import {
3
+ MASKED,
4
+ UNRESOLVED,
5
+ deepObservation,
6
+ deepPathSet,
7
+ deepValueEqual,
8
+ flattenDeepProperties,
9
+ isDeepObservationResult,
10
+ isSensitiveKey,
11
+ normalizeDeepObservation,
12
+ normalizeDeepProperties,
13
+ type DeepNormalizationHooks,
14
+ } from "./deep-observation";
15
+
16
+ describe("the deep observation envelope", () => {
17
+ test("is discriminated by its version literal", () => {
18
+ expect(isDeepObservationResult(deepObservation({}))).toBe(true);
19
+ expect(isDeepObservationResult({ resources: {} })).toBe(false);
20
+ expect(isDeepObservationResult({ observation: "v1", resources: {} })).toBe(false);
21
+ expect(isDeepObservationResult(null)).toBe(false);
22
+ });
23
+
24
+ test("omits an empty unobserved map rather than emitting one", () => {
25
+ expect(deepObservation({}, {})).toEqual({ deepObservation: "v1", resources: {} });
26
+ });
27
+
28
+ test("normalizing undefined yields two empty maps", () => {
29
+ expect(normalizeDeepObservation(undefined)).toEqual({ resources: {}, unobserved: {} });
30
+ });
31
+
32
+ test("carries the tri-state contract's unobserved entries verbatim", () => {
33
+ const result = deepObservation(
34
+ { a: { type: "T", properties: {} } },
35
+ { b: { type: "T", reason: "unsupported-kind", detail: "no reader" } },
36
+ );
37
+ expect(normalizeDeepObservation(result).unobserved.b.reason).toBe("unsupported-kind");
38
+ });
39
+ });
40
+
41
+ describe("normalizeDeepProperties", () => {
42
+ test("canonicalizes object key order", () => {
43
+ const out = normalizeDeepProperties(
44
+ { zeta: 1, alpha: 2, mid: { z: 1, a: 2 } },
45
+ { entityType: "T", side: "live" },
46
+ );
47
+ expect(Object.keys(out)).toEqual(["alpha", "mid", "zeta"]);
48
+ expect(Object.keys(out.mid as Record<string, unknown>)).toEqual(["a", "z"]);
49
+ });
50
+
51
+ test("leaves array order alone with no ordering hook", () => {
52
+ const out = normalizeDeepProperties({ Tags: [{ Key: "z" }, { Key: "a" }] }, { entityType: "T", side: "live" });
53
+ expect(out.Tags).toEqual([{ Key: "z" }, { Key: "a" }]);
54
+ });
55
+
56
+ test("orders an array when the hook keys every element", () => {
57
+ const hooks: DeepNormalizationHooks = {
58
+ orderKey: (el) => (el.pattern === "Tags" ? String((el.element as { Key: string }).Key) : undefined),
59
+ };
60
+ const out = normalizeDeepProperties(
61
+ { Tags: [{ Key: "z" }, { Key: "a" }], Steps: ["second", "first"] },
62
+ { entityType: "T", side: "live", hooks },
63
+ );
64
+ expect(out.Tags).toEqual([{ Key: "a" }, { Key: "z" }]);
65
+ // No key for Steps — order is left alone, because list order is often
66
+ // semantic and a guess here is worse than a stable false negative.
67
+ expect(out.Steps).toEqual(["second", "first"]);
68
+ });
69
+
70
+ test("a partially-keyed array keeps its order", () => {
71
+ const hooks: DeepNormalizationHooks = {
72
+ orderKey: (el) => (el.index === 0 ? "a" : undefined),
73
+ };
74
+ const out = normalizeDeepProperties({ List: ["x", "y"] }, { entityType: "T", side: "live", hooks });
75
+ expect(out.List).toEqual(["x", "y"]);
76
+ });
77
+
78
+ test("prunes by hook, and prunes the whole subtree", () => {
79
+ const hooks: DeepNormalizationHooks = { prune: (n) => n.pattern === "Status" };
80
+ const out = normalizeDeepProperties(
81
+ { Status: { Phase: "Ready", Conditions: [1, 2] }, Name: "n" },
82
+ { entityType: "T", side: "live", hooks },
83
+ );
84
+ expect(out).toEqual({ Name: "n" });
85
+ });
86
+
87
+ test("hooks see an index-erased pattern alongside the exact path", () => {
88
+ const seen: Array<[string, string]> = [];
89
+ const hooks: DeepNormalizationHooks = {
90
+ prune: (n) => {
91
+ seen.push([n.path, n.pattern]);
92
+ return false;
93
+ },
94
+ };
95
+ normalizeDeepProperties({ Tags: [{ Key: "a" }] }, { entityType: "T", side: "live", hooks });
96
+ expect(seen).toContainEqual(["Tags[0].Key", "Tags[].Key"]);
97
+ });
98
+
99
+ test("counterpart is `unknown` for a one-sided pass and resolved when paths are supplied", () => {
100
+ const seen: Record<string, string> = {};
101
+ const hooks: DeepNormalizationHooks = {
102
+ prune: (n) => {
103
+ seen[n.pattern] = n.counterpart;
104
+ return false;
105
+ },
106
+ };
107
+ normalizeDeepProperties({ A: 1, B: 2 }, { entityType: "T", side: "live", hooks });
108
+ expect(seen).toEqual({ A: "unknown", B: "unknown" });
109
+
110
+ normalizeDeepProperties(
111
+ { A: 1, B: 2 },
112
+ { entityType: "T", side: "live", hooks, counterpartPaths: deepPathSet({ A: 9 }) },
113
+ );
114
+ expect(seen).toEqual({ A: "present", B: "absent" });
115
+ });
116
+
117
+ test("an array element counts as declared when the pattern is declared at any index", () => {
118
+ const seen: Record<string, string> = {};
119
+ const hooks: DeepNormalizationHooks = {
120
+ prune: (n) => {
121
+ seen[n.path] = n.counterpart;
122
+ return false;
123
+ },
124
+ };
125
+ normalizeDeepProperties(
126
+ { Tags: [{ Key: "b" }, { Key: "a" }] },
127
+ { entityType: "T", side: "live", hooks, counterpartPaths: deepPathSet({ Tags: [{ Key: "a" }] }) },
128
+ );
129
+ // Source declares one tag; both live tags match the `Tags[].Key` pattern.
130
+ expect(seen["Tags[1].Key"]).toBe("present");
131
+ });
132
+
133
+ test("masks secret-bearing property names without recursing into them", () => {
134
+ const out = normalizeDeepProperties(
135
+ { MasterUserPassword: "hunter2", Nested: { ClientSecret: { a: 1 } }, Tags: [{ Key: "k", Value: "v" }] },
136
+ { entityType: "T", side: "live" },
137
+ );
138
+ expect(out.MasterUserPassword).toBe(MASKED);
139
+ expect((out.Nested as Record<string, unknown>).ClientSecret).toBe(MASKED);
140
+ // `Key`/`Value` are not secrets — masking them would be its own drift signal.
141
+ expect(out.Tags).toEqual([{ Key: "k", Value: "v" }]);
142
+ });
143
+
144
+ test("collapses non-JSON values (an unevaluated intrinsic) to the unresolved sentinel", () => {
145
+ class SubIntrinsic {
146
+ constructor(readonly template: string) {}
147
+ }
148
+ const out = normalizeDeepProperties(
149
+ { BucketName: new SubIntrinsic("${AWS::StackName}-data"), Plain: "x" },
150
+ { entityType: "T", side: "declared" },
151
+ );
152
+ expect(out.BucketName).toBe(UNRESOLVED);
153
+ expect(out.Plain).toBe("x");
154
+ });
155
+
156
+ test("drops undefined but keeps null", () => {
157
+ const out = normalizeDeepProperties({ a: undefined, b: null }, { entityType: "T", side: "live" });
158
+ expect("a" in out).toBe(false);
159
+ expect(out.b).toBeNull();
160
+ });
161
+ });
162
+
163
+ describe("isSensitiveKey", () => {
164
+ test("matches the secret-bearing names and nothing broader", () => {
165
+ for (const k of ["Password", "clientSecret", "AuthToken", "PrivateKey", "credentials", "ConnectionString"]) {
166
+ expect(isSensitiveKey(k), k).toBe(true);
167
+ }
168
+ for (const k of ["Key", "KeyName", "KmsKeyId", "Value", "Name"]) {
169
+ expect(isSensitiveKey(k), k).toBe(false);
170
+ }
171
+ });
172
+ });
173
+
174
+ describe("flattenDeepProperties", () => {
175
+ test("flattens to leaf paths, keeping empty containers as values", () => {
176
+ const flat = flattenDeepProperties({
177
+ A: { B: 1 },
178
+ List: [{ C: "x" }, "y"],
179
+ EmptyObj: {},
180
+ EmptyArr: [],
181
+ });
182
+ expect(Object.fromEntries(flat)).toEqual({
183
+ "A.B": 1,
184
+ "List[0].C": "x",
185
+ "List[1]": "y",
186
+ EmptyObj: {},
187
+ EmptyArr: [],
188
+ });
189
+ });
190
+ });
191
+
192
+ describe("flattenDeepProperties with an ordering hook", () => {
193
+ const hooks: DeepNormalizationHooks = {
194
+ orderKey: (el) => (el.pattern === "Tags" ? String((el.element as { Key: string }).Key) : undefined),
195
+ };
196
+ const opts = { entityType: "T", side: "live" as const, hooks };
197
+
198
+ test("addresses a keyable array by key, so an inserted element shifts nothing", () => {
199
+ const flat = flattenDeepProperties({ Tags: [{ Key: "env", Value: "prod" }] }, opts);
200
+ expect([...flat.keys()].sort()).toEqual(["Tags[#env].Key", "Tags[#env].Value"]);
201
+
202
+ const withExtra = flattenDeepProperties(
203
+ { Tags: [{ Key: "cost", Value: "x" }, { Key: "env", Value: "prod" }] },
204
+ opts,
205
+ );
206
+ expect(withExtra.get("Tags[#env].Value")).toBe("prod");
207
+ });
208
+
209
+ test("falls back to positional paths when keys collide", () => {
210
+ const flat = flattenDeepProperties({ Tags: [{ Key: "env", Value: "a" }, { Key: "env", Value: "b" }] }, opts);
211
+ expect([...flat.keys()]).toContain("Tags[0].Value");
212
+ });
213
+
214
+ test("falls back to positional paths for an array the hook cannot key", () => {
215
+ const flat = flattenDeepProperties({ Steps: ["a", "b"] }, opts);
216
+ expect([...flat.keys()]).toEqual(["Steps[0]", "Steps[1]"]);
217
+ });
218
+ });
219
+
220
+ describe("deepPathSet", () => {
221
+ test("records both the exact path and the index-erased pattern", () => {
222
+ const set = deepPathSet({ Tags: [{ Key: "a" }] });
223
+ expect([...set].sort()).toEqual(["Tags", "Tags[0]", "Tags[0].Key", "Tags[]", "Tags[].Key"]);
224
+ });
225
+ });
226
+
227
+ describe("deepValueEqual", () => {
228
+ test("compares structurally", () => {
229
+ expect(deepValueEqual({ a: [1, 2] }, { a: [1, 2] })).toBe(true);
230
+ expect(deepValueEqual({ a: [1, 2] }, { a: [2, 1] })).toBe(false);
231
+ expect(deepValueEqual(null, undefined)).toBe(false);
232
+ expect(deepValueEqual(1, 1)).toBe(true);
233
+ });
234
+ });