@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
@@ -0,0 +1,255 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { normalizeObservation } from "@intentius/chant/observation";
3
+ import { describeResources } from "./describe-resources";
4
+ import { parseReleaseDocuments } from "./release-observe";
5
+ import type { HelmRunner } from "./release-observe";
6
+
7
+ const LIST = JSON.stringify([
8
+ {
9
+ name: "web-app",
10
+ namespace: "prod",
11
+ revision: "3",
12
+ updated: "2026-08-01 10:00:00.000000000 +0000 UTC",
13
+ status: "deployed",
14
+ chart: "web-app-0.1.0",
15
+ app_version: "1.0.0",
16
+ },
17
+ ]);
18
+
19
+ const MANIFEST = `---
20
+ # Source: web-app/templates/deployment.yaml
21
+ apiVersion: apps/v1
22
+ kind: Deployment
23
+ metadata:
24
+ name: web-app
25
+ labels:
26
+ app.kubernetes.io/managed-by: chant
27
+ chant.intentius.io/stack: shop
28
+ chant.intentius.io/env: prod
29
+ spec:
30
+ replicas: 2
31
+ ---
32
+ # Source: web-app/templates/service.yaml
33
+ apiVersion: v1
34
+ kind: Service
35
+ metadata:
36
+ name: web-app
37
+ namespace: elsewhere
38
+ spec:
39
+ ports:
40
+ - port: 80
41
+ ---
42
+ # Source: web-app/crds/widgets.yaml
43
+ apiVersion: apiextensions.k8s.io/v1
44
+ kind: CustomResourceDefinition
45
+ metadata:
46
+ name: widgets.example.com
47
+ `;
48
+
49
+ const HOOKS = `---
50
+ # Source: web-app/templates/migrate-job.yaml
51
+ apiVersion: batch/v1
52
+ kind: Job
53
+ metadata:
54
+ name: web-app-migrate
55
+ annotations:
56
+ "helm.sh/hook": pre-install,pre-upgrade
57
+ "helm.sh/hook-weight": "1"
58
+ "helm.sh/hook-delete-policy": hook-succeeded
59
+ `;
60
+
61
+ /** A scripted helm CLI: canned stdout per subcommand, recorded commands. */
62
+ function scriptedHelm(
63
+ outputs: { list?: string; manifest?: string; hooks?: string },
64
+ commands: string[] = [],
65
+ ): HelmRunner {
66
+ return async (command) => {
67
+ commands.push(command);
68
+ if (command.startsWith("helm list")) return { stdout: outputs.list ?? "[]" };
69
+ if (command.startsWith("helm get manifest")) {
70
+ if (outputs.manifest === undefined) throw new Error("release: not found");
71
+ return { stdout: outputs.manifest };
72
+ }
73
+ if (command.startsWith("helm get hooks")) {
74
+ if (outputs.hooks === undefined) throw new Error("release: not found");
75
+ return { stdout: outputs.hooks };
76
+ }
77
+ throw new Error(`unexpected command: ${command}`);
78
+ };
79
+ }
80
+
81
+ const chartEntities = new Map([
82
+ ["chart", { entityType: "Helm::Chart", props: { name: "web-app" } }],
83
+ ["valuesSchema", { entityType: "Helm::Values", props: { replicaCount: 2 } }],
84
+ ]);
85
+
86
+ const baseOptions = {
87
+ environment: "prod",
88
+ buildOutput: "",
89
+ entityNames: [...chartEntities.keys()],
90
+ entities: chartEntities,
91
+ };
92
+
93
+ describe("helm describeResources (#1246)", () => {
94
+ test("reads BOTH channels: manifest resources and hook resources are rows", async () => {
95
+ const commands: string[] = [];
96
+ const result = await describeResources(baseOptions, scriptedHelm({ list: LIST, manifest: MANIFEST, hooks: HOOKS }, commands));
97
+ const { resources, unobserved } = normalizeObservation(result);
98
+
99
+ expect(commands.some((c) => c.startsWith("helm get manifest 'web-app' -n 'prod'"))).toBe(true);
100
+ expect(commands.some((c) => c.startsWith("helm get hooks 'web-app' -n 'prod'"))).toBe(true);
101
+
102
+ // The release row, keyed by the chart entity.
103
+ expect(resources.chart).toMatchObject({
104
+ type: "Helm::Release",
105
+ physicalId: "prod/web-app",
106
+ status: "deployed",
107
+ ownership: "owned",
108
+ });
109
+
110
+ // Manifest channel: namespace-silent documents inherit the release
111
+ // namespace; explicit namespaces are kept; cluster-scoped kinds get none.
112
+ expect(resources["Deployment/prod/web-app"]).toMatchObject({
113
+ type: "K8s::Apps::Deployment",
114
+ attributes: expect.objectContaining({ channel: "manifest", release: "web-app" }),
115
+ });
116
+ expect(resources["Service/elsewhere/web-app"]).toBeDefined();
117
+ expect(resources["cluster:CustomResourceDefinition/widgets.example.com"]).toBeDefined();
118
+
119
+ // Hooks channel: excluded from `helm get manifest`, so this row only
120
+ // exists because both channels were read.
121
+ expect(resources["Job/prod/web-app-migrate"]).toMatchObject({
122
+ type: "K8s::Batch::Job",
123
+ attributes: expect.objectContaining({
124
+ channel: "hooks",
125
+ hook: "pre-install,pre-upgrade",
126
+ hookWeight: "1",
127
+ hookDeletePolicy: "hook-succeeded",
128
+ }),
129
+ });
130
+
131
+ expect(unobserved).toEqual({});
132
+ });
133
+
134
+ test("every rendered resource is owned via release identity and a runtime child of its chart (#1077)", async () => {
135
+ const result = await describeResources(baseOptions, scriptedHelm({ list: LIST, manifest: MANIFEST, hooks: HOOKS }));
136
+ const { resources } = normalizeObservation(result);
137
+ for (const key of ["Deployment/prod/web-app", "Service/elsewhere/web-app", "Job/prod/web-app-migrate"]) {
138
+ expect(resources[key].ownership).toBe("owned");
139
+ expect(resources[key].ownerChain).toEqual({ root: "declared", entity: "chart" });
140
+ }
141
+ });
142
+
143
+ test("surfaces chant's stack/env marker verbatim where the rendered labels carry it (#1222)", async () => {
144
+ const result = await describeResources(baseOptions, scriptedHelm({ list: LIST, manifest: MANIFEST, hooks: HOOKS }));
145
+ const { resources } = normalizeObservation(result);
146
+ expect(resources["Deployment/prod/web-app"].marker).toEqual({ stack: "shop", env: "prod" });
147
+ expect(resources["Service/elsewhere/web-app"].marker).toBeUndefined();
148
+ });
149
+
150
+ test("chart-authoring satellites ride the single chart's release verdict", async () => {
151
+ const present = normalizeObservation(
152
+ await describeResources(baseOptions, scriptedHelm({ list: LIST, manifest: MANIFEST, hooks: HOOKS })),
153
+ );
154
+ expect(present.resources.valuesSchema).toMatchObject({
155
+ type: "Helm::Values",
156
+ status: "deployed",
157
+ ownership: "owned",
158
+ });
159
+
160
+ // Release absent → chart AND satellites in neither map (create is right).
161
+ const absent = normalizeObservation(await describeResources(baseOptions, scriptedHelm({ list: "[]" })));
162
+ expect(absent.resources).toEqual({});
163
+ expect(absent.unobserved).toEqual({});
164
+ });
165
+
166
+ test("missing helm binary: every declared entity is unobserved with no-credentials, never a clean empty snapshot", async () => {
167
+ const result = await describeResources(baseOptions, async () => {
168
+ throw Object.assign(new Error("/bin/sh: helm: command not found"), { code: 127 });
169
+ });
170
+ const { resources, unobserved } = normalizeObservation(result);
171
+ expect(resources).toEqual({});
172
+ expect(unobserved.chart.reason).toBe("no-credentials");
173
+ expect(unobserved.valuesSchema.reason).toBe("no-credentials");
174
+ });
175
+
176
+ test("unreachable cluster (kubeconfig): no-credentials with the CLI's own words", async () => {
177
+ const result = await describeResources(baseOptions, async () => {
178
+ throw new Error('Error: Kubernetes cluster unreachable: Get "https://127.0.0.1:6443/version": connect: connection refused');
179
+ });
180
+ const { unobserved } = normalizeObservation(result);
181
+ expect(unobserved.chart.reason).toBe("no-credentials");
182
+ });
183
+
184
+ test("an unreachable release is unobserved with a reason, never absent (#1246)", async () => {
185
+ // `helm list` succeeds, `helm get manifest` fails.
186
+ const result = await describeResources(baseOptions, scriptedHelm({ list: LIST }));
187
+ const { resources, unobserved } = normalizeObservation(result);
188
+ expect(resources.chart).toBeUndefined();
189
+ expect(unobserved.chart.reason).toBe("read-failed");
190
+ // The satellite shares the hole, with the same reason.
191
+ expect(unobserved.valuesSchema.reason).toBe("read-failed");
192
+ });
193
+
194
+ test("a release deployed under another name resolves through its chart identity", async () => {
195
+ const list = JSON.stringify([
196
+ { name: "web-prod", namespace: "prod", status: "deployed", chart: "web-app-0.1.0", revision: "1" },
197
+ ]);
198
+ const { resources } = normalizeObservation(
199
+ await describeResources(baseOptions, scriptedHelm({ list, manifest: MANIFEST, hooks: "" })),
200
+ );
201
+ expect(resources.chart).toMatchObject({ physicalId: "prod/web-prod" });
202
+ });
203
+
204
+ test("the deploy unit (`stack`) names the release for a single-chart project", async () => {
205
+ const list = JSON.stringify([
206
+ { name: "custom-name", namespace: "apps", status: "deployed", chart: "something-else-2.0.0" },
207
+ ]);
208
+ const { resources } = normalizeObservation(
209
+ await describeResources(
210
+ { ...baseOptions, stack: "apps/custom-name" },
211
+ scriptedHelm({ list, manifest: "", hooks: "" }),
212
+ ),
213
+ );
214
+ expect(resources.chart).toMatchObject({ physicalId: "apps/custom-name" });
215
+ });
216
+
217
+ test("the same release name in several namespaces is unobserved, not guessed and not absent", async () => {
218
+ const list = JSON.stringify([
219
+ { name: "web-app", namespace: "a", status: "deployed", chart: "web-app-0.1.0" },
220
+ { name: "web-app", namespace: "b", status: "deployed", chart: "web-app-0.1.0" },
221
+ ]);
222
+ const { resources, unobserved } = normalizeObservation(
223
+ await describeResources(baseOptions, scriptedHelm({ list })),
224
+ );
225
+ expect(resources.chart).toBeUndefined();
226
+ expect(unobserved.chart.reason).toBe("read-failed");
227
+ expect(unobserved.chart.detail).toContain("several namespaces");
228
+ });
229
+
230
+ test("--owned filters nothing and says so in a note — every row is release-scoped", async () => {
231
+ const result = await describeResources(
232
+ { ...baseOptions, owned: true },
233
+ scriptedHelm({ list: LIST, manifest: MANIFEST, hooks: HOOKS }),
234
+ );
235
+ const { resources, notes } = normalizeObservation(result);
236
+ expect(Object.keys(resources).length).toBeGreaterThan(0);
237
+ expect(notes.some((n) => n.includes("--owned"))).toBe(true);
238
+ });
239
+ });
240
+
241
+ describe("helm release document parsing", () => {
242
+ test("tolerates comments, empty documents and non-object documents", () => {
243
+ const rows = parseReleaseDocuments(
244
+ `# just a comment\n---\n---\n"scalar"\n---\napiVersion: v1\nkind: ConfigMap\nmetadata:\n name: cm\n`,
245
+ "manifest",
246
+ "prod",
247
+ );
248
+ expect(rows).toHaveLength(1);
249
+ expect(rows[0]).toMatchObject({ key: "ConfigMap/prod/cm", entityType: "K8s::Core::ConfigMap" });
250
+ });
251
+
252
+ test("unparseable YAML yields no rows rather than throwing", () => {
253
+ expect(parseReleaseDocuments("{{ not yaml", "manifest", "prod")).toEqual([]);
254
+ });
255
+ });
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Live introspection of Helm releases per resource — the helm lexicon's
3
+ * `describeResources` (#1246, epic #1228 phase 5).
4
+ *
5
+ * `listArtifacts` reports release *metadata* (name, revision, status) and no
6
+ * resource state, which is why `lifecycle diff --live` had nothing to compare
7
+ * for a Helm-managed estate. This read goes one level down: for each release
8
+ * a declared `Helm::Chart` deploys, it fetches what the release holds —
9
+ * `helm get manifest` AND `helm get hooks`, both channels, because hooks are
10
+ * excluded from the manifest channel and reading one would report every hook
11
+ * resource as drift — and reports one row per rendered resource.
12
+ *
13
+ * ## Verdicts (#1089)
14
+ *
15
+ * - The chart entity itself is OBSERVED-PRESENT as the release
16
+ * (`Helm::Release`), OBSERVED-ABSENT when `helm list` was asked and did
17
+ * not report it (only that becomes a `create`), and NOT-OBSERVED with a
18
+ * total reason when helm or the cluster could not be reached — a missing
19
+ * binary or kubeconfig is `no-credentials`, never a clean empty snapshot.
20
+ * - Every rendered resource is reported `ownership: "owned"` — helm-managed
21
+ * via release identity: the read is scoped to the release, so everything
22
+ * it returns is by construction managed by a release this project
23
+ * declares. Rows additionally surface chant's own stack/env marker
24
+ * (#1222) verbatim when the rendered labels carry it.
25
+ * - Every rendered resource carries `ownerChain: { root: "declared" }`
26
+ * pointing at its chart entity, so the diff engine classifies it
27
+ * `runtime` rather than `orphan` (#1077) — an unpinned release is not
28
+ * drift. Controller-created children (Pods under a chart's Deployment)
29
+ * are the k8s lexicon's rows; its own owner-chain walk classifies them.
30
+ *
31
+ * ## Chart-authoring satellites
32
+ *
33
+ * `Helm::Values`, `Helm::Notes`, `Helm::Test` and the other authoring
34
+ * entities have no runtime identity of their own — their deployed form IS
35
+ * the release (helm stores the applied values and rendered NOTES.txt in the
36
+ * release record). In a single-chart project they ride the chart's verdict:
37
+ * present when the release is, absent when it is not, unobserved with the
38
+ * same reason otherwise. A multi-chart project cannot attribute them to one
39
+ * release, so they are NOT-OBSERVED (`unsupported-kind`) with a detail
40
+ * saying so.
41
+ */
42
+ import type { ResourceMetadata, UnobservedEntity } from "@intentius/chant/lexicon";
43
+ import type { ObservationResult } from "@intentius/chant/observation";
44
+ import { observation } from "@intentius/chant/observation";
45
+ import { LABEL_OWNERSHIP_KEYS, readOwnership } from "@intentius/chant/ownership";
46
+ import {
47
+ HELM_CHART_ENTITY_TYPE,
48
+ HELM_RELEASE_TYPE,
49
+ defaultHelmRunner,
50
+ observeReleases,
51
+ type HelmRunner,
52
+ type ObservedRelease,
53
+ } from "./release-observe";
54
+
55
+ export interface HelmDescribeOptions {
56
+ environment: string;
57
+ buildOutput: string;
58
+ entityNames: string[];
59
+ entities: Map<string, { entityType: string; props: Record<string, unknown> }>;
60
+ stack?: string;
61
+ region?: string;
62
+ owned?: boolean;
63
+ namespace?: string;
64
+ }
65
+
66
+ function pruneUndefined<T extends Record<string, unknown>>(obj: T): Record<string, unknown> {
67
+ const out: Record<string, unknown> = {};
68
+ for (const [k, v] of Object.entries(obj)) {
69
+ if (v !== undefined) out[k] = v;
70
+ }
71
+ return out;
72
+ }
73
+
74
+ function releaseRow(release: ObservedRelease): ResourceMetadata {
75
+ return {
76
+ type: HELM_RELEASE_TYPE,
77
+ physicalId: `${release.namespace}/${release.release}`,
78
+ status: release.status ?? "unknown",
79
+ ...(release.updated ? { lastUpdated: release.updated } : {}),
80
+ ownership: "owned",
81
+ attributes: pruneUndefined({
82
+ chart: release.chart,
83
+ revision: release.revision,
84
+ appVersion: release.appVersion,
85
+ namespace: release.namespace,
86
+ resources: release.resources.length,
87
+ }),
88
+ };
89
+ }
90
+
91
+ export async function describeResources(
92
+ options: HelmDescribeOptions,
93
+ run: HelmRunner = defaultHelmRunner,
94
+ ): Promise<ObservationResult> {
95
+ const satellites: Array<{ entityName: string; entityType: string }> = [];
96
+ for (const [entityName, entity] of options.entities) {
97
+ if (entity.entityType !== HELM_CHART_ENTITY_TYPE) {
98
+ satellites.push({ entityName, entityType: entity.entityType });
99
+ }
100
+ }
101
+
102
+ const observed = await observeReleases(
103
+ { environment: options.environment, entities: options.entities, stack: options.stack },
104
+ run,
105
+ );
106
+
107
+ const chartCount = options.entities.size - satellites.length;
108
+ const resources: Record<string, ResourceMetadata> = {};
109
+ const unobserved: Record<string, UnobservedEntity> = { ...observed.unobserved };
110
+ const notes: string[] = [];
111
+
112
+ if (chartCount === 0 && satellites.length > 0) {
113
+ for (const { entityName, entityType } of satellites) {
114
+ unobserved[entityName] = {
115
+ type: entityType,
116
+ reason: "unsupported-kind",
117
+ detail: "chart-authoring entity with no declared Helm::Chart to attribute a release to",
118
+ };
119
+ }
120
+ return observation(resources, unobserved, observed.queried, notes);
121
+ }
122
+
123
+ for (const release of observed.releases) {
124
+ resources[release.entityName] = releaseRow(release);
125
+
126
+ for (const row of release.resources) {
127
+ const labels = (row.doc.metadata as { labels?: Record<string, unknown> } | undefined)?.labels;
128
+ const marker = readOwnership(labels, LABEL_OWNERSHIP_KEYS);
129
+ resources[row.key] = {
130
+ type: row.entityType,
131
+ physicalId: row.namespace ? `${row.namespace}/${row.name}` : row.name,
132
+ status: release.status ?? "unknown",
133
+ // Owned via release identity (#1246): the read is scoped to a release
134
+ // this project declares, so every document it returns is helm-managed
135
+ // by construction — the release IS the marker channel here.
136
+ ownership: "owned",
137
+ ...(marker ? { marker } : {}),
138
+ // The release's resources are what the declared chart deploys —
139
+ // expected runtime, never orphan/delete candidates (#1077).
140
+ ownerChain: { root: "declared", entity: release.entityName },
141
+ attributes: pruneUndefined({
142
+ release: release.release,
143
+ releaseNamespace: release.namespace,
144
+ namespace: row.namespace,
145
+ channel: row.channel,
146
+ hook: row.hook?.hook,
147
+ hookWeight: row.hook?.weight,
148
+ hookDeletePolicy: row.hook?.deletePolicy,
149
+ }),
150
+ };
151
+ }
152
+ }
153
+
154
+ // Chart-authoring satellites ride the single chart's verdict; see the
155
+ // module doc. Their deployed form is the release itself, so the release
156
+ // read is the honest answer for them — present, absent, or unobserved
157
+ // exactly as the chart is.
158
+ if (satellites.length > 0) {
159
+ if (chartCount === 1) {
160
+ const [chartEntity] = [...options.entities].find(
161
+ ([, e]) => e.entityType === HELM_CHART_ENTITY_TYPE,
162
+ )!;
163
+ const release = observed.releases.find((r) => r.entityName === chartEntity);
164
+ const hole = unobserved[chartEntity];
165
+ for (const { entityName, entityType } of satellites) {
166
+ if (release) {
167
+ resources[entityName] = {
168
+ type: entityType,
169
+ physicalId: `${release.namespace}/${release.release}`,
170
+ status: release.status ?? "unknown",
171
+ ownership: "owned",
172
+ attributes: { release: release.release, namespace: release.namespace, via: "release-identity" },
173
+ };
174
+ } else if (hole) {
175
+ unobserved[entityName] = { type: entityType, reason: hole.reason, ...(hole.detail ? { detail: hole.detail } : {}) };
176
+ }
177
+ // Release confirmed absent → the satellite is absent too: in neither
178
+ // map, and deploying the chart is what creates it.
179
+ }
180
+ } else {
181
+ for (const { entityName, entityType } of satellites) {
182
+ unobserved[entityName] = {
183
+ type: entityType,
184
+ reason: "unsupported-kind",
185
+ detail: "chart-authoring entity in a multi-chart project — cannot attribute it to a single release",
186
+ };
187
+ }
188
+ }
189
+ }
190
+
191
+ if (options.owned) {
192
+ notes.push(
193
+ "helm scopes reads to declared releases, so every row is helm-managed (owned) by construction — --owned withholds nothing",
194
+ );
195
+ }
196
+
197
+ return observation(resources, unobserved, observed.queried, notes);
198
+ }
package/src/index.ts CHANGED
@@ -10,12 +10,103 @@ export { helmPlugin } from "./plugin";
10
10
  export { helmCapabilityPlugin, HELM_VERB_FAMILIES } from "./components/capability-plugin";
11
11
  export { helmUpgradeCapability, createHelmUpgradeCapability, type HelmUpgradeInput, type HelmUpgradeOutcome } from "./components/helm-upgrade";
12
12
 
13
+ // Typed Op step-builder wrappers (chant #1288 Stage 2) — helmInstall/
14
+ // helmInstallPinned with authoring-time types derived from this lexicon's
15
+ // own HelmInstallArgs (see ./op/builders.ts's module doc for why these live
16
+ // here rather than in core or the temporal barrel). Opt-in:
17
+ // `@intentius/chant-lexicon-temporal`'s same-named exports are core's
18
+ // original untyped builders, unchanged, for cloud-agnostic authoring.
19
+ export { helmInstall, helmInstallPinned } from "./op/builders";
20
+
13
21
  // Resources
14
22
  export { Chart, Values, ValuesOverride, HelmTest, HelmNotes, HelmHook, HelmDependency, HelmMaintainer, HelmCRD } from "./resources";
15
23
 
16
24
  // HelmRender — render an upstream chart at chant build time
17
- export { HelmRender } from "./render";
18
- export type { HelmRenderProps } from "./render";
25
+ export { HelmRender, getHelmRenderRecords, clearHelmRenderRecords } from "./render";
26
+ export type { HelmRenderProps, HelmRenderRecord } from "./render";
27
+
28
+ // #1237 — render canonicalization + the contentDigest/inputDigest split.
29
+ export { canonicalizeRender, helmContentDigest, helmInputDigest, renderStability } from "./render-digest";
30
+ export type { HelmInputDigestSource, RenderStabilityGroup, RenderStabilityReport } from "./render-digest";
31
+
32
+ // #1238 — RenderManifest + content-addressed render storage.
33
+ export {
34
+ helmValuesDigest,
35
+ indexRenderDocuments,
36
+ renderCacheKey,
37
+ renderStoreRoot,
38
+ persistHelmRender,
39
+ loadRenderManifest,
40
+ loadRenderContent,
41
+ listRenderManifests,
42
+ findRenderByCacheKey,
43
+ readRenderDocument,
44
+ } from "./render-store";
45
+ export type {
46
+ RenderManifest,
47
+ RenderDocumentEntry,
48
+ RenderDocumentRef,
49
+ RenderCacheKeySource,
50
+ RenderInputsIndexEntry,
51
+ PersistHelmRenderInput,
52
+ PersistedHelmRender,
53
+ } from "./render-store";
54
+
55
+ // #1239 — structure-preserving render wrapper: segment-based CRD routing.
56
+ export { routeRender, routeStoredRender } from "./render-wrapper";
57
+ export type {
58
+ RoutedRender,
59
+ RoutedDocument,
60
+ RoutedRenderWarning,
61
+ RoutedRenderWarningCode,
62
+ RouteRenderOptions,
63
+ } from "./render-wrapper";
64
+
65
+ // #1242 — pinned install path: wrapper-chart materialization.
66
+ export { materializeWrapperChart, wrapperChartName } from "./wrapper-chart";
67
+ export type { MaterializedWrapperChart } from "./wrapper-chart";
68
+
69
+ // #1240 — addArchiveHelmRender: fold a pinned render into a build archive.
70
+ export { addArchiveHelmRender, HELM_RENDER_MEDIA_TYPE } from "./archive-render";
71
+ export type { AddArchiveHelmRenderInput, AddArchiveHelmRenderOutput } from "./archive-render";
72
+
73
+ // #1251/#1252 — build-time coalesced-values probe and its provenance products.
74
+ export {
75
+ probeCoalescedValues,
76
+ coalescedValuesDigest,
77
+ computeValueSources,
78
+ findDeadAssignments,
79
+ rootCoalescedValues,
80
+ getValuesProbeRecords,
81
+ recordValuesProbe,
82
+ clearValuesProbeRecords,
83
+ } from "./values-probe";
84
+ export type {
85
+ ValuesProbeOptions,
86
+ CoalescedValuesProbe,
87
+ CoalescedChartValues,
88
+ DisabledDependency,
89
+ SuppliedValuesLayer,
90
+ ValueOrigin,
91
+ ValuesAttributionInput,
92
+ DeadAssignment,
93
+ HelmValuesProbeRecord,
94
+ } from "./values-probe";
95
+
96
+ // Capability profiles — per-cluster render inputs (#1235, epic #1228)
97
+ export {
98
+ helmConfigSchema,
99
+ helmCapabilityProfileSchema,
100
+ resolveCapabilityProfile,
101
+ validateCapabilityProfile,
102
+ KUBE_VERSION_PATTERN,
103
+ } from "./config";
104
+ export type {
105
+ HelmChantConfig,
106
+ HelmCapabilityProfile,
107
+ HelmCapabilityProfileConfig,
108
+ HelmCapabilityProfileRef,
109
+ } from "./config";
19
110
 
20
111
  // Intrinsics
21
112
  export {
@@ -113,6 +204,19 @@ export { HelmGenerator } from "./import/generator";
113
204
  export { stripTemplateExpressions, classifyExpression } from "./import/template-stripper";
114
205
  export type { StrippedExpression, StripResult, ExpressionKind } from "./import/template-stripper";
115
206
 
207
+ // Pinnability gate (#1234, epic #1228) — what the pinned-render pipeline
208
+ // consults before anything is pinned
209
+ export { classifyChart } from "./pinnability";
210
+ export type {
211
+ PinnabilityVerdict,
212
+ PinnabilityReport,
213
+ ClassifyChartOptions,
214
+ CapabilityRequirement,
215
+ ClosedInput,
216
+ ConditionalHazard,
217
+ RenderEvidence,
218
+ } from "./pinnability";
219
+
116
220
  // LSP providers
117
221
  export { helmCompletions } from "./lsp/completions";
118
222
  export { helmHover } from "./lsp/hover";