@intentius/chant-lexicon-helm 0.53.1 → 0.54.1

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "algorithm": "sha256",
3
3
  "artifacts": {
4
- "manifest.json": "816f563c3e8797335edeee248215b216c57f786d8f80780d4997839ab259bbb3",
4
+ "manifest.json": "2dafdf3357b31ae4e71eb1a9d231ab393023c53d6a8c418b6eb2d4f4653e44c9",
5
5
  "meta.json": "14243c5730a07c6a6edc35ddd351438547d58df5cf345f2233a355b0c7611ccc",
6
6
  "types/index.d.ts": "5377696ca8698cd2999e4680feb8e8e4b54a7b49fb603a87b2f27356114d1794",
7
7
  "rules/chart-metadata.ts": "8f3377e893d5e2828460b7fe5924fca098334245a9a2fdb90f6b67e490eaf091",
@@ -30,11 +30,11 @@
30
30
  "rules/whm407.ts": "200190f5de2d86d2bcc5aee0b5ce4807919ec727b1fc14576ea17b9b3073838b",
31
31
  "rules/whm501.ts": "e6afa7c0eface9820380bf189fe8be58f4a6997dd5b74f76335022650616d080",
32
32
  "rules/whm502.ts": "c3ed7b4b5215d96e46eba758aca2171928d8f8fb1eb20452ea6b4393672755b0",
33
- "rules/whm503.ts": "72808bba7c66ef71757a2dc73c9c3afff5980fd729bbdd5ba00af9c0c2213304",
33
+ "rules/whm503.ts": "84c2c4d3e261d12a1dbeee974af02d512ccdc5c0495119c0d6f997619db019dd",
34
34
  "rules/whm504.ts": "15c03b47c271270eee93de45310ff6dfc2665dfe221d457665a638cfd9637005",
35
35
  "skills/chant-helm.md": "828a169f9275c58df0a079287befb14c450ad8a92257e3cb6960450221941973",
36
36
  "skills/chant-helm-patterns.md": "9e79e6a46391da46709d8aa57e2825a7cd9eb981cd923f02ad60836c49b2561e",
37
37
  "skills/chant-helm-security.md": "bfc367eabceed2e84f1cf94501b407df78aeed963cec104f24a321d0962063c9"
38
38
  },
39
- "composite": "ed37bd53ac897b52e32634e217895250b330336de9b4bf1834a90fa6043680fe"
39
+ "composite": "35076abd609245fabdb6d5e6067e82654eba0b56b705c5b0c0eb96de8d9c403f"
40
40
  }
@@ -19,7 +19,7 @@
19
19
  * non-empty.
20
20
  */
21
21
  import type { PostSynthCheck, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
22
- import type { HelmRenderRecord } from "../../render.js";
22
+ import type { HelmRenderRecord } from "../../render-records.js";
23
23
  /**
24
24
  * Scan a set of `HelmRenderRecord`s for pinned renders whose stored artifact
25
25
  * carries a `Secret` document with populated `data`/`stringData`. Exported
@@ -1 +1 @@
1
- {"version":3,"file":"whm503.d.ts","sourceRoot":"","sources":["../../../src/lint/post-synth/whm503.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAoB,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAG9G,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAcrD;;;;;;GAMG;AACH,wBAAgB,4BAA4B,CAC1C,OAAO,EAAE,SAAS,gBAAgB,EAAE,EACpC,IAAI,CAAC,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GACvB,mBAAmB,EAAE,CAwCvB;AAED,eAAO,MAAM,MAAM,EAAE,cAUpB,CAAC"}
1
+ {"version":3,"file":"whm503.d.ts","sourceRoot":"","sources":["../../../src/lint/post-synth/whm503.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAoB,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAM9G,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAc7D;;;;;;GAMG;AACH,wBAAgB,4BAA4B,CAC1C,OAAO,EAAE,SAAS,gBAAgB,EAAE,EACpC,IAAI,CAAC,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GACvB,mBAAmB,EAAE,CAwCvB;AAED,eAAO,MAAM,MAAM,EAAE,cAUpB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"list-artifacts.d.ts","sourceRoot":"","sources":["../src/list-artifacts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAwBjE,wBAAsB,aAAa,CAAC,OAAO,EAAE;IAC3C,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAAC;CAC/E,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAoD5C"}
1
+ {"version":3,"file":"list-artifacts.d.ts","sourceRoot":"","sources":["../src/list-artifacts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AA4DjE,wBAAsB,aAAa,CAAC,OAAO,EAAE;IAC3C,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAAC;CAC/E,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC,CA0E5C"}
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "helm",
3
- "version": "0.53.1",
3
+ "version": "0.54.1",
4
4
  "chantVersion": ">=0.1.0",
5
5
  "namespace": "Helm",
6
6
  "intrinsics": [
@@ -0,0 +1,67 @@
1
+ /**
2
+ * The in-process ledger of `HelmRender` invocations — split out of
3
+ * `./render.ts` so a post-synth check can read it without importing the
4
+ * render machinery (#1979 follow-up, found by the hosted audit's bump).
5
+ *
6
+ * WHM503 (`./lint/post-synth/whm503.ts`) scans these records, and post-synth
7
+ * barrels are edge-imported by the hosted audit service: everything they
8
+ * reach through static imports must be safe to initialize on workerd.
9
+ * `render.ts` is not — it imports the full `@intentius/chant` barrel, whose
10
+ * static graph reaches the TypeScript compiler (fold-import), and
11
+ * TypeScript's own module init touches `__filename`, which crashes the
12
+ * worker at startup. This module carries only the record shape and the
13
+ * ledger, imports nothing but a type, and keeps the barrel edge-clean;
14
+ * `render.ts` re-exports it all, so the lexicon's public surface is
15
+ * unchanged.
16
+ */
17
+ import type { HelmCapabilityProfile } from "./config.js";
18
+ /**
19
+ * What one `HelmRender` invocation recorded about itself. `capabilityProfile`
20
+ * is the profile identity the render was pinned against; `undefined` means
21
+ * the render was unpinned and its bytes depend on the local helm binary's
22
+ * defaults.
23
+ *
24
+ * Pinned renders (profile present — the v1 gate, see #1237) also carry the
25
+ * digest pair:
26
+ *
27
+ * - `inputDigest` — `sha256:` over the canonical JSON of the declared inputs
28
+ * (chart reference, version, values, capability facts). Shared with the
29
+ * release-ledger digest #1243 records on deploy, via `helmInputDigest`.
30
+ * Answers "same inputs?" without touching any bytes.
31
+ * - `contentDigest` — `sha256:` over the canonical rendered bytes
32
+ * (`canonicalizeRender`). The artifact identity: answers "same bytes on
33
+ * the cluster?".
34
+ *
35
+ * They diverge exactly when the render is not a function of its declared
36
+ * inputs — `renderStability` in ./render-digest.ts names that.
37
+ *
38
+ * Unpinned renders record neither digest. Their bytes are a function of the
39
+ * local helm binary's defaulted capabilities, so a digest over them would
40
+ * assert an identity the render does not have — it would differ across
41
+ * machines that did nothing differently, and equal digests would still
42
+ * prove nothing about a cluster. No digest is the honest record.
43
+ */
44
+ export interface HelmRenderRecord {
45
+ /** The render's logical name (`HelmRenderProps.name`) — also the helm release name baked into the bytes. */
46
+ name: string;
47
+ chart: string;
48
+ version?: string;
49
+ capabilityProfile?: HelmCapabilityProfile;
50
+ /** Input-side identity (#1237/#1243). Present only for pinned renders. */
51
+ inputDigest?: string;
52
+ /** Content-side identity over canonical rendered bytes (#1237). Present only for pinned renders. */
53
+ contentDigest?: string;
54
+ /**
55
+ * The build-time coalesced-values probe's digest (#1251), present only
56
+ * when the probe ran — a pinned render of a local chart (the probe needs
57
+ * the chart source on disk, so a repo-fetched chart never gets one).
58
+ */
59
+ coalescedValuesDigest?: string;
60
+ }
61
+ /** Append one invocation's record (called by `HelmRender`, ./render.ts). */
62
+ export declare function recordHelmRender(record: HelmRenderRecord): void;
63
+ /** Every render recorded in this process, in invocation order. */
64
+ export declare function getHelmRenderRecords(): readonly HelmRenderRecord[];
65
+ /** Reset the record list (test isolation). */
66
+ export declare function clearHelmRenderRecords(): void;
67
+ //# sourceMappingURL=render-records.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render-records.d.ts","sourceRoot":"","sources":["../src/render-records.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AAEtD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,WAAW,gBAAgB;IAC/B,4GAA4G;IAC5G,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iBAAiB,CAAC,EAAE,qBAAqB,CAAC;IAC1C,0EAA0E;IAC1E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,oGAAoG;IACpG,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC;AAID,4EAA4E;AAC5E,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,IAAI,CAE/D;AAED,kEAAkE;AAClE,wBAAgB,oBAAoB,IAAI,SAAS,gBAAgB,EAAE,CAElE;AAED,8CAA8C;AAC9C,wBAAgB,sBAAsB,IAAI,IAAI,CAE7C"}
package/dist/render.d.ts CHANGED
@@ -30,7 +30,7 @@
30
30
  * values: { installCRDs: true },
31
31
  * });
32
32
  */
33
- import { type HelmCapabilityProfile, type HelmCapabilityProfileRef } from "./config.js";
33
+ import { type HelmCapabilityProfileRef } from "./config.js";
34
34
  export interface HelmRenderProps {
35
35
  /** Logical name for the render (used in cache key + composite name). */
36
36
  name: string;
@@ -85,52 +85,6 @@ export interface HelmRenderProps {
85
85
  */
86
86
  sourceRef?: string;
87
87
  }
88
- /**
89
- * What one `HelmRender` invocation recorded about itself. `capabilityProfile`
90
- * is the profile identity the render was pinned against; `undefined` means
91
- * the render was unpinned and its bytes depend on the local helm binary's
92
- * defaults.
93
- *
94
- * Pinned renders (profile present — the v1 gate, see #1237) also carry the
95
- * digest pair:
96
- *
97
- * - `inputDigest` — `sha256:` over the canonical JSON of the declared inputs
98
- * (chart reference, version, values, capability facts). Shared with the
99
- * release-ledger digest #1243 records on deploy, via `helmInputDigest`.
100
- * Answers "same inputs?" without touching any bytes.
101
- * - `contentDigest` — `sha256:` over the canonical rendered bytes
102
- * (`canonicalizeRender`). The artifact identity: answers "same bytes on
103
- * the cluster?".
104
- *
105
- * They diverge exactly when the render is not a function of its declared
106
- * inputs — `renderStability` in ./render-digest.ts names that.
107
- *
108
- * Unpinned renders record neither digest. Their bytes are a function of the
109
- * local helm binary's defaulted capabilities, so a digest over them would
110
- * assert an identity the render does not have — it would differ across
111
- * machines that did nothing differently, and equal digests would still
112
- * prove nothing about a cluster. No digest is the honest record.
113
- */
114
- export interface HelmRenderRecord {
115
- /** The render's logical name (`HelmRenderProps.name`) — also the helm release name baked into the bytes. */
116
- name: string;
117
- chart: string;
118
- version?: string;
119
- capabilityProfile?: HelmCapabilityProfile;
120
- /** Input-side identity (#1237/#1243). Present only for pinned renders. */
121
- inputDigest?: string;
122
- /** Content-side identity over canonical rendered bytes (#1237). Present only for pinned renders. */
123
- contentDigest?: string;
124
- /**
125
- * The build-time coalesced-values probe's digest (#1251), present only
126
- * when the probe ran — a pinned render of a local chart (the probe needs
127
- * the chart source on disk, so a repo-fetched chart never gets one).
128
- */
129
- coalescedValuesDigest?: string;
130
- }
131
- /** Every render recorded in this process, in invocation order. */
132
- export declare function getHelmRenderRecords(): readonly HelmRenderRecord[];
133
- /** Reset the record list (test isolation). */
134
- export declare function clearHelmRenderRecords(): void;
88
+ export { getHelmRenderRecords, clearHelmRenderRecords, type HelmRenderRecord } from "./render-records.js";
135
89
  export declare const HelmRender: import("@intentius/chant").CompositeDefinition<HelmRenderProps, import("@intentius/chant").CompositeMembers>;
136
90
  //# sourceMappingURL=render.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAeH,OAAO,EAA4B,KAAK,qBAAqB,EAAE,KAAK,wBAAwB,EAAE,MAAM,UAAU,CAAC;AAU/G,MAAM,WAAW,eAAe;IAC9B,wEAAwE;IACxE,IAAI,EAAE,MAAM,CAAC;IACb,8DAA8D;IAC9D,IAAI,EAAE,MAAM,CAAC;IACb,0CAA0C;IAC1C,KAAK,EAAE,MAAM,CAAC;IACd,0CAA0C;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,8DAA8D;IAC9D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,sDAAsD;IACtD,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,2EAA2E;IAC3E,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;;;;;;;;OAWG;IACH,iBAAiB,CAAC,EAAE,wBAAwB,CAAC;IAC7C;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,WAAW,gBAAgB;IAC/B,4GAA4G;IAC5G,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iBAAiB,CAAC,EAAE,qBAAqB,CAAC;IAC1C,0EAA0E;IAC1E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,oGAAoG;IACpG,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC;AAID,kEAAkE;AAClE,wBAAgB,oBAAoB,IAAI,SAAS,gBAAgB,EAAE,CAElE;AAED,8CAA8C;AAC9C,wBAAgB,sBAAsB,IAAI,IAAI,CAE7C;AAmRD,eAAO,MAAM,UAAU,8GAsFP,CAAC"}
1
+ {"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAeH,OAAO,EAAwD,KAAK,wBAAwB,EAAE,MAAM,UAAU,CAAC;AAY/G,MAAM,WAAW,eAAe;IAC9B,wEAAwE;IACxE,IAAI,EAAE,MAAM,CAAC;IACb,8DAA8D;IAC9D,IAAI,EAAE,MAAM,CAAC;IACb,0CAA0C;IAC1C,KAAK,EAAE,MAAM,CAAC;IACd,0CAA0C;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,8DAA8D;IAC9D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,sDAAsD;IACtD,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,2EAA2E;IAC3E,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;;;;;;;;OAWG;IACH,iBAAiB,CAAC,EAAE,wBAAwB,CAAC;IAC7C;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAMD,OAAO,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,KAAK,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AA8RvG,eAAO,MAAM,UAAU,8GAsFP,CAAC"}
@@ -22,8 +22,11 @@
22
22
  import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
23
23
  import yaml from "js-yaml";
24
24
 
25
- import type { HelmRenderRecord } from "../../render";
26
- import { getHelmRenderRecords } from "../../render";
25
+ // From ./render-records, NOT ./render: this barrel is edge-imported by the
26
+ // hosted audit, and render.ts's static graph reaches the TypeScript compiler
27
+ // through the @intentius/chant barrel — whose module init crashes workerd.
28
+ import type { HelmRenderRecord } from "../../render-records";
29
+ import { getHelmRenderRecords } from "../../render-records";
27
30
  import { loadRenderManifest, readRenderDocument } from "../../render-store";
28
31
 
29
32
  interface ParsedSecretDoc {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant-lexicon-helm",
3
- "version": "0.53.1",
3
+ "version": "0.54.1",
4
4
  "description": "Helm chart lexicon for chant — declarative IaC in TypeScript",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://intentius.io/chant",
@@ -86,7 +86,7 @@
86
86
  "@types/js-yaml": "^4.0.9"
87
87
  },
88
88
  "peerDependencies": {
89
- "@intentius/chant": "^0.53.1",
89
+ "@intentius/chant": "^0.54.1",
90
90
  "typescript": "^5.9.3",
91
91
  "zod": "^4.3.6"
92
92
  }
@@ -22,8 +22,11 @@
22
22
  import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
23
23
  import yaml from "js-yaml";
24
24
 
25
- import type { HelmRenderRecord } from "../../render";
26
- import { getHelmRenderRecords } from "../../render";
25
+ // From ./render-records, NOT ./render: this barrel is edge-imported by the
26
+ // hosted audit, and render.ts's static graph reaches the TypeScript compiler
27
+ // through the @intentius/chant barrel — whose module init crashes workerd.
28
+ import type { HelmRenderRecord } from "../../render-records";
29
+ import { getHelmRenderRecords } from "../../render-records";
27
30
  import { loadRenderManifest, readRenderDocument } from "../../render-store";
28
31
 
29
32
  interface ParsedSecretDoc {
@@ -11,11 +11,37 @@ vi.mock("node:child_process", async () => {
11
11
  } };
12
12
  });
13
13
 
14
+ const ledgerMock = vi.fn();
15
+ vi.mock("@intentius/chant/lifecycle/release-ledger", async () => {
16
+ const actual = await vi.importActual<typeof import("@intentius/chant/lifecycle/release-ledger")>(
17
+ "@intentius/chant/lifecycle/release-ledger",
18
+ );
19
+ return { ...actual, readReleaseLedger: (env: string) => ledgerMock(env) };
20
+ });
21
+
14
22
  const { listArtifacts } = await import("./list-artifacts");
15
23
 
24
+ /** One ledger line as the helm deploy activity records it (#1243). */
25
+ function ledgerRecord(overrides: Record<string, unknown>): Record<string, unknown> {
26
+ return {
27
+ version: 1,
28
+ component: "web",
29
+ env: "prod",
30
+ digest: "sha256:input-digest",
31
+ gitSha: "deadbeef",
32
+ runId: "run-1",
33
+ timestamp: "2026-08-30T00:00:00.000Z",
34
+ actor: "ci-bot",
35
+ ...overrides,
36
+ };
37
+ }
38
+
16
39
  describe("helm listArtifacts", () => {
17
40
  beforeEach(() => {
18
41
  execMock.mockReset();
42
+ ledgerMock.mockReset();
43
+ // Default: no ledger readable — the join contributes nothing.
44
+ ledgerMock.mockRejectedValue(new Error("no lifecycle branch"));
19
45
  });
20
46
 
21
47
  test("queries `helm list -A -o json` and maps releases to artifacts", async () => {
@@ -91,4 +117,79 @@ describe("helm listArtifacts", () => {
91
117
  const result = await listArtifacts({ environment: "prod", entities: new Map() });
92
118
  expect(result).toEqual({});
93
119
  });
120
+
121
+ describe("render identity read back from the release ledger (#2031)", () => {
122
+ const webRelease = { name: "web", namespace: "default", revision: "1", status: "deployed", chart: "web-1.0.0", app_version: "1.0" };
123
+
124
+ test("an unpinned chant deploy reports its inputDigest", async () => {
125
+ execMock.mockResolvedValue({ stdout: JSON.stringify([webRelease]), stderr: "" });
126
+ ledgerMock.mockResolvedValue({ records: [ledgerRecord({})], malformed: 0 });
127
+
128
+ const result = await listArtifacts({ environment: "prod", entities: new Map() });
129
+ expect(ledgerMock).toHaveBeenCalledWith("prod");
130
+ expect(result["release/default/web"].attributes).toMatchObject({ inputDigest: "sha256:input-digest" });
131
+ expect(result["release/default/web"].attributes!.contentDigest).toBeUndefined();
132
+ });
133
+
134
+ test("a pinned deploy reports contentDigest and inputDigest — joinable to `helm renders`", async () => {
135
+ execMock.mockResolvedValue({ stdout: JSON.stringify([webRelease]), stderr: "" });
136
+ ledgerMock.mockResolvedValue({
137
+ records: [ledgerRecord({ digest: "sha256:content-digest", inputDigest: "sha256:input-digest" })],
138
+ malformed: 0,
139
+ });
140
+
141
+ const result = await listArtifacts({ environment: "prod", entities: new Map() });
142
+ expect(result["release/default/web"].attributes).toMatchObject({
143
+ inputDigest: "sha256:input-digest",
144
+ contentDigest: "sha256:content-digest",
145
+ });
146
+ });
147
+
148
+ test("the latest record per component wins", async () => {
149
+ execMock.mockResolvedValue({ stdout: JSON.stringify([webRelease]), stderr: "" });
150
+ ledgerMock.mockResolvedValue({
151
+ records: [
152
+ ledgerRecord({ digest: "sha256:older", timestamp: "2026-08-01T00:00:00.000Z" }),
153
+ ledgerRecord({ digest: "sha256:newer", timestamp: "2026-08-30T00:00:00.000Z" }),
154
+ ],
155
+ malformed: 0,
156
+ });
157
+ const result = await listArtifacts({ environment: "prod", entities: new Map() });
158
+ expect(result["release/default/web"].attributes!.inputDigest).toBe("sha256:newer");
159
+ });
160
+
161
+ test("a release deployed outside chant reports nothing — absent is the honest answer", async () => {
162
+ execMock.mockResolvedValue({
163
+ stdout: JSON.stringify([webRelease, { ...webRelease, name: "manual", chart: "manual-0.1.0" }]),
164
+ stderr: "",
165
+ });
166
+ ledgerMock.mockResolvedValue({ records: [ledgerRecord({})], malformed: 0 });
167
+
168
+ const result = await listArtifacts({ environment: "prod", entities: new Map() });
169
+ expect(result["release/default/web"].attributes!.inputDigest).toBe("sha256:input-digest");
170
+ expect(result["release/default/manual"].attributes!.inputDigest).toBeUndefined();
171
+ expect(result["release/default/manual"].attributes!.contentDigest).toBeUndefined();
172
+ });
173
+
174
+ test("one name in several namespaces is ambiguous against a namespace-less record — neither joins", async () => {
175
+ execMock.mockResolvedValue({
176
+ stdout: JSON.stringify([webRelease, { ...webRelease, namespace: "staging-ns" }]),
177
+ stderr: "",
178
+ });
179
+ ledgerMock.mockResolvedValue({ records: [ledgerRecord({})], malformed: 0 });
180
+
181
+ const result = await listArtifacts({ environment: "prod", entities: new Map() });
182
+ expect(result["release/default/web"].attributes!.inputDigest).toBeUndefined();
183
+ expect(result["release/staging-ns/web"].attributes!.inputDigest).toBeUndefined();
184
+ });
185
+
186
+ test("an unreadable ledger joins nothing rather than failing the snapshot", async () => {
187
+ execMock.mockResolvedValue({ stdout: JSON.stringify([webRelease]), stderr: "" });
188
+ ledgerMock.mockRejectedValue(new Error("not a git repository"));
189
+
190
+ const result = await listArtifacts({ environment: "prod", entities: new Map() });
191
+ expect(result["release/default/web"]).toBeDefined();
192
+ expect(result["release/default/web"].attributes!.inputDigest).toBeUndefined();
193
+ });
194
+ });
94
195
  });
@@ -17,9 +17,45 @@ import { promisify } from "node:util";
17
17
  import type { ArtifactMetadata } from "@intentius/chant/lexicon";
18
18
  import { loadChantConfigUpward } from "@intentius/chant/config";
19
19
  import { resolveClusterTarget } from "@intentius/chant/kubectl-context";
20
+ import { readReleaseLedger, latestPerComponent, type ReleaseRecord } from "@intentius/chant/lifecycle/release-ledger";
20
21
 
21
22
  const execAsync = promisify(exec);
22
23
 
24
+ /**
25
+ * The render identity a chant deploy recorded for a release (#2031). `helm
26
+ * list` cannot know which bytes a release was installed from — but the
27
+ * release ledger already records it on deploy (#1243): the input digest for
28
+ * an unpinned deploy, contentDigest + inputDigest for a pinned one. This
29
+ * reads that record back, so an observed release joins `helm renders` /
30
+ * `helm diff` by digest instead of by chart-name prefix matching.
31
+ *
32
+ * The join key is the ledger record's `component`, which the helm deploy
33
+ * activity defaults to the release name (`args.component ?? args.name`,
34
+ * ./op/activities/helm.ts). Deliberately best-effort and honest about
35
+ * absence:
36
+ *
37
+ * - a release deployed outside chant has no record, and reports nothing;
38
+ * - a ledger that cannot be read (no repo, no lifecycle branch) joins
39
+ * nothing rather than failing the snapshot;
40
+ * - two observed releases sharing one name (different namespaces) are
41
+ * ambiguous — the record does not carry a namespace — so neither joins;
42
+ * - a record whose `component` was overridden away from the release name
43
+ * does not join (the honest miss, same as today).
44
+ */
45
+ function renderIdentityOf(
46
+ record: ReleaseRecord | undefined,
47
+ ): { inputDigest?: string; contentDigest?: string } {
48
+ if (!record) return {};
49
+ if (record.inputDigest) {
50
+ // Pinned deploy (#1242): `digest` is the render's contentDigest, the
51
+ // input identity rides alongside.
52
+ return { inputDigest: record.inputDigest, contentDigest: record.digest };
53
+ }
54
+ // Unpinned deploy: the record is keyed by the input digest itself
55
+ // (`ReleaseRecord.inputDigest` is "absent when digest is already input-side").
56
+ return { inputDigest: record.digest };
57
+ }
58
+
23
59
  interface HelmListEntry {
24
60
  name?: string;
25
61
  namespace?: string;
@@ -75,9 +111,29 @@ export async function listArtifacts(options: {
75
111
  return result;
76
112
  }
77
113
 
114
+ // The environment's latest release record per component (#2031) — the
115
+ // deploy-side identity read back. Best-effort: an unreadable ledger joins
116
+ // nothing rather than blocking the snapshot.
117
+ let recorded: Map<string, ReleaseRecord> = new Map();
118
+ try {
119
+ const { records } = await readReleaseLedger(options.environment);
120
+ recorded = latestPerComponent(records);
121
+ } catch {
122
+ recorded = new Map();
123
+ }
124
+
125
+ // A name observed in several namespaces is ambiguous against a record that
126
+ // carries no namespace — neither joins.
127
+ const nameCounts = new Map<string, number>();
128
+ for (const entry of entries) {
129
+ if (entry.name) nameCounts.set(entry.name, (nameCounts.get(entry.name) ?? 0) + 1);
130
+ }
131
+
78
132
  for (const entry of entries) {
79
133
  if (!entry.name || !entry.namespace) continue;
80
134
  const key = `release/${entry.namespace}/${entry.name}`;
135
+ const record = nameCounts.get(entry.name) === 1 ? recorded.get(entry.name) : undefined;
136
+ const { inputDigest, contentDigest } = renderIdentityOf(record);
81
137
  result[key] = {
82
138
  type: "Helm::Release",
83
139
  physicalId: `${entry.namespace}/${entry.name}`,
@@ -88,6 +144,8 @@ export async function listArtifacts(options: {
88
144
  revision: entry.revision,
89
145
  appVersion: entry.app_version,
90
146
  namespace: entry.namespace,
147
+ inputDigest,
148
+ contentDigest,
91
149
  }),
92
150
  };
93
151
  }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * The in-process ledger of `HelmRender` invocations — split out of
3
+ * `./render.ts` so a post-synth check can read it without importing the
4
+ * render machinery (#1979 follow-up, found by the hosted audit's bump).
5
+ *
6
+ * WHM503 (`./lint/post-synth/whm503.ts`) scans these records, and post-synth
7
+ * barrels are edge-imported by the hosted audit service: everything they
8
+ * reach through static imports must be safe to initialize on workerd.
9
+ * `render.ts` is not — it imports the full `@intentius/chant` barrel, whose
10
+ * static graph reaches the TypeScript compiler (fold-import), and
11
+ * TypeScript's own module init touches `__filename`, which crashes the
12
+ * worker at startup. This module carries only the record shape and the
13
+ * ledger, imports nothing but a type, and keeps the barrel edge-clean;
14
+ * `render.ts` re-exports it all, so the lexicon's public surface is
15
+ * unchanged.
16
+ */
17
+
18
+ import type { HelmCapabilityProfile } from "./config";
19
+
20
+ /**
21
+ * What one `HelmRender` invocation recorded about itself. `capabilityProfile`
22
+ * is the profile identity the render was pinned against; `undefined` means
23
+ * the render was unpinned and its bytes depend on the local helm binary's
24
+ * defaults.
25
+ *
26
+ * Pinned renders (profile present — the v1 gate, see #1237) also carry the
27
+ * digest pair:
28
+ *
29
+ * - `inputDigest` — `sha256:` over the canonical JSON of the declared inputs
30
+ * (chart reference, version, values, capability facts). Shared with the
31
+ * release-ledger digest #1243 records on deploy, via `helmInputDigest`.
32
+ * Answers "same inputs?" without touching any bytes.
33
+ * - `contentDigest` — `sha256:` over the canonical rendered bytes
34
+ * (`canonicalizeRender`). The artifact identity: answers "same bytes on
35
+ * the cluster?".
36
+ *
37
+ * They diverge exactly when the render is not a function of its declared
38
+ * inputs — `renderStability` in ./render-digest.ts names that.
39
+ *
40
+ * Unpinned renders record neither digest. Their bytes are a function of the
41
+ * local helm binary's defaulted capabilities, so a digest over them would
42
+ * assert an identity the render does not have — it would differ across
43
+ * machines that did nothing differently, and equal digests would still
44
+ * prove nothing about a cluster. No digest is the honest record.
45
+ */
46
+ export interface HelmRenderRecord {
47
+ /** The render's logical name (`HelmRenderProps.name`) — also the helm release name baked into the bytes. */
48
+ name: string;
49
+ chart: string;
50
+ version?: string;
51
+ capabilityProfile?: HelmCapabilityProfile;
52
+ /** Input-side identity (#1237/#1243). Present only for pinned renders. */
53
+ inputDigest?: string;
54
+ /** Content-side identity over canonical rendered bytes (#1237). Present only for pinned renders. */
55
+ contentDigest?: string;
56
+ /**
57
+ * The build-time coalesced-values probe's digest (#1251), present only
58
+ * when the probe ran — a pinned render of a local chart (the probe needs
59
+ * the chart source on disk, so a repo-fetched chart never gets one).
60
+ */
61
+ coalescedValuesDigest?: string;
62
+ }
63
+
64
+ const renderRecords: HelmRenderRecord[] = [];
65
+
66
+ /** Append one invocation's record (called by `HelmRender`, ./render.ts). */
67
+ export function recordHelmRender(record: HelmRenderRecord): void {
68
+ renderRecords.push(record);
69
+ }
70
+
71
+ /** Every render recorded in this process, in invocation order. */
72
+ export function getHelmRenderRecords(): readonly HelmRenderRecord[] {
73
+ return renderRecords;
74
+ }
75
+
76
+ /** Reset the record list (test isolation). */
77
+ export function clearHelmRenderRecords(): void {
78
+ renderRecords.length = 0;
79
+ }
@@ -1,5 +1,5 @@
1
1
  import { describe, test, expect, beforeAll, beforeEach, afterEach } from "vitest";
2
- import { mkdirSync, writeFileSync, rmSync, existsSync, readFileSync, mkdtempSync } from "node:fs";
2
+ import { mkdirSync, writeFileSync, rmSync, existsSync, readFileSync, readdirSync, mkdtempSync } from "node:fs";
3
3
  import { delimiter, join } from "node:path";
4
4
  import { execFileSync } from "node:child_process";
5
5
  import { tmpdir } from "node:os";
@@ -237,6 +237,39 @@ describe.skipIf(!fixtureAvailable)("HelmRender", () => {
237
237
  expect(dep.props.spec.replicas).toBe(3);
238
238
  });
239
239
 
240
+ test("CHANT_HELM_RENDER_ROOT redirects the unpinned cache — the air-gapped handoff (#2035)", () => {
241
+ // Before #2035 the unprofiled path hardcoded ~/.chant/helm-renders and
242
+ // honoured no environment variable, so a hermetic build had no way to be
243
+ // handed a pre-rendered chart short of pre-warming $HOME.
244
+ const root = mkdtempSync(join(tmpdir(), "chant-helm-render-root-"));
245
+ const origRoot = process.env.CHANT_HELM_RENDER_ROOT;
246
+ process.env.CHANT_HELM_RENDER_ROOT = root;
247
+ try {
248
+ const first = HelmRender({ name: "rel-rooted", chart: CHART_DIR });
249
+ expect(Object.keys(first.members as Record<string, unknown>).length).toBeGreaterThanOrEqual(2);
250
+ // The cache landed under the override, not under $HOME.
251
+ const cached = readdirSync(root).filter((d) => !d.startsWith("_"));
252
+ expect(cached.length).toBe(1);
253
+ expect(existsSync(join(root, cached[0], "manifests.yaml"))).toBe(true);
254
+
255
+ // A machine with no helm at all renders from the handed-over store.
256
+ const emptyDir = join(tmpdir(), "chant-helm-render-empty-path");
257
+ if (!existsSync(emptyDir)) mkdirSync(emptyDir);
258
+ const origPath = process.env.PATH;
259
+ process.env.PATH = emptyDir;
260
+ try {
261
+ const second = HelmRender({ name: "rel-rooted", chart: CHART_DIR });
262
+ expect(Object.keys(second.members as Record<string, unknown>).length).toBeGreaterThanOrEqual(2);
263
+ } finally {
264
+ process.env.PATH = origPath;
265
+ }
266
+ } finally {
267
+ if (origRoot === undefined) delete process.env.CHANT_HELM_RENDER_ROOT;
268
+ else process.env.CHANT_HELM_RENDER_ROOT = origRoot;
269
+ rmSync(root, { recursive: true, force: true });
270
+ }
271
+ });
272
+
240
273
  test("cache reuse: second render with same args skips helm CLI", () => {
241
274
  // First, render with cache enabled.
242
275
  const first = HelmRender({
package/src/render.ts CHANGED
@@ -34,7 +34,7 @@
34
34
  import { execFileSync } from "node:child_process";
35
35
  import { createHash } from "node:crypto";
36
36
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
37
- import { homedir, tmpdir } from "node:os";
37
+ import { tmpdir } from "node:os";
38
38
  import { join } from "node:path";
39
39
 
40
40
  import { Composite } from "@intentius/chant";
@@ -51,8 +51,10 @@ import {
51
51
  loadRenderContent,
52
52
  persistHelmRender,
53
53
  renderCacheKey,
54
+ renderStoreRoot,
54
55
  } from "./render-store";
55
56
  import { probeCoalescedValues, type CoalescedValuesProbe } from "./values-probe";
57
+ import { recordHelmRender } from "./render-records";
56
58
 
57
59
  export interface HelmRenderProps {
58
60
  /** Logical name for the render (used in cache key + composite name). */
@@ -109,61 +111,11 @@ export interface HelmRenderProps {
109
111
  sourceRef?: string;
110
112
  }
111
113
 
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;
166
- }
114
+ // The invocation-record ledger lives in ./render-records.ts — a module with
115
+ // no runtime imports so WHM503 (edge-imported via the post-synth barrel)
116
+ // can read it without pulling this module's heavy static graph. Re-exported
117
+ // here to keep the lexicon's public surface unchanged.
118
+ export { getHelmRenderRecords, clearHelmRenderRecords, type HelmRenderRecord } from "./render-records";
167
119
 
168
120
  interface RenderedDoc {
169
121
  apiVersion?: string;
@@ -172,7 +124,18 @@ interface RenderedDoc {
172
124
  [k: string]: unknown;
173
125
  }
174
126
 
175
- const CACHE_ROOT = join(homedir(), ".chant", "helm-renders");
127
+ /**
128
+ * Root of the legacy unpinned cache — the same directory (and the same
129
+ * `CHANT_HELM_RENDER_ROOT` override) as the content-addressed render store
130
+ * (#2035). Before this the unprofiled path hardcoded `~/.chant/helm-renders`
131
+ * and honoured no environment variable, so a hermetic or air-gapped build
132
+ * had no way to hand it a pre-rendered chart short of pre-warming `$HOME`.
133
+ * A function, not a module-level constant, so the override is read when a
134
+ * render happens rather than when this module loaded.
135
+ */
136
+ function cacheRoot(): string {
137
+ return renderStoreRoot();
138
+ }
176
139
 
177
140
  /**
178
141
  * Legacy cache key — truncated, unprefixed, input-derived. Since #1238 this
@@ -251,7 +214,7 @@ function renderViaHelm(props: HelmRenderProps, profile?: HelmCapabilityProfile):
251
214
  "--repository-config",
252
215
  "/dev/null",
253
216
  "--repository-cache",
254
- join(CACHE_ROOT, "_helm-repo-cache"),
217
+ join(cacheRoot(), "_helm-repo-cache"),
255
218
  ]
256
219
  : [];
257
220
 
@@ -302,7 +265,7 @@ function loadOrRender(props: HelmRenderProps, profile?: HelmCapabilityProfile):
302
265
  if (props.noCache) {
303
266
  return renderViaHelm(props, profile);
304
267
  }
305
- const cacheDir = join(CACHE_ROOT, cacheKey(props, profile));
268
+ const cacheDir = join(cacheRoot(), cacheKey(props, profile));
306
269
  const cachePath = join(cacheDir, "manifests.yaml");
307
270
  if (existsSync(cachePath)) {
308
271
  return readFileSync(cachePath, "utf8");
@@ -490,7 +453,7 @@ export const HelmRender = Composite<HelmRenderProps>((props) => {
490
453
  }
491
454
  : {};
492
455
 
493
- renderRecords.push({
456
+ recordHelmRender({
494
457
  name: props.name,
495
458
  chart: props.chart,
496
459
  version: props.version,