@intentius/chant-lexicon-helm 0.46.0 → 0.50.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 (122) 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 +19 -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 +25 -13
  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/op/builders.d.ts +47 -0
  34. package/dist/op/builders.d.ts.map +1 -0
  35. package/dist/pinnability/actions.d.ts +76 -0
  36. package/dist/pinnability/actions.d.ts.map +1 -0
  37. package/dist/pinnability/classify.d.ts +129 -0
  38. package/dist/pinnability/classify.d.ts.map +1 -0
  39. package/dist/pinnability/conditions.d.ts +109 -0
  40. package/dist/pinnability/conditions.d.ts.map +1 -0
  41. package/dist/pinnability/index.d.ts +17 -0
  42. package/dist/pinnability/index.d.ts.map +1 -0
  43. package/dist/pinnability/localize.d.ts +124 -0
  44. package/dist/pinnability/localize.d.ts.map +1 -0
  45. package/dist/pinnability/render-stream.d.ts +27 -0
  46. package/dist/pinnability/render-stream.d.ts.map +1 -0
  47. package/dist/pinnability/values.d.ts +69 -0
  48. package/dist/pinnability/values.d.ts.map +1 -0
  49. package/dist/plugin.d.ts.map +1 -1
  50. package/dist/release-observe.d.ts +109 -0
  51. package/dist/release-observe.d.ts.map +1 -0
  52. package/dist/render-diff.d.ts +192 -0
  53. package/dist/render-diff.d.ts.map +1 -0
  54. package/dist/render-digest.d.ts +135 -0
  55. package/dist/render-digest.d.ts.map +1 -0
  56. package/dist/render-store.d.ts +310 -0
  57. package/dist/render-store.d.ts.map +1 -0
  58. package/dist/render-wrapper.d.ts +140 -0
  59. package/dist/render-wrapper.d.ts.map +1 -0
  60. package/dist/render.d.ts +81 -0
  61. package/dist/render.d.ts.map +1 -1
  62. package/dist/rules/whm503.ts +101 -0
  63. package/dist/rules/whm504.ts +52 -0
  64. package/dist/skills/chant-helm.md +7 -0
  65. package/dist/values-probe.d.ts +177 -0
  66. package/dist/values-probe.d.ts.map +1 -0
  67. package/dist/wrapper-chart.d.ts +61 -0
  68. package/dist/wrapper-chart.d.ts.map +1 -0
  69. package/package.json +10 -4
  70. package/src/archive-render.test.ts +201 -0
  71. package/src/archive-render.ts +119 -0
  72. package/src/commands.test.ts +487 -0
  73. package/src/commands.ts +445 -0
  74. package/src/config.test.ts +130 -0
  75. package/src/config.ts +220 -0
  76. package/src/deep-observe-hooks.ts +20 -0
  77. package/src/deep-observe.test.ts +165 -0
  78. package/src/deep-observe.ts +121 -0
  79. package/src/describe-resources.test.ts +255 -0
  80. package/src/describe-resources.ts +198 -0
  81. package/src/index.ts +106 -2
  82. package/src/lifecycle-integration.test.ts +289 -0
  83. package/src/lint/audit-catalog.ts +2 -0
  84. package/src/lint/post-synth/index.ts +4 -0
  85. package/src/lint/post-synth/whm503.test.ts +134 -0
  86. package/src/lint/post-synth/whm503.ts +101 -0
  87. package/src/lint/post-synth/whm504.test.ts +105 -0
  88. package/src/lint/post-synth/whm504.ts +52 -0
  89. package/src/op/activities/cluster-probe.test.ts +66 -0
  90. package/src/op/activities/cluster-probe.ts +148 -0
  91. package/src/op/activities/helm.test.ts +824 -0
  92. package/src/op/activities/helm.ts +628 -5
  93. package/src/op/activities/index.ts +17 -2
  94. package/src/op/builders.test.ts +70 -0
  95. package/src/op/builders.ts +66 -0
  96. package/src/pinnability/actions.ts +263 -0
  97. package/src/pinnability/classify.ts +579 -0
  98. package/src/pinnability/conditions.ts +0 -0
  99. package/src/pinnability/index.ts +77 -0
  100. package/src/pinnability/localize.test.ts +234 -0
  101. package/src/pinnability/localize.ts +644 -0
  102. package/src/pinnability/pinnability.test.ts +560 -0
  103. package/src/pinnability/render-stream.ts +59 -0
  104. package/src/pinnability/values.ts +205 -0
  105. package/src/plugin.test.ts +3 -1
  106. package/src/plugin.ts +97 -12
  107. package/src/release-observe.ts +392 -0
  108. package/src/render-diff.test.ts +366 -0
  109. package/src/render-diff.ts +407 -0
  110. package/src/render-digest.test.ts +186 -0
  111. package/src/render-digest.ts +207 -0
  112. package/src/render-store.test.ts +412 -0
  113. package/src/render-store.ts +621 -0
  114. package/src/render-wrapper.test.ts +259 -0
  115. package/src/render-wrapper.ts +274 -0
  116. package/src/render.test.ts +383 -4
  117. package/src/render.ts +319 -8
  118. package/src/skills/chant-helm.md +7 -0
  119. package/src/values-probe.test.ts +400 -0
  120. package/src/values-probe.ts +627 -0
  121. package/src/wrapper-chart.test.ts +110 -0
  122. package/src/wrapper-chart.ts +124 -0
package/src/render.ts CHANGED
@@ -44,6 +44,16 @@ import { Composite } from "@intentius/chant";
44
44
  import { Deployment } from "@intentius/chant-lexicon-k8s/generated/index";
45
45
  import yaml from "js-yaml";
46
46
 
47
+ import { resolveCapabilityProfile, type HelmCapabilityProfile, type HelmCapabilityProfileRef } from "./config";
48
+ import { helmContentDigest, helmInputDigest } from "./render-digest";
49
+ import {
50
+ findRenderByCacheKey,
51
+ loadRenderContent,
52
+ persistHelmRender,
53
+ renderCacheKey,
54
+ } from "./render-store";
55
+ import { probeCoalescedValues, type CoalescedValuesProbe } from "./values-probe";
56
+
47
57
  export interface HelmRenderProps {
48
58
  /** Logical name for the render (used in cache key + composite name). */
49
59
  name: string;
@@ -64,6 +74,95 @@ export interface HelmRenderProps {
64
74
  * fresh render.
65
75
  */
66
76
  noCache?: boolean;
77
+ /**
78
+ * Capability profile the render is pinned against (#1235, epic #1228).
79
+ *
80
+ * A string names a profile declared in `chant.config.ts`'s
81
+ * `helm.capabilityProfiles` (per cluster — see `./config.ts`); an inline
82
+ * object carries the same facts directly. When set, `helm template` runs
83
+ * with `--kube-version` and `--api-versions` from the profile, so
84
+ * `.Capabilities` reflects the declared cluster instead of whatever the
85
+ * helm binary defaults to. A named profile the config does not declare is
86
+ * an error at synth, never a silent fallback. Absent, rendering is
87
+ * unpinned — exactly today's behavior.
88
+ */
89
+ capabilityProfile?: HelmCapabilityProfileRef;
90
+ /**
91
+ * Whether this render persists to the content-addressed render store
92
+ * (#1238) — canonical bytes under their `contentDigest` plus a
93
+ * `RenderManifest`, in `~/.chant/helm-renders/sha256-<hex>/` (see
94
+ * ./render-store.ts).
95
+ *
96
+ * Only a pinned render (capabilityProfile present) can persist — an
97
+ * unpinned render has no content identity, and `persist: true` on one is
98
+ * a synth error naming that reason. For pinned renders the default
99
+ * follows the cache knob: persistence is on unless `noCache` is set
100
+ * (`noCache: true` + `persist: true` forces a fresh render that is still
101
+ * persisted; `persist: false` turns the store off entirely).
102
+ */
103
+ persist?: boolean;
104
+ /**
105
+ * Source ref/commit to record in the persisted `RenderManifest`, when the
106
+ * caller has one. Never resolved implicitly and never fabricated — absent
107
+ * means the manifest records `sourceRef: null`.
108
+ */
109
+ sourceRef?: string;
110
+ }
111
+
112
+ /**
113
+ * What one `HelmRender` invocation recorded about itself. `capabilityProfile`
114
+ * is the profile identity the render was pinned against; `undefined` means
115
+ * the render was unpinned and its bytes depend on the local helm binary's
116
+ * defaults.
117
+ *
118
+ * Pinned renders (profile present — the v1 gate, see #1237) also carry the
119
+ * digest pair:
120
+ *
121
+ * - `inputDigest` — `sha256:` over the canonical JSON of the declared inputs
122
+ * (chart reference, version, values, capability facts). Shared with the
123
+ * release-ledger digest #1243 records on deploy, via `helmInputDigest`.
124
+ * Answers "same inputs?" without touching any bytes.
125
+ * - `contentDigest` — `sha256:` over the canonical rendered bytes
126
+ * (`canonicalizeRender`). The artifact identity: answers "same bytes on
127
+ * the cluster?".
128
+ *
129
+ * They diverge exactly when the render is not a function of its declared
130
+ * inputs — `renderStability` in ./render-digest.ts names that.
131
+ *
132
+ * Unpinned renders record neither digest. Their bytes are a function of the
133
+ * local helm binary's defaulted capabilities, so a digest over them would
134
+ * assert an identity the render does not have — it would differ across
135
+ * machines that did nothing differently, and equal digests would still
136
+ * prove nothing about a cluster. No digest is the honest record.
137
+ */
138
+ export interface HelmRenderRecord {
139
+ /** The render's logical name (`HelmRenderProps.name`) — also the helm release name baked into the bytes. */
140
+ name: string;
141
+ chart: string;
142
+ version?: string;
143
+ capabilityProfile?: HelmCapabilityProfile;
144
+ /** Input-side identity (#1237/#1243). Present only for pinned renders. */
145
+ inputDigest?: string;
146
+ /** Content-side identity over canonical rendered bytes (#1237). Present only for pinned renders. */
147
+ contentDigest?: string;
148
+ /**
149
+ * The build-time coalesced-values probe's digest (#1251), present only
150
+ * when the probe ran — a pinned render of a local chart (the probe needs
151
+ * the chart source on disk, so a repo-fetched chart never gets one).
152
+ */
153
+ coalescedValuesDigest?: string;
154
+ }
155
+
156
+ const renderRecords: HelmRenderRecord[] = [];
157
+
158
+ /** Every render recorded in this process, in invocation order. */
159
+ export function getHelmRenderRecords(): readonly HelmRenderRecord[] {
160
+ return renderRecords;
161
+ }
162
+
163
+ /** Reset the record list (test isolation). */
164
+ export function clearHelmRenderRecords(): void {
165
+ renderRecords.length = 0;
67
166
  }
68
167
 
69
168
  interface RenderedDoc {
@@ -75,22 +174,61 @@ interface RenderedDoc {
75
174
 
76
175
  const CACHE_ROOT = join(homedir(), ".chant", "helm-renders");
77
176
 
78
- function cacheKey(props: HelmRenderProps): string {
177
+ /**
178
+ * Legacy cache key — truncated, unprefixed, input-derived. Since #1238 this
179
+ * keys only *unpinned* renders (and names the values tempfile): pinned
180
+ * renders live in the content-addressed store (./render-store.ts) under
181
+ * their full `sha256:` contentDigest, with the inputs index providing cache
182
+ * hits. Existing truncated-hash entries stay where they are — a new root,
183
+ * not a migration.
184
+ */
185
+ function cacheKey(props: HelmRenderProps, profile?: HelmCapabilityProfile): string {
79
186
  const stable = JSON.stringify({
80
187
  repo: props.repo,
81
188
  chart: props.chart,
82
189
  version: props.version,
83
190
  namespace: props.namespace ?? null,
84
191
  values: props.values ?? null,
192
+ // Only present for pinned renders (which no longer read this cache but
193
+ // still name their values tempfile with this key). A pinned render must
194
+ // never reuse an unpinned render's bytes (or another profile's) — the
195
+ // profile is a real render input (#1235).
196
+ ...(profile
197
+ ? {
198
+ capabilityProfile: {
199
+ name: profile.name,
200
+ kubeVersion: profile.kubeVersion,
201
+ apiVersions: profile.apiVersions ?? [],
202
+ },
203
+ }
204
+ : {}),
85
205
  });
86
206
  return createHash("sha256").update(stable).digest("hex").slice(0, 16);
87
207
  }
88
208
 
89
- function renderViaHelm(props: HelmRenderProps): string {
209
+ /**
210
+ * Version of the local helm binary, for the persisted `RenderManifest`.
211
+ * Load-bearing there — the defaulted kube version is a property of the
212
+ * binary, so this field explains a digest mismatch between machines. Never
213
+ * fails a build: `"unknown"` when helm cannot answer.
214
+ */
215
+ function helmBinaryVersion(): string {
216
+ try {
217
+ const out = execFileSync("helm", ["version", "--template", "{{.Version}}"], {
218
+ encoding: "utf8",
219
+ stdio: ["ignore", "pipe", "ignore"],
220
+ });
221
+ return out.trim().split("\n")[0] || "unknown";
222
+ } catch {
223
+ return "unknown";
224
+ }
225
+ }
226
+
227
+ function renderViaHelm(props: HelmRenderProps, profile?: HelmCapabilityProfile): string {
90
228
  // Write values overrides to a tempfile if any.
91
229
  let valuesArgs: string[] = [];
92
230
  if (props.values && Object.keys(props.values).length > 0) {
93
- const valuesPath = join(tmpdir(), `chant-helm-values-${cacheKey(props)}.yaml`);
231
+ const valuesPath = join(tmpdir(), `chant-helm-values-${cacheKey(props, profile)}.yaml`);
94
232
  writeFileSync(valuesPath, yaml.dump(props.values));
95
233
  valuesArgs = ["--values", valuesPath];
96
234
  }
@@ -124,6 +262,17 @@ function renderViaHelm(props: HelmRenderProps): string {
124
262
  // Without this, `helm template` silently drops manifests shipped in the
125
263
  // chart's (or a subchart's) crds/ directory.
126
264
  "--include-crds",
265
+ // Pin .Capabilities to the declared cluster profile. Without these, the
266
+ // kube version defaults to one baked into the helm binary and APIVersions
267
+ // is empty — both silently, both making the rendered bytes a function of
268
+ // the local toolchain (#1235).
269
+ ...(profile
270
+ ? [
271
+ "--kube-version",
272
+ profile.kubeVersion,
273
+ ...(profile.apiVersions ?? []).flatMap((v) => ["--api-versions", v]),
274
+ ]
275
+ : []),
127
276
  ...fetchArgs,
128
277
  ...isolationArgs,
129
278
  ...(props.namespace ? ["--namespace", props.namespace] : []),
@@ -149,16 +298,16 @@ function renderViaHelm(props: HelmRenderProps): string {
149
298
  }
150
299
  }
151
300
 
152
- function loadOrRender(props: HelmRenderProps): string {
301
+ function loadOrRender(props: HelmRenderProps, profile?: HelmCapabilityProfile): string {
153
302
  if (props.noCache) {
154
- return renderViaHelm(props);
303
+ return renderViaHelm(props, profile);
155
304
  }
156
- const cacheDir = join(CACHE_ROOT, cacheKey(props));
305
+ const cacheDir = join(CACHE_ROOT, cacheKey(props, profile));
157
306
  const cachePath = join(cacheDir, "manifests.yaml");
158
307
  if (existsSync(cachePath)) {
159
308
  return readFileSync(cachePath, "utf8");
160
309
  }
161
- const out = renderViaHelm(props);
310
+ const out = renderViaHelm(props, profile);
162
311
  try {
163
312
  mkdirSync(cacheDir, { recursive: true });
164
313
  writeFileSync(cachePath, out);
@@ -168,6 +317,112 @@ function loadOrRender(props: HelmRenderProps): string {
168
317
  return out;
169
318
  }
170
319
 
320
+ /**
321
+ * Best-effort coalesced-values probe (#1251) for a `HelmRender` invocation.
322
+ *
323
+ * The probe (`./values-probe.ts`) needs the chart source on disk — the same
324
+ * requirement the pinnability classifier (#1234) has — so it only runs for
325
+ * a local chart (`props.repo` unset, `props.chart` a directory with a
326
+ * `Chart.yaml`). A repo-fetched chart's source is never kept on disk here,
327
+ * so it gets no probe, the same v1 gate `render.ts`'s digest recording
328
+ * already documents.
329
+ *
330
+ * Never fatal: a probe failure (a chart the probe's template injection
331
+ * can't render, `helm` briefly unavailable, ...) must not break the actual
332
+ * render it is only observing. `undefined` means "no probe result" — never
333
+ * fabricated, and WHM504 simply has nothing to report for this render.
334
+ */
335
+ function runValuesProbeBestEffort(props: HelmRenderProps): CoalescedValuesProbe | undefined {
336
+ if (props.repo) return undefined;
337
+ if (!existsSync(join(props.chart, "Chart.yaml"))) return undefined;
338
+ try {
339
+ return probeCoalescedValues({
340
+ chartDir: props.chart,
341
+ name: props.name,
342
+ releaseName: props.name,
343
+ namespace: props.namespace,
344
+ supplied:
345
+ props.values && Object.keys(props.values).length > 0
346
+ ? [{ origin: "supplied file", values: props.values }]
347
+ : [],
348
+ });
349
+ } catch {
350
+ // Non-fatal — see the docstring above. The main render already ran (or
351
+ // is about to) via its own `helm template` invocation; this extra,
352
+ // probe-only render observing the same chart is never load-bearing.
353
+ return undefined;
354
+ }
355
+ }
356
+
357
+ /**
358
+ * The pinned render path (#1238): the content-addressed store is both the
359
+ * cache and the durable record.
360
+ *
361
+ * Read: unless `noCache` (or `persist: false`) is set, the full-inputs
362
+ * cache key resolves through the store's inputs index to canonical bytes a
363
+ * previous identical render stored — semantically identical to helm's
364
+ * output (canonicalization only normalizes render noise) and byte-stable
365
+ * under re-canonicalization, so digests recompute identically.
366
+ *
367
+ * Write: a fresh render persists { canonical bytes, RenderManifest } unless
368
+ * the caller opted out — the manifest is written alongside every pinned
369
+ * render by default. `persist: true` with `noCache` still persists (a
370
+ * forced-fresh render is still a durable artifact); a persist failure on
371
+ * the default path is non-fatal like a legacy cache-write failure, but an
372
+ * explicit `persist: true` surfaces it.
373
+ */
374
+ function loadOrRenderPinned(
375
+ props: HelmRenderProps,
376
+ profile: HelmCapabilityProfile,
377
+ valuesProbe?: CoalescedValuesProbe,
378
+ ): string {
379
+ const chartRef = props.repo ? `${props.repo}/${props.chart}` : props.chart;
380
+ const storeRead = !props.noCache && props.persist !== false;
381
+ const storeWrite = props.persist === true || (!props.noCache && props.persist !== false);
382
+ const key = renderCacheKey({
383
+ chart: chartRef,
384
+ chartVersion: props.version,
385
+ releaseName: props.name,
386
+ namespace: props.namespace,
387
+ values: props.values,
388
+ capabilityProfile: profile,
389
+ });
390
+
391
+ if (storeRead) {
392
+ const hit = findRenderByCacheKey(key);
393
+ if (hit) {
394
+ const content = loadRenderContent(hit.contentDigest);
395
+ if (content !== undefined) return content;
396
+ // Index points at pruned/corrupt content — fall through and re-render.
397
+ }
398
+ }
399
+
400
+ const out = renderViaHelm(props, profile);
401
+ if (storeWrite) {
402
+ try {
403
+ persistHelmRender({
404
+ rendered: out,
405
+ releaseName: props.name,
406
+ chart: props.chart,
407
+ repo: props.repo || undefined,
408
+ chartVersion: props.version,
409
+ namespace: props.namespace,
410
+ values: props.values,
411
+ capabilityProfile: profile,
412
+ helmVersion: helmBinaryVersion(),
413
+ sourceRef: props.sourceRef,
414
+ coalescedValuesDigest: valuesProbe?.digest,
415
+ valueSources: valuesProbe?.valueSources,
416
+ });
417
+ } catch (err) {
418
+ if (props.persist === true) throw err;
419
+ // Default-on persistence degrades like a legacy cache-write failure:
420
+ // the render is still in memory and the build goes on.
421
+ }
422
+ }
423
+ return out;
424
+ }
425
+
171
426
  function parseMultiDoc(text: string): RenderedDoc[] {
172
427
  const docs = yaml.loadAll(text);
173
428
  return docs
@@ -184,9 +439,65 @@ function safeKey(input: string): string {
184
439
  }
185
440
 
186
441
  export const HelmRender = Composite<HelmRenderProps>((props) => {
187
- const yamlText = loadOrRender(props);
442
+ // Resolve first: a named profile the config does not declare must fail the
443
+ // build here, before any helm invocation could silently render against the
444
+ // binary's default capabilities.
445
+ const profile = props.capabilityProfile !== undefined ? resolveCapabilityProfile(props.capabilityProfile) : undefined;
446
+
447
+ if (!profile && props.persist === true) {
448
+ throw new Error(
449
+ `HelmRender "${props.name}": persist requires a pinned render, and this one is unpinned ` +
450
+ `(no capabilityProfile declared). An unpinned render's bytes are a function of the local ` +
451
+ `helm binary's defaulted capabilities, so they have no stable content identity to store ` +
452
+ `under. Declare capabilityProfile to pin the render (#1235), or drop persist.`,
453
+ );
454
+ }
455
+
456
+ // Build-time coalesced-values probe (#1251/#1252). Runs (best-effort) for
457
+ // a pinned render of a local chart — the same gate the digests below use,
458
+ // since the probe needs the chart source on disk and only a pinned render
459
+ // has a RenderManifest to carry its digest. `probeCoalescedValues` records
460
+ // itself into the WHM504 surface (`getValuesProbeRecords`) as part of
461
+ // running — no separate `recordValuesProbe` call needed here, and any
462
+ // dead assignment it found is worth reporting whether or not this render
463
+ // ultimately persists.
464
+ const valuesProbe = profile ? runValuesProbeBestEffort(props) : undefined;
465
+
466
+ const yamlText = profile ? loadOrRenderPinned(props, profile, valuesProbe) : loadOrRender(props, undefined);
188
467
  const docs = parseMultiDoc(yamlText);
189
468
 
469
+ // Digests are recorded only for pinned renders (#1237). Profile presence
470
+ // is the v1 gate: the classifier (#1234) needs the chart source on disk,
471
+ // which a repo-fetched render does not keep, so the gate here is the
472
+ // declared-inputs property, not a template analysis. An unpinned render's
473
+ // digest would be a function of the local helm binary — meaningless as an
474
+ // identity — so unpinned renders record neither digest.
475
+ const digests = profile
476
+ ? {
477
+ inputDigest: helmInputDigest({
478
+ // Same chart-reference convention helmInstall digests: a local
479
+ // path stays itself; a repo-fetched chart is `<repo-url>/<chart>`.
480
+ chart: props.repo ? `${props.repo}/${props.chart}` : props.chart,
481
+ chartVersion: props.version,
482
+ values: props.values ?? {},
483
+ capabilityProfile: {
484
+ kubeVersion: profile.kubeVersion,
485
+ apiVersions: profile.apiVersions,
486
+ },
487
+ }),
488
+ contentDigest: helmContentDigest(yamlText),
489
+ ...(valuesProbe ? { coalescedValuesDigest: valuesProbe.digest } : {}),
490
+ }
491
+ : {};
492
+
493
+ renderRecords.push({
494
+ name: props.name,
495
+ chart: props.chart,
496
+ version: props.version,
497
+ capabilityProfile: profile,
498
+ ...digests,
499
+ });
500
+
190
501
  const out: Record<string, InstanceType<typeof Deployment>> = {};
191
502
 
192
503
  if (props.createNamespace && props.namespace) {
@@ -289,6 +289,13 @@ const lifecycle = HelmCRDLifecycle({
289
289
  | WHM407 | post-synth | Secret with inline data |
290
290
  | WHM501 | post-synth | Unused values keys |
291
291
  | WHM502 | post-synth | Deprecated K8s API versions |
292
+ | WHM503 | pinned-render | Pinned render artifact carries populated Secret data/stringData — use `runtimeSlot()` or `HelmExternalSecret` |
293
+
294
+ WHM503 is a different stage from the rest: it scans the STORED, rendered
295
+ artifact a pinned `HelmRender` writes to the render store (#1238), not the
296
+ chart source `WHM005`-`WHM502` validate. `WHM407` catches an inline literal
297
+ in a chart TEMPLATE before rendering; WHM503 catches a resolved credential
298
+ that made it into a durable, promotable artifact.
292
299
 
293
300
  ## OCI registry workflow
294
301