@intentius/chant-lexicon-helm 0.124.30 → 0.124.32
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/archive-render.js +76 -0
- package/dist/codegen/docs-cli.js +3 -0
- package/dist/codegen/docs.js +113 -0
- package/dist/codegen/generate-cli.js +19 -0
- package/dist/codegen/generate.js +210 -0
- package/dist/codegen/naming.js +29 -0
- package/dist/codegen/package.js +44 -0
- package/dist/codegen/values-type.js +87 -0
- package/dist/commands.js +413 -0
- package/dist/components/builders.js +9 -0
- package/dist/components/capability-plugin.js +17 -0
- package/dist/components/helm-upgrade.js +109 -0
- package/dist/components/index.js +11 -0
- package/dist/composites/catalog.js +1119 -0
- package/dist/composites/helm-batch-job.js +148 -0
- package/dist/composites/helm-crd-lifecycle.js +145 -0
- package/dist/composites/helm-cron-job.js +130 -0
- package/dist/composites/helm-daemon-set.js +125 -0
- package/dist/composites/helm-external-secret.js +63 -0
- package/dist/composites/helm-library.js +24 -0
- package/dist/composites/helm-microservice.js +260 -0
- package/dist/composites/helm-monitored-service.js +188 -0
- package/dist/composites/helm-namespace-env.js +116 -0
- package/dist/composites/helm-secure-ingress.js +86 -0
- package/dist/composites/helm-stateful-service.js +160 -0
- package/dist/composites/helm-web-app.js +201 -0
- package/dist/composites/helm-worker.js +164 -0
- package/dist/composites/index.js +13 -0
- package/dist/config.js +161 -0
- package/dist/coverage.js +37 -0
- package/dist/deep-observe-hooks.js +19 -0
- package/dist/deep-observe.js +55 -0
- package/dist/describe-resources.js +119 -0
- package/dist/describe-stack-status.js +66 -0
- package/dist/detect.js +11 -0
- package/dist/generated/index.js +11 -0
- package/dist/helpers.js +84 -0
- package/dist/import/generator.js +292 -0
- package/dist/import/parser.js +141 -0
- package/dist/import/template-stripper.js +86 -0
- package/dist/index.js +55 -0
- package/dist/integrity.json +2 -2
- package/dist/intrinsics.js +482 -0
- package/dist/lint/audit-catalog.js +33 -0
- package/dist/lint/audit-lineage.js +53 -0
- package/dist/lint/post-synth/helm-helpers.js +60 -0
- package/dist/lint/post-synth/index.js +48 -0
- package/dist/lint/post-synth/whm005-no-empty-wrapper.js +47 -0
- package/dist/lint/post-synth/whm101.js +38 -0
- package/dist/lint/post-synth/whm102.js +26 -0
- package/dist/lint/post-synth/whm103.js +51 -0
- package/dist/lint/post-synth/whm104.js +29 -0
- package/dist/lint/post-synth/whm105.js +23 -0
- package/dist/lint/post-synth/whm201.js +29 -0
- package/dist/lint/post-synth/whm202.js +42 -0
- package/dist/lint/post-synth/whm203.js +32 -0
- package/dist/lint/post-synth/whm204.js +51 -0
- package/dist/lint/post-synth/whm301.js +31 -0
- package/dist/lint/post-synth/whm302.js +33 -0
- package/dist/lint/post-synth/whm401.js +52 -0
- package/dist/lint/post-synth/whm402.js +38 -0
- package/dist/lint/post-synth/whm403.js +38 -0
- package/dist/lint/post-synth/whm404.js +30 -0
- package/dist/lint/post-synth/whm405.js +47 -0
- package/dist/lint/post-synth/whm406.js +27 -0
- package/dist/lint/post-synth/whm407.js +72 -0
- package/dist/lint/post-synth/whm501.js +89 -0
- package/dist/lint/post-synth/whm502.js +85 -0
- package/dist/lint/post-synth/whm503.js +82 -0
- package/dist/lint/post-synth/whm504.js +47 -0
- package/dist/lint/rules/chart-metadata.js +53 -0
- package/dist/lint/rules/no-hardcoded-image.js +50 -0
- package/dist/lint/rules/values-no-helm-tpl.js +78 -0
- package/dist/lint/rules/values-no-secrets.js +65 -0
- package/dist/list-artifacts.js +134 -0
- package/dist/lsp/completions.d.ts.map +1 -1
- package/dist/lsp/completions.js +18 -0
- package/dist/lsp/hover.d.ts.map +1 -1
- package/dist/lsp/hover.js +40 -0
- package/dist/manifest.json +1 -1
- package/dist/op/activities/cluster-probe.js +116 -0
- package/dist/op/activities/helm.js +449 -0
- package/dist/op/activities/index.js +10 -0
- package/dist/op/builders.js +45 -0
- package/dist/package-cli.js +21 -0
- package/dist/pinnability/actions.js +207 -0
- package/dist/pinnability/classify.js +416 -0
- package/dist/pinnability/conditions.js +0 -0
- package/dist/pinnability/index.js +16 -0
- package/dist/pinnability/localize.js +518 -0
- package/dist/pinnability/render-stream.js +53 -0
- package/dist/pinnability/values.js +151 -0
- package/dist/plugin.js +458 -0
- package/dist/project-codegen.js +215 -0
- package/dist/release-observe.js +287 -0
- package/dist/render-diff.js +279 -0
- package/dist/render-digest.js +0 -0
- package/dist/render-records.js +29 -0
- package/dist/render-store.js +401 -0
- package/dist/render-wrapper.js +164 -0
- package/dist/render.js +394 -0
- package/dist/resources.js +109 -0
- package/dist/serializer.js +903 -0
- package/dist/validate-cli.js +17 -0
- package/dist/validate.js +30 -0
- package/dist/values-probe.js +504 -0
- package/dist/wrapper-chart.js +95 -0
- package/package.json +11 -11
- package/src/codegen/package.ts +2 -2
- package/src/lsp/completions.ts +2 -1
- package/src/lsp/hover.ts +2 -1
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Double-render localizer for open generated inputs (#1236, epic #1228
|
|
3
|
+
* Phase 1).
|
|
4
|
+
*
|
|
5
|
+
* The static classifier (#1234) NAMES generator sites; this module proves
|
|
6
|
+
* which of them actually fire and maps every unstable output line back to
|
|
7
|
+
* the input that produced it. The mechanism is the epic's own: pin the
|
|
8
|
+
* input, not the output. For each candidate values path that could close a
|
|
9
|
+
* generator, render the chart twice with that path pinned — the lines that
|
|
10
|
+
* stop differing are the lines that input produced, cascades included
|
|
11
|
+
* (finding 6: one `randAlphaNum` admin password differed as the secret line
|
|
12
|
+
* PLUS a `checksum/secret` annotation derived from it; hoisting the secret
|
|
13
|
+
* alone would have desynchronized the checksum and silently broken
|
|
14
|
+
* restart-on-change).
|
|
15
|
+
*
|
|
16
|
+
* Candidate pins come from three static sources, each validated dynamically
|
|
17
|
+
* before being suggested:
|
|
18
|
+
*
|
|
19
|
+
* 1. the generator's own action (`x | default (randAlphaNum 16)` — the
|
|
20
|
+
* classifier already names `x`),
|
|
21
|
+
* 2. enclosing control-flow gates (`if not .Values.core.existingSecret`
|
|
22
|
+
* around the generator: supplying the existing-secret slot closes it),
|
|
23
|
+
* 3. the include chain — a generator inside `define "grafana.password"`
|
|
24
|
+
* inherits the gates of every `include "grafana.password"` site, which
|
|
25
|
+
* is where grafana's `{{- if .Values.adminPassword }} ... {{- else }}`
|
|
26
|
+
* supply slot lives.
|
|
27
|
+
*
|
|
28
|
+
* A differing line no probe and no source mapping can explain is reported as
|
|
29
|
+
* UNLOCALIZED instability — never silently pinnable. A base double render
|
|
30
|
+
* with zero differing lines confirms the chart deterministic.
|
|
31
|
+
*/
|
|
32
|
+
import { readFileSync } from "node:fs";
|
|
33
|
+
import { extractActions, scopeActions, } from "./actions.js";
|
|
34
|
+
import { collectOwnTemplateFiles, } from "./classify.js";
|
|
35
|
+
import { parseExpr } from "./conditions.js";
|
|
36
|
+
import { splitDocuments, sourcePath } from "./render-stream.js";
|
|
37
|
+
import { buildChartInstances, mergeValues, readYamlFile, valueAtPath, } from "./values.js";
|
|
38
|
+
const PROBE_VALUE = "chant-localizer-probe";
|
|
39
|
+
function docIdOf(doc) {
|
|
40
|
+
const kind = doc.match(/^kind: (\S+)/m)?.[1];
|
|
41
|
+
const name = doc.match(/^ {2}name: "?([^"\n]+)"?$/m)?.[1];
|
|
42
|
+
if (kind === undefined)
|
|
43
|
+
return "?";
|
|
44
|
+
return name === undefined ? kind : `${kind}/${name}`;
|
|
45
|
+
}
|
|
46
|
+
function lineKeyOf(text, index) {
|
|
47
|
+
const m = text.match(/^\s*(?:- )?"?([^:"]{1,120}?)"?:/);
|
|
48
|
+
return m ? m[1] : `@${index}`;
|
|
49
|
+
}
|
|
50
|
+
/** Differing line indices of one paired document, positional. */
|
|
51
|
+
function diffDocLines(la, lb) {
|
|
52
|
+
if (la.length === lb.length) {
|
|
53
|
+
const out = [];
|
|
54
|
+
for (let i = 0; i < la.length; i++)
|
|
55
|
+
if (la[i] !== lb[i])
|
|
56
|
+
out.push(i);
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
// Unequal shapes: peel the common prefix and suffix, count A's middle.
|
|
60
|
+
let start = 0;
|
|
61
|
+
while (start < la.length && start < lb.length && la[start] === lb[start])
|
|
62
|
+
start++;
|
|
63
|
+
let endA = la.length;
|
|
64
|
+
let endB = lb.length;
|
|
65
|
+
while (endA > start && endB > start && la[endA - 1] === lb[endB - 1]) {
|
|
66
|
+
endA--;
|
|
67
|
+
endB--;
|
|
68
|
+
}
|
|
69
|
+
const out = [];
|
|
70
|
+
for (let i = start; i < endA; i++)
|
|
71
|
+
out.push(i);
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Diff two renders of the SAME inputs into a set of differing-line
|
|
76
|
+
* occurrences with identities stable across separate probe runs: documents
|
|
77
|
+
* pair by (`# Source:` origin, ordinal), lines identify by their YAML key
|
|
78
|
+
* plus ordinal rather than raw content — the content is exactly what varies.
|
|
79
|
+
*/
|
|
80
|
+
export function diffRenderPair(a, b) {
|
|
81
|
+
const out = new Map();
|
|
82
|
+
const group = (docs) => {
|
|
83
|
+
const g = new Map();
|
|
84
|
+
for (const d of docs) {
|
|
85
|
+
const src = sourcePath(d) ?? "?";
|
|
86
|
+
const arr = g.get(src);
|
|
87
|
+
if (arr === undefined)
|
|
88
|
+
g.set(src, [d]);
|
|
89
|
+
else
|
|
90
|
+
arr.push(d);
|
|
91
|
+
}
|
|
92
|
+
return g;
|
|
93
|
+
};
|
|
94
|
+
const groupsA = group(splitDocuments(a));
|
|
95
|
+
const groupsB = group(splitDocuments(b));
|
|
96
|
+
for (const [src, docsA] of groupsA) {
|
|
97
|
+
const docsB = groupsB.get(src) ?? [];
|
|
98
|
+
for (let ord = 0; ord < docsA.length; ord++) {
|
|
99
|
+
const docA = docsA[ord];
|
|
100
|
+
const docB = docsB[ord];
|
|
101
|
+
const linesA = docA.split("\n");
|
|
102
|
+
const differing = docB === undefined
|
|
103
|
+
? linesA.map((_, i) => i)
|
|
104
|
+
: diffDocLines(linesA, docB.split("\n"));
|
|
105
|
+
if (differing.length === 0)
|
|
106
|
+
continue;
|
|
107
|
+
const docKey = `${src}#${ord}`;
|
|
108
|
+
const docId = docIdOf(docA);
|
|
109
|
+
const keyCounts = new Map();
|
|
110
|
+
for (const i of differing) {
|
|
111
|
+
const key = lineKeyOf(linesA[i], i);
|
|
112
|
+
const n = keyCounts.get(key) ?? 0;
|
|
113
|
+
keyCounts.set(key, n + 1);
|
|
114
|
+
out.set(`${docKey}\u0000${key}\u0000${n}`, {
|
|
115
|
+
ident: `${docKey}\u0000${key}\u0000${n}`,
|
|
116
|
+
doc: src,
|
|
117
|
+
docId,
|
|
118
|
+
key,
|
|
119
|
+
line: i + 1,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
// --- static pin-candidate discovery ----------------------------------------
|
|
127
|
+
/**
|
|
128
|
+
* Values paths that, set truthy, make `expr` FALSE — closing a gate the
|
|
129
|
+
* generator sits behind. Only shapes a single string pin can definitively
|
|
130
|
+
* close are extracted: `not .Values.x` and `and`-conjunctions of them.
|
|
131
|
+
*/
|
|
132
|
+
function closingPins(node) {
|
|
133
|
+
if (node === undefined)
|
|
134
|
+
return [];
|
|
135
|
+
if (node.type === "call") {
|
|
136
|
+
if (node.op === "not" && node.args.length === 1)
|
|
137
|
+
return openingPins(node.args[0]);
|
|
138
|
+
if (node.op === "and")
|
|
139
|
+
return node.args.flatMap((a) => closingPins(a));
|
|
140
|
+
if (node.op === "or" && node.args.length === 1)
|
|
141
|
+
return closingPins(node.args[0]);
|
|
142
|
+
}
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
/** Values paths that, set truthy, make `expr` TRUE — killing an else branch. */
|
|
146
|
+
function openingPins(node) {
|
|
147
|
+
if (node === undefined)
|
|
148
|
+
return [];
|
|
149
|
+
if (node.type === "atom" && node.token.t === "word") {
|
|
150
|
+
const m = node.token.v.match(/^\$?\.Values((?:\.[A-Za-z0-9_-]+)+)$/);
|
|
151
|
+
return m ? [m[1].slice(1)] : [];
|
|
152
|
+
}
|
|
153
|
+
if (node.type === "call") {
|
|
154
|
+
if (node.op === "or")
|
|
155
|
+
return node.args.flatMap((a) => openingPins(a));
|
|
156
|
+
if (node.op === "and" && node.args.length === 1)
|
|
157
|
+
return openingPins(node.args[0]);
|
|
158
|
+
if (node.op === "not" && node.args.length === 1)
|
|
159
|
+
return closingPins(node.args[0]);
|
|
160
|
+
}
|
|
161
|
+
return [];
|
|
162
|
+
}
|
|
163
|
+
const STYLE_RANK = { supply: 0, "existing-slot": 1, gate: 2 };
|
|
164
|
+
function styleOfGatePath(path) {
|
|
165
|
+
return /existing/i.test(path.split(".").pop() ?? "") ? "existing-slot" : "gate";
|
|
166
|
+
}
|
|
167
|
+
const INCLUDE_RE = /(?:include|template)\s+"([^"]+)"/g;
|
|
168
|
+
const DEFINE_NAME_RE = /^(?:define|block)\s+"([^"]+)"/;
|
|
169
|
+
function scanChart(chartDir, supplied) {
|
|
170
|
+
const { instances } = buildChartInstances(chartDir, supplied);
|
|
171
|
+
const files = new Map();
|
|
172
|
+
const includeSites = new Map();
|
|
173
|
+
const seenDirs = new Set();
|
|
174
|
+
for (const instance of instances) {
|
|
175
|
+
// Aliased dependencies scan the same directory twice; the first
|
|
176
|
+
// instance's scope stands in for both (pin paths differ per alias, but
|
|
177
|
+
// the FILES exist once — same convention the classifier uses).
|
|
178
|
+
if (seenDirs.has(instance.dir))
|
|
179
|
+
continue;
|
|
180
|
+
seenDirs.add(instance.dir);
|
|
181
|
+
for (const abs of collectOwnTemplateFiles(instance.dir).sort()) {
|
|
182
|
+
const rel = abs.slice(instance.dir.length + 1);
|
|
183
|
+
const file = instance.relDir === "" ? rel : `${instance.relDir}/${rel}`;
|
|
184
|
+
const scoped = scopeActions(extractActions(readFileSync(abs, "utf8"), file));
|
|
185
|
+
// Track the enclosing define per action: define/block/if/with/range
|
|
186
|
+
// push, end pops — mirroring scopeActions' stack discipline.
|
|
187
|
+
const stack = [];
|
|
188
|
+
const containingDefine = [];
|
|
189
|
+
for (let i = 0; i < scoped.length; i++) {
|
|
190
|
+
const action = scoped[i].action;
|
|
191
|
+
const nearest = () => {
|
|
192
|
+
for (let s = stack.length - 1; s >= 0; s--) {
|
|
193
|
+
if (stack[s] !== undefined)
|
|
194
|
+
return stack[s];
|
|
195
|
+
}
|
|
196
|
+
return undefined;
|
|
197
|
+
};
|
|
198
|
+
switch (action.kind) {
|
|
199
|
+
case "define":
|
|
200
|
+
case "block": {
|
|
201
|
+
containingDefine.push(nearest());
|
|
202
|
+
stack.push(action.body.match(DEFINE_NAME_RE)?.[1]);
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
case "if":
|
|
206
|
+
case "with":
|
|
207
|
+
case "range":
|
|
208
|
+
containingDefine.push(nearest());
|
|
209
|
+
stack.push(undefined);
|
|
210
|
+
break;
|
|
211
|
+
case "end":
|
|
212
|
+
stack.pop();
|
|
213
|
+
containingDefine.push(nearest());
|
|
214
|
+
break;
|
|
215
|
+
default:
|
|
216
|
+
containingDefine.push(nearest());
|
|
217
|
+
}
|
|
218
|
+
for (const m of action.body.matchAll(INCLUDE_RE)) {
|
|
219
|
+
const sites = includeSites.get(m[1]);
|
|
220
|
+
const site = { file, index: i };
|
|
221
|
+
if (sites === undefined)
|
|
222
|
+
includeSites.set(m[1], [site]);
|
|
223
|
+
else
|
|
224
|
+
sites.push(site);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
files.set(file, { file, instance, scoped, containingDefine });
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return { files, includeSites };
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Candidate pins for one scoped action: gates from enclosing frames
|
|
234
|
+
* (closing a condition, or satisfying an else branch's negated condition),
|
|
235
|
+
* scoped into root coordinates.
|
|
236
|
+
*/
|
|
237
|
+
function pinsForAction(scan, index) {
|
|
238
|
+
const out = [];
|
|
239
|
+
const scope = scan.instance.scope;
|
|
240
|
+
const scopePath = (rel) => [...scope, rel].join(".");
|
|
241
|
+
for (const frame of scan.scoped[index].frames) {
|
|
242
|
+
for (const cond of frame.conditions) {
|
|
243
|
+
for (const p of closingPins(parseExpr(cond))) {
|
|
244
|
+
out.push({ path: scopePath(p), style: styleOfGatePath(p) });
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
for (const neg of frame.negatedConditions) {
|
|
248
|
+
// The else branch of `if .Values.x` runs the generator; supplying x
|
|
249
|
+
// is the supply-style pin — the value that replaces the generated one.
|
|
250
|
+
for (const p of openingPins(parseExpr(neg))) {
|
|
251
|
+
out.push({ path: scopePath(p), style: "supply" });
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return out;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* All candidate pins for a generator site, walking the include chain: a
|
|
259
|
+
* generator inside a define inherits the gates of every include site of
|
|
260
|
+
* that define, recursively (bounded).
|
|
261
|
+
*/
|
|
262
|
+
function candidatesForSite(scan, site, rootValues) {
|
|
263
|
+
const collected = [];
|
|
264
|
+
if (site.suppliable === true && site.valuesPath !== undefined) {
|
|
265
|
+
collected.push({ path: site.valuesPath, style: "supply" });
|
|
266
|
+
}
|
|
267
|
+
const fileScan = site.file !== undefined ? scan.files.get(site.file) : undefined;
|
|
268
|
+
if (fileScan !== undefined) {
|
|
269
|
+
const fnRe = new RegExp(`(?<![.\\w$])${site.fn}\\b`);
|
|
270
|
+
const index = fileScan.scoped.findIndex((s) => s.action.line === site.line && fnRe.test(s.action.body));
|
|
271
|
+
if (index !== -1) {
|
|
272
|
+
const visitedDefines = new Set();
|
|
273
|
+
const visit = (fs, actionIndex, depth) => {
|
|
274
|
+
collected.push(...pinsForAction(fs, actionIndex));
|
|
275
|
+
if (depth >= 4)
|
|
276
|
+
return;
|
|
277
|
+
const define = fs.containingDefine[actionIndex];
|
|
278
|
+
if (define === undefined || visitedDefines.has(define))
|
|
279
|
+
return;
|
|
280
|
+
visitedDefines.add(define);
|
|
281
|
+
for (const inc of scan.includeSites.get(define) ?? []) {
|
|
282
|
+
const incScan = scan.files.get(inc.file);
|
|
283
|
+
if (incScan !== undefined)
|
|
284
|
+
visit(incScan, inc.index, depth + 1);
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
visit(fileScan, index, 0);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
// Dedupe by path (strongest style wins); drop pins that cannot change the
|
|
291
|
+
// render: already-truthy values (the gate would already be closed) and
|
|
292
|
+
// structured values (a string pin would corrupt them).
|
|
293
|
+
const byPath = new Map();
|
|
294
|
+
for (const pin of collected) {
|
|
295
|
+
const current = valueAtPath(rootValues, pin.path.split("."));
|
|
296
|
+
if (current !== undefined && current !== null) {
|
|
297
|
+
if (typeof current === "object")
|
|
298
|
+
continue;
|
|
299
|
+
if (current !== "" && current !== false && current !== 0)
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
const existing = byPath.get(pin.path);
|
|
303
|
+
if (existing === undefined || STYLE_RANK[pin.style] < STYLE_RANK[existing.style]) {
|
|
304
|
+
byPath.set(pin.path, pin);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return [...byPath.values()].sort((a, b) => STYLE_RANK[a.style] - STYLE_RANK[b.style]);
|
|
308
|
+
}
|
|
309
|
+
// --- localization ----------------------------------------------------------
|
|
310
|
+
function suggestedPinFor(pin) {
|
|
311
|
+
switch (pin.style) {
|
|
312
|
+
case "supply":
|
|
313
|
+
return `${pin.path}: <generate once and supply>`;
|
|
314
|
+
case "existing-slot":
|
|
315
|
+
return `${pin.path}: <existing Secret name>`;
|
|
316
|
+
case "gate":
|
|
317
|
+
return `${pin.path}: <supply to disable in-template generation>`;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Localize the open generated inputs of a chart: double-render, and map
|
|
322
|
+
* every differing line back to the input that produced it by pinning
|
|
323
|
+
* candidate inputs one at a time. See the module doc for the mechanism.
|
|
324
|
+
*/
|
|
325
|
+
export function localizeOpenInputs(chartDir, report, options) {
|
|
326
|
+
let supplied;
|
|
327
|
+
for (const file of options.valuesFiles ?? []) {
|
|
328
|
+
supplied = mergeValues(supplied ?? {}, readYamlFile(file));
|
|
329
|
+
}
|
|
330
|
+
if (options.values !== undefined)
|
|
331
|
+
supplied = mergeValues(supplied ?? {}, options.values);
|
|
332
|
+
let renders = 0;
|
|
333
|
+
const probeCache = new Map();
|
|
334
|
+
const probe = (pins) => {
|
|
335
|
+
const key = JSON.stringify(Object.entries(pins).sort());
|
|
336
|
+
const cached = probeCache.get(key);
|
|
337
|
+
if (cached !== undefined)
|
|
338
|
+
return cached;
|
|
339
|
+
const a = options.render(pins);
|
|
340
|
+
const b = options.render(pins);
|
|
341
|
+
renders += 2;
|
|
342
|
+
const diff = diffRenderPair(a, b);
|
|
343
|
+
probeCache.set(key, diff);
|
|
344
|
+
return diff;
|
|
345
|
+
};
|
|
346
|
+
const base = probe({});
|
|
347
|
+
const sites = report.closedInputs.filter((c) => c.kind === "generated-value" &&
|
|
348
|
+
c.fn !== undefined &&
|
|
349
|
+
c.file !== undefined &&
|
|
350
|
+
c.line !== undefined);
|
|
351
|
+
const inputs = sites.map((s) => ({
|
|
352
|
+
fn: s.fn,
|
|
353
|
+
file: s.file,
|
|
354
|
+
line: s.line,
|
|
355
|
+
valuesPath: undefined,
|
|
356
|
+
suppliable: false,
|
|
357
|
+
suggestedPin: undefined,
|
|
358
|
+
existingSlots: [],
|
|
359
|
+
occurrences: [],
|
|
360
|
+
}));
|
|
361
|
+
if (base.size === 0) {
|
|
362
|
+
// Deterministic: the statically-named inputs did not fire. Report them
|
|
363
|
+
// with their static supply paths and no occurrences; no probes needed.
|
|
364
|
+
for (let i = 0; i < sites.length; i++) {
|
|
365
|
+
if (sites[i].suppliable === true && sites[i].valuesPath !== undefined) {
|
|
366
|
+
inputs[i].valuesPath = sites[i].valuesPath;
|
|
367
|
+
inputs[i].suppliable = true;
|
|
368
|
+
inputs[i].suggestedPin = suggestedPinFor({
|
|
369
|
+
path: sites[i].valuesPath,
|
|
370
|
+
style: "supply",
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return {
|
|
375
|
+
deterministic: true,
|
|
376
|
+
differingLines: 0,
|
|
377
|
+
inputs,
|
|
378
|
+
unlocalized: [],
|
|
379
|
+
renders,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
const rootValues = mergeValues(readYamlFile(`${chartDir}/values.yaml`) ?? {}, supplied);
|
|
383
|
+
const scan = scanChart(chartDir, supplied);
|
|
384
|
+
const maxProbes = options.maxProbesPerInput ?? 6;
|
|
385
|
+
const attributed = new Set();
|
|
386
|
+
const attributedTo = new Map();
|
|
387
|
+
const bestPin = new Array(sites.length).fill(undefined);
|
|
388
|
+
const attribute = (siteIndex, ident) => {
|
|
389
|
+
let set = attributedTo.get(siteIndex);
|
|
390
|
+
if (set === undefined) {
|
|
391
|
+
set = new Set();
|
|
392
|
+
attributedTo.set(siteIndex, set);
|
|
393
|
+
}
|
|
394
|
+
set.add(ident);
|
|
395
|
+
attributed.add(ident);
|
|
396
|
+
};
|
|
397
|
+
// Phase A — individual pin probes. A pin validates when it stabilizes at
|
|
398
|
+
// least one base-differing line; those lines belong to its site.
|
|
399
|
+
for (let i = 0; i < sites.length; i++) {
|
|
400
|
+
const cands = candidatesForSite(scan, sites[i], rootValues);
|
|
401
|
+
let probes = 0;
|
|
402
|
+
for (const cand of cands) {
|
|
403
|
+
if (probes >= maxProbes)
|
|
404
|
+
break;
|
|
405
|
+
// Plain gates are a last resort: once any pin validated, only probe
|
|
406
|
+
// remaining supply/existing-slot candidates (they name user slots).
|
|
407
|
+
if (cand.style === "gate" && bestPin[i] !== undefined)
|
|
408
|
+
continue;
|
|
409
|
+
probes += 1;
|
|
410
|
+
const pinned = probe({ [cand.path]: PROBE_VALUE });
|
|
411
|
+
const stabilized = [...base.keys()].filter((id) => !pinned.has(id));
|
|
412
|
+
if (stabilized.length === 0)
|
|
413
|
+
continue;
|
|
414
|
+
for (const id of stabilized)
|
|
415
|
+
attribute(i, id);
|
|
416
|
+
if (cand.style === "existing-slot")
|
|
417
|
+
inputs[i].existingSlots.push(cand.path);
|
|
418
|
+
if (bestPin[i] === undefined ||
|
|
419
|
+
STYLE_RANK[cand.style] < STYLE_RANK[bestPin[i].style]) {
|
|
420
|
+
bestPin[i] = cand;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
if (bestPin[i] !== undefined) {
|
|
424
|
+
const pin = bestPin[i];
|
|
425
|
+
inputs[i].suppliable = true;
|
|
426
|
+
inputs[i].suggestedPin = suggestedPinFor(pin);
|
|
427
|
+
if (pin.style === "supply")
|
|
428
|
+
inputs[i].valuesPath = pin.path;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
// Phase B — per-file joint probes for cascades with several roots: a
|
|
432
|
+
// checksum over a secret carrying two generated values stabilizes only
|
|
433
|
+
// when BOTH are pinned, so it attributes to every generator of that file.
|
|
434
|
+
const byFile = new Map();
|
|
435
|
+
for (let i = 0; i < sites.length; i++) {
|
|
436
|
+
if (bestPin[i] === undefined)
|
|
437
|
+
continue;
|
|
438
|
+
const arr = byFile.get(sites[i].file);
|
|
439
|
+
if (arr === undefined)
|
|
440
|
+
byFile.set(sites[i].file, [i]);
|
|
441
|
+
else
|
|
442
|
+
arr.push(i);
|
|
443
|
+
}
|
|
444
|
+
if ([...attributed].length < base.size) {
|
|
445
|
+
for (const members of byFile.values()) {
|
|
446
|
+
if (members.length < 2)
|
|
447
|
+
continue;
|
|
448
|
+
const pins = {};
|
|
449
|
+
for (const i of members)
|
|
450
|
+
pins[bestPin[i].path] = PROBE_VALUE;
|
|
451
|
+
const pinned = probe(pins);
|
|
452
|
+
for (const id of base.keys()) {
|
|
453
|
+
if (attributed.has(id) || pinned.has(id))
|
|
454
|
+
continue;
|
|
455
|
+
for (const i of members)
|
|
456
|
+
attribute(i, id);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
// Phase C — all validated pins at once: confirms that closing every
|
|
461
|
+
// localized input closes the chart, and catches cross-file joint cascades.
|
|
462
|
+
let stableWithAllPins;
|
|
463
|
+
const pinnedSites = bestPin.flatMap((p, i) => (p === undefined ? [] : [i]));
|
|
464
|
+
if (pinnedSites.length > 0) {
|
|
465
|
+
const pins = {};
|
|
466
|
+
for (const i of pinnedSites)
|
|
467
|
+
pins[bestPin[i].path] = PROBE_VALUE;
|
|
468
|
+
const finalDiff = probe(pins);
|
|
469
|
+
stableWithAllPins = finalDiff.size === 0;
|
|
470
|
+
for (const id of base.keys()) {
|
|
471
|
+
if (attributed.has(id) || finalDiff.has(id))
|
|
472
|
+
continue;
|
|
473
|
+
for (const i of pinnedSites)
|
|
474
|
+
attribute(i, id);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
// Phase D — source fallback for sites no pin can close (a genCA behind an
|
|
478
|
+
// include-computed gate): a differing line in a document rendered FROM the
|
|
479
|
+
// generator's own template maps to it without a probe.
|
|
480
|
+
for (const [id, occ] of base) {
|
|
481
|
+
if (attributed.has(id))
|
|
482
|
+
continue;
|
|
483
|
+
for (let i = 0; i < sites.length; i++) {
|
|
484
|
+
if (sites[i].file === occ.doc)
|
|
485
|
+
attribute(i, id);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
const toOccurrence = (occ, derived) => ({
|
|
489
|
+
doc: occ.doc,
|
|
490
|
+
docId: occ.docId,
|
|
491
|
+
key: occ.key,
|
|
492
|
+
line: occ.line,
|
|
493
|
+
derived,
|
|
494
|
+
});
|
|
495
|
+
const unlocalized = [];
|
|
496
|
+
for (const [id, occ] of base) {
|
|
497
|
+
if (!attributed.has(id)) {
|
|
498
|
+
unlocalized.push(toOccurrence(occ, false));
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
for (let i = 0; i < sites.length; i++) {
|
|
502
|
+
const ids = attributedTo.get(i);
|
|
503
|
+
if (ids === undefined)
|
|
504
|
+
continue;
|
|
505
|
+
inputs[i].occurrences = [...ids]
|
|
506
|
+
.map((id) => base.get(id))
|
|
507
|
+
.sort((a, b) => (a.doc === b.doc ? a.line - b.line : a.doc < b.doc ? -1 : 1))
|
|
508
|
+
.map((occ) => toOccurrence(occ, occ.doc !== sites[i].file));
|
|
509
|
+
}
|
|
510
|
+
return {
|
|
511
|
+
deterministic: false,
|
|
512
|
+
differingLines: base.size,
|
|
513
|
+
inputs,
|
|
514
|
+
unlocalized,
|
|
515
|
+
stableWithAllPins,
|
|
516
|
+
renders,
|
|
517
|
+
};
|
|
518
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers over a `helm template` output stream (#1234, epic #1228).
|
|
3
|
+
*
|
|
4
|
+
* The CRD routing rule here is what the structure-preserving wrapper
|
|
5
|
+
* (#1239) builds on, and the line diff seeds the double-render localizer
|
|
6
|
+
* (#1236). Both were proven in the survey harness before promotion.
|
|
7
|
+
*/
|
|
8
|
+
/** Split a `helm template` stream into non-empty documents. */
|
|
9
|
+
export function splitDocuments(rendered) {
|
|
10
|
+
return rendered
|
|
11
|
+
.split(/^---$/m)
|
|
12
|
+
.map((d) => d.trim())
|
|
13
|
+
.filter((d) => d.length > 0);
|
|
14
|
+
}
|
|
15
|
+
/** The `# Source: <chart>/<path>` origin of a rendered document, if any. */
|
|
16
|
+
export function sourcePath(doc) {
|
|
17
|
+
const m = doc.match(/^# Source: \S+?\/(\S+)$/m);
|
|
18
|
+
return m ? m[1] : undefined;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Finding 10: segment matching, not prefix matching. A subchart's CRDs
|
|
22
|
+
* arrive as `charts/<child>/crds/<file>`; `startsWith("crds/")` catches
|
|
23
|
+
* 0 of them and would route every one into templates/, making it deletable
|
|
24
|
+
* on uninstall.
|
|
25
|
+
*/
|
|
26
|
+
export function isCrdSource(path) {
|
|
27
|
+
return /(^|\/)crds\//.test(path);
|
|
28
|
+
}
|
|
29
|
+
/** Route rendered documents by origin: `crds/`-segment sources vs the rest. */
|
|
30
|
+
export function routeBySource(rendered) {
|
|
31
|
+
const routed = { crds: [], templates: [] };
|
|
32
|
+
for (const doc of splitDocuments(rendered)) {
|
|
33
|
+
const p = sourcePath(doc);
|
|
34
|
+
(p !== undefined && isCrdSource(p) ? routed.crds : routed.templates).push(doc);
|
|
35
|
+
}
|
|
36
|
+
return routed;
|
|
37
|
+
}
|
|
38
|
+
/** Count lines that differ between two renders (symmetric, order-blind). */
|
|
39
|
+
export function countDifferingLines(a, b) {
|
|
40
|
+
if (a === b)
|
|
41
|
+
return 0;
|
|
42
|
+
const linesA = a.split("\n");
|
|
43
|
+
const linesB = b.split("\n");
|
|
44
|
+
const count = new Map();
|
|
45
|
+
for (const l of linesA)
|
|
46
|
+
count.set(l, (count.get(l) ?? 0) + 1);
|
|
47
|
+
for (const l of linesB)
|
|
48
|
+
count.set(l, (count.get(l) ?? 0) - 1);
|
|
49
|
+
let diff = 0;
|
|
50
|
+
for (const n of count.values())
|
|
51
|
+
diff += Math.abs(n);
|
|
52
|
+
return diff;
|
|
53
|
+
}
|