@intentius/chant-lexicon-helm 0.46.0 → 0.49.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 (118) hide show
  1. package/dist/archive-render.d.ts +94 -0
  2. package/dist/archive-render.d.ts.map +1 -0
  3. package/dist/commands.d.ts +54 -0
  4. package/dist/commands.d.ts.map +1 -0
  5. package/dist/config.d.ts +135 -0
  6. package/dist/config.d.ts.map +1 -0
  7. package/dist/deep-observe-hooks.d.ts +2 -0
  8. package/dist/deep-observe-hooks.d.ts.map +1 -0
  9. package/dist/deep-observe.d.ts +54 -0
  10. package/dist/deep-observe.d.ts.map +1 -0
  11. package/dist/describe-resources.d.ts +17 -0
  12. package/dist/describe-resources.d.ts.map +1 -0
  13. package/dist/index.d.ts +18 -2
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/integrity.json +5 -3
  16. package/dist/lint/audit-catalog.d.ts.map +1 -1
  17. package/dist/lint/post-synth/index.d.ts.map +1 -1
  18. package/dist/lint/post-synth/whm503.d.ts +34 -0
  19. package/dist/lint/post-synth/whm503.d.ts.map +1 -0
  20. package/dist/lint/post-synth/whm504.d.ts +20 -0
  21. package/dist/lint/post-synth/whm504.d.ts.map +1 -0
  22. package/dist/manifest.json +1 -1
  23. package/dist/okf/index.md +2 -0
  24. package/dist/okf/rules/WHM503.md +15 -0
  25. package/dist/okf/rules/WHM504.md +11 -0
  26. package/dist/okf/types/Values.md +1 -0
  27. package/dist/op/activities/cluster-probe.d.ts +44 -0
  28. package/dist/op/activities/cluster-probe.d.ts.map +1 -0
  29. package/dist/op/activities/helm.d.ts +179 -5
  30. package/dist/op/activities/helm.d.ts.map +1 -1
  31. package/dist/op/activities/index.d.ts +4 -2
  32. package/dist/op/activities/index.d.ts.map +1 -1
  33. package/dist/pinnability/actions.d.ts +76 -0
  34. package/dist/pinnability/actions.d.ts.map +1 -0
  35. package/dist/pinnability/classify.d.ts +129 -0
  36. package/dist/pinnability/classify.d.ts.map +1 -0
  37. package/dist/pinnability/conditions.d.ts +109 -0
  38. package/dist/pinnability/conditions.d.ts.map +1 -0
  39. package/dist/pinnability/index.d.ts +17 -0
  40. package/dist/pinnability/index.d.ts.map +1 -0
  41. package/dist/pinnability/localize.d.ts +124 -0
  42. package/dist/pinnability/localize.d.ts.map +1 -0
  43. package/dist/pinnability/render-stream.d.ts +27 -0
  44. package/dist/pinnability/render-stream.d.ts.map +1 -0
  45. package/dist/pinnability/values.d.ts +69 -0
  46. package/dist/pinnability/values.d.ts.map +1 -0
  47. package/dist/plugin.d.ts.map +1 -1
  48. package/dist/release-observe.d.ts +109 -0
  49. package/dist/release-observe.d.ts.map +1 -0
  50. package/dist/render-diff.d.ts +192 -0
  51. package/dist/render-diff.d.ts.map +1 -0
  52. package/dist/render-digest.d.ts +135 -0
  53. package/dist/render-digest.d.ts.map +1 -0
  54. package/dist/render-store.d.ts +310 -0
  55. package/dist/render-store.d.ts.map +1 -0
  56. package/dist/render-wrapper.d.ts +140 -0
  57. package/dist/render-wrapper.d.ts.map +1 -0
  58. package/dist/render.d.ts +81 -0
  59. package/dist/render.d.ts.map +1 -1
  60. package/dist/rules/whm503.ts +101 -0
  61. package/dist/rules/whm504.ts +52 -0
  62. package/dist/skills/chant-helm.md +7 -0
  63. package/dist/values-probe.d.ts +177 -0
  64. package/dist/values-probe.d.ts.map +1 -0
  65. package/dist/wrapper-chart.d.ts +61 -0
  66. package/dist/wrapper-chart.d.ts.map +1 -0
  67. package/package.json +10 -4
  68. package/src/archive-render.test.ts +201 -0
  69. package/src/archive-render.ts +119 -0
  70. package/src/commands.test.ts +487 -0
  71. package/src/commands.ts +445 -0
  72. package/src/config.test.ts +130 -0
  73. package/src/config.ts +220 -0
  74. package/src/deep-observe-hooks.ts +20 -0
  75. package/src/deep-observe.test.ts +165 -0
  76. package/src/deep-observe.ts +121 -0
  77. package/src/describe-resources.test.ts +255 -0
  78. package/src/describe-resources.ts +198 -0
  79. package/src/index.ts +98 -2
  80. package/src/lifecycle-integration.test.ts +289 -0
  81. package/src/lint/audit-catalog.ts +2 -0
  82. package/src/lint/post-synth/index.ts +4 -0
  83. package/src/lint/post-synth/whm503.test.ts +134 -0
  84. package/src/lint/post-synth/whm503.ts +101 -0
  85. package/src/lint/post-synth/whm504.test.ts +105 -0
  86. package/src/lint/post-synth/whm504.ts +52 -0
  87. package/src/op/activities/cluster-probe.test.ts +66 -0
  88. package/src/op/activities/cluster-probe.ts +148 -0
  89. package/src/op/activities/helm.test.ts +824 -0
  90. package/src/op/activities/helm.ts +628 -5
  91. package/src/op/activities/index.ts +17 -2
  92. package/src/pinnability/actions.ts +263 -0
  93. package/src/pinnability/classify.ts +579 -0
  94. package/src/pinnability/conditions.ts +0 -0
  95. package/src/pinnability/index.ts +77 -0
  96. package/src/pinnability/localize.test.ts +234 -0
  97. package/src/pinnability/localize.ts +644 -0
  98. package/src/pinnability/pinnability.test.ts +560 -0
  99. package/src/pinnability/render-stream.ts +59 -0
  100. package/src/pinnability/values.ts +205 -0
  101. package/src/plugin.test.ts +3 -1
  102. package/src/plugin.ts +47 -0
  103. package/src/release-observe.ts +392 -0
  104. package/src/render-diff.test.ts +366 -0
  105. package/src/render-diff.ts +407 -0
  106. package/src/render-digest.test.ts +186 -0
  107. package/src/render-digest.ts +207 -0
  108. package/src/render-store.test.ts +412 -0
  109. package/src/render-store.ts +621 -0
  110. package/src/render-wrapper.test.ts +259 -0
  111. package/src/render-wrapper.ts +274 -0
  112. package/src/render.test.ts +383 -4
  113. package/src/render.ts +319 -8
  114. package/src/skills/chant-helm.md +7 -0
  115. package/src/values-probe.test.ts +400 -0
  116. package/src/values-probe.ts +627 -0
  117. package/src/wrapper-chart.test.ts +110 -0
  118. package/src/wrapper-chart.ts +124 -0
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Unit tests for the double-render localizer (#1236, epic #1228 Phase 1).
3
+ *
4
+ * Pure — no helm binary, no network. The render function is faked with the
5
+ * shapes the survey corpus proved real: a generated secret cascading into a
6
+ * checksum annotation in another template (finding 6, grafana), a generator
7
+ * reachable only through an include chain whose supply slot lives at the
8
+ * include site (grafana's `adminPassword` else branch), an existing-secret
9
+ * gate (harbor's `existingSecret` family), and instability no generator
10
+ * explains — which must surface as unlocalized, never silently pinnable.
11
+ * The end-to-end run against real helm renders lives in the survey harness
12
+ * (test/survey/survey.test.ts).
13
+ */
14
+
15
+ import { describe, test, expect } from "vitest";
16
+ import { createHash } from "node:crypto";
17
+ import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
18
+ import { tmpdir } from "node:os";
19
+ import { dirname, join } from "node:path";
20
+
21
+ import { classifyChart } from "./classify";
22
+ import { diffRenderPair, localizeOpenInputs } from "./localize";
23
+
24
+ /** Materialize a chart directory from a { relPath: content } map. */
25
+ function mkChart(files: Record<string, string>): string {
26
+ const dir = mkdtempSync(join(tmpdir(), "chant-localize-"));
27
+ for (const [rel, content] of Object.entries(files)) {
28
+ const p = join(dir, rel);
29
+ mkdirSync(dirname(p), { recursive: true });
30
+ writeFileSync(p, content);
31
+ }
32
+ return dir;
33
+ }
34
+
35
+ const MINIMAL_CHART = "apiVersion: v2\nname: unit\ntype: application\nversion: 0.1.0\n";
36
+
37
+ const sha = (s: string): string => createHash("sha256").update(s).digest("hex");
38
+ const b64 = (s: string): string => Buffer.from(s).toString("base64");
39
+
40
+ describe("diffRenderPair", () => {
41
+ test("identical renders diff to nothing", () => {
42
+ const r = "---\n# Source: unit/templates/cm.yaml\nkind: ConfigMap\ndata:\n a: 1";
43
+ expect(diffRenderPair(r, r).size).toBe(0);
44
+ });
45
+
46
+ test("line identity is source + key, stable across probe runs", () => {
47
+ const doc = (v: string): string =>
48
+ `---\n# Source: unit/templates/secret.yaml\nkind: Secret\nmetadata:\n name: app\ndata:\n admin-password: ${v}`;
49
+ const diff = diffRenderPair(doc("aaa"), doc("bbb"));
50
+ expect(diff.size).toBe(1);
51
+ const occ = [...diff.values()][0];
52
+ expect(occ.doc).toBe("templates/secret.yaml");
53
+ expect(occ.docId).toBe("Secret/app");
54
+ expect(occ.key).toBe("admin-password");
55
+ expect(occ.line).toBe(6); // 1-based within the split document
56
+ // The same location diffs to the same identity with different content.
57
+ expect([...diffRenderPair(doc("ccc"), doc("ddd")).keys()]).toEqual([...diff.keys()]);
58
+ });
59
+
60
+ test("a document present in one render only diffs on every line", () => {
61
+ const stable = "---\n# Source: unit/templates/cm.yaml\nkind: ConfigMap\ndata:\n a: 1";
62
+ const extra = `${stable}\n---\n# Source: unit/templates/secret.yaml\nkind: Secret\ndata:\n b: 2`;
63
+ const diff = diffRenderPair(extra, stable);
64
+ expect(diff.size).toBeGreaterThan(0);
65
+ for (const occ of diff.values()) expect(occ.doc).toBe("templates/secret.yaml");
66
+ });
67
+ });
68
+
69
+ describe("localizeOpenInputs", () => {
70
+ const cascadeChart = (): string =>
71
+ mkChart({
72
+ "Chart.yaml": MINIMAL_CHART,
73
+ "values.yaml": 'adminPassword: ""\nexistingSecret: ""\n',
74
+ "templates/secret.yaml": [
75
+ "{{- if not .Values.existingSecret }}",
76
+ "apiVersion: v1",
77
+ "kind: Secret",
78
+ "metadata:",
79
+ " name: app-admin",
80
+ "data:",
81
+ " admin-password: {{ .Values.adminPassword | default (randAlphaNum 16) | b64enc }}",
82
+ "{{- end }}",
83
+ ].join("\n"),
84
+ "templates/deployment.yaml": [
85
+ "apiVersion: apps/v1",
86
+ "kind: Deployment",
87
+ "metadata:",
88
+ " name: app",
89
+ "spec:",
90
+ " template:",
91
+ " metadata:",
92
+ " annotations:",
93
+ ' checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }}',
94
+ ].join("\n"),
95
+ });
96
+
97
+ /** Fake helm: fresh password per render unless pinned; the checksum derives. */
98
+ function cascadeRender(): (pins: Record<string, string>) => string {
99
+ let nonce = 0;
100
+ return (pins) => {
101
+ const pw = pins.adminPassword ?? `random-${nonce++}`;
102
+ const gated = (pins.existingSecret ?? "") !== "";
103
+ const docs: string[] = [];
104
+ if (!gated) {
105
+ docs.push(
106
+ `# Source: unit/templates/secret.yaml\napiVersion: v1\nkind: Secret\nmetadata:\n name: app-admin\ndata:\n admin-password: ${b64(pw)}`,
107
+ );
108
+ }
109
+ docs.push(
110
+ `# Source: unit/templates/deployment.yaml\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: app\nspec:\n template:\n metadata:\n annotations:\n checksum/secret: ${sha(gated ? "" : pw)}`,
111
+ );
112
+ return `---\n${docs.join("\n---\n")}\n`;
113
+ };
114
+ }
115
+
116
+ test("a generated secret and its derived checksum group under one input (finding 6)", () => {
117
+ const chart = cascadeChart();
118
+ const report = classifyChart(chart);
119
+ const loc = localizeOpenInputs(chart, report, { render: cascadeRender() });
120
+
121
+ expect(loc.deterministic).toBe(false);
122
+ expect(loc.differingLines).toBe(2);
123
+ expect(loc.inputs).toHaveLength(1);
124
+ const input = loc.inputs[0];
125
+ expect(input.fn).toBe("randAlphaNum");
126
+ expect(input.file).toBe("templates/secret.yaml");
127
+ expect(input.suppliable).toBe(true);
128
+ expect(input.valuesPath).toBe("adminPassword");
129
+ expect(input.suggestedPin).toBe("adminPassword: <generate once and supply>");
130
+ // The chart-provided slot that also closes the generator is named.
131
+ expect(input.existingSlots).toEqual(["existingSecret"]);
132
+
133
+ const keys = input.occurrences.map((o) => `${o.key}${o.derived ? " (derived)" : ""}`);
134
+ expect(keys).toContain("admin-password");
135
+ // The checksum line lives in ANOTHER template — grouped under the same
136
+ // root input, flagged derived. Hoisting the secret output alone would
137
+ // leave it pointing at bytes that never deploy.
138
+ expect(keys).toContain("checksum/secret (derived)");
139
+ const checksum = input.occurrences.find((o) => o.key === "checksum/secret");
140
+ expect(checksum?.doc).toBe("templates/deployment.yaml");
141
+ expect(checksum?.docId).toBe("Deployment/app");
142
+
143
+ expect(loc.unlocalized).toEqual([]);
144
+ expect(loc.stableWithAllPins).toBe(true);
145
+ });
146
+
147
+ test("a stable double render confirms deterministic without probing", () => {
148
+ const chart = cascadeChart();
149
+ const report = classifyChart(chart);
150
+ const loc = localizeOpenInputs(chart, report, {
151
+ render: () =>
152
+ "---\n# Source: unit/templates/secret.yaml\nkind: Secret\ndata:\n admin-password: fixed\n",
153
+ });
154
+ expect(loc.deterministic).toBe(true);
155
+ expect(loc.differingLines).toBe(0);
156
+ expect(loc.unlocalized).toEqual([]);
157
+ // Two base renders, zero probes.
158
+ expect(loc.renders).toBe(2);
159
+ // The statically-named input is still reported, with its supply path.
160
+ expect(loc.inputs).toHaveLength(1);
161
+ expect(loc.inputs[0].occurrences).toEqual([]);
162
+ expect(loc.inputs[0].valuesPath).toBe("adminPassword");
163
+ });
164
+
165
+ test("a generator inside a define finds its supply slot at the include site", () => {
166
+ // grafana's shape: `randAlphaNum` lives in a helper with no values in
167
+ // reach; the `{{- if .Values.adminPassword }} ... {{- else }}` that
168
+ // decides whether it runs sits where the helper is INCLUDED.
169
+ const chart = mkChart({
170
+ "Chart.yaml": MINIMAL_CHART,
171
+ "values.yaml": 'adminPassword: ""\n',
172
+ "templates/_helpers.tpl": [
173
+ '{{- define "unit.password" -}}',
174
+ "{{- (randAlphaNum 40) | b64enc | quote }}",
175
+ "{{- end }}",
176
+ ].join("\n"),
177
+ "templates/secret.yaml": [
178
+ "apiVersion: v1",
179
+ "kind: Secret",
180
+ "metadata:",
181
+ " name: app",
182
+ "data:",
183
+ "{{- if .Values.adminPassword }}",
184
+ " admin-password: {{ .Values.adminPassword | b64enc }}",
185
+ "{{- else }}",
186
+ ' admin-password: {{ include "unit.password" . }}',
187
+ "{{- end }}",
188
+ ].join("\n"),
189
+ });
190
+ const report = classifyChart(chart);
191
+ const site = report.closedInputs.find((c) => c.kind === "generated-value");
192
+ expect(site?.suppliable).toBe(false); // statically: nothing in the action
193
+
194
+ let nonce = 0;
195
+ const loc = localizeOpenInputs(chart, report, {
196
+ render: (pins) =>
197
+ `---\n# Source: unit/templates/secret.yaml\napiVersion: v1\nkind: Secret\nmetadata:\n name: app\ndata:\n admin-password: ${b64(pins.adminPassword ?? `random-${nonce++}`)}\n`,
198
+ });
199
+ expect(loc.inputs).toHaveLength(1);
200
+ // Dynamically: the include-chain pin validated.
201
+ expect(loc.inputs[0].suppliable).toBe(true);
202
+ expect(loc.inputs[0].valuesPath).toBe("adminPassword");
203
+ expect(loc.inputs[0].suggestedPin).toBe("adminPassword: <generate once and supply>");
204
+ expect(loc.inputs[0].occurrences.map((o) => o.key)).toEqual(["admin-password"]);
205
+ expect(loc.unlocalized).toEqual([]);
206
+ expect(loc.stableWithAllPins).toBe(true);
207
+ });
208
+
209
+ test("instability no generator explains is unlocalized, never silently pinnable", () => {
210
+ const chart = mkChart({
211
+ "Chart.yaml": MINIMAL_CHART,
212
+ "values.yaml": 'adminPassword: ""\n',
213
+ "templates/secret.yaml":
214
+ "kind: Secret\ndata:\n admin-password: {{ .Values.adminPassword | default (randAlphaNum 16) | b64enc }}",
215
+ "templates/cm.yaml": "kind: ConfigMap\ndata:\n stamp: static",
216
+ });
217
+ const report = classifyChart(chart);
218
+ let nonce = 0;
219
+ const loc = localizeOpenInputs(chart, report, {
220
+ render: (pins) =>
221
+ [
222
+ `---\n# Source: unit/templates/secret.yaml\nkind: Secret\nmetadata:\n name: app\ndata:\n admin-password: ${b64(pins.adminPassword ?? `random-${nonce++}`)}`,
223
+ // Varies per render and maps to NO generator site (the template
224
+ // holds none) — the localizer must not attribute it anywhere.
225
+ `# Source: unit/templates/cm.yaml\nkind: ConfigMap\nmetadata:\n name: app-cm\ndata:\n stamp: ${nonce++}\n`,
226
+ ].join("\n---\n"),
227
+ });
228
+ expect(loc.inputs[0].occurrences.map((o) => o.key)).toEqual(["admin-password"]);
229
+ expect(loc.unlocalized).toHaveLength(1);
230
+ expect(loc.unlocalized[0].doc).toBe("templates/cm.yaml");
231
+ expect(loc.unlocalized[0].key).toBe("stamp");
232
+ expect(loc.stableWithAllPins).toBe(false);
233
+ });
234
+ });