@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,205 @@
1
+ /**
2
+ * Chart model and value coalescing for the pinnability classifier (#1234,
3
+ * epic #1228).
4
+ *
5
+ * Pinnability is a property of `(chart, values)`, not of the chart alone —
6
+ * so the classifier needs each subchart instance's EFFECTIVE values to ask
7
+ * whether a gate is open. This module walks `Chart.yaml` dependencies
8
+ * (aliases and `condition:` included — finding 12: dependency conditions are
9
+ * values-driven and therefore closed inputs) and approximates helm's
10
+ * coalescing: child defaults, overridden by the parent's `<alias-or-name>`
11
+ * subtree, with `global` propagated down.
12
+ *
13
+ * The approximation is deliberate and bounded. `import-values`, `tags`, and
14
+ * `tpl`-computed values are not modeled; a gate they would decide resolves
15
+ * as UNKNOWN and the classifier refuses rather than pins (see
16
+ * conditions.ts). The build-time values probe (#1251) replaces this with
17
+ * helm's own coalescing.
18
+ */
19
+
20
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
21
+ import { join } from "node:path";
22
+ import yaml from "js-yaml";
23
+
24
+ export interface ChartDependency {
25
+ name: string;
26
+ alias?: string;
27
+ /** Comma-separated values paths, evaluated against the ROOT values. */
28
+ condition?: string;
29
+ }
30
+
31
+ export interface ChartMeta {
32
+ name: string;
33
+ dependencies: ChartDependency[];
34
+ }
35
+
36
+ export function readChartMeta(chartDir: string): ChartMeta {
37
+ const raw = readYamlFile(join(chartDir, "Chart.yaml")) as
38
+ | { name?: unknown; dependencies?: unknown }
39
+ | undefined;
40
+ const deps: ChartDependency[] = [];
41
+ if (Array.isArray(raw?.dependencies)) {
42
+ for (const d of raw.dependencies) {
43
+ if (d === null || typeof d !== "object") continue;
44
+ const dep = d as Record<string, unknown>;
45
+ if (typeof dep.name !== "string") continue;
46
+ deps.push({
47
+ name: dep.name,
48
+ alias: typeof dep.alias === "string" ? dep.alias : undefined,
49
+ condition: typeof dep.condition === "string" ? dep.condition : undefined,
50
+ });
51
+ }
52
+ }
53
+ return {
54
+ name: typeof raw?.name === "string" ? raw.name : "",
55
+ dependencies: deps,
56
+ };
57
+ }
58
+
59
+ export function readYamlFile(path: string): unknown {
60
+ if (!existsSync(path)) return undefined;
61
+ return yaml.load(readFileSync(path, "utf8"));
62
+ }
63
+
64
+ function isPlainObject(v: unknown): v is Record<string, unknown> {
65
+ return v !== null && typeof v === "object" && !Array.isArray(v);
66
+ }
67
+
68
+ /**
69
+ * Helm-style deep merge: `override` wins, maps merge recursively, a `null`
70
+ * override deletes the key, arrays replace wholesale.
71
+ */
72
+ export function mergeValues(base: unknown, override: unknown): unknown {
73
+ if (override === undefined) return base;
74
+ if (!isPlainObject(base) || !isPlainObject(override)) return override;
75
+ const out: Record<string, unknown> = { ...base };
76
+ for (const [k, v] of Object.entries(override)) {
77
+ if (v === null) {
78
+ delete out[k];
79
+ continue;
80
+ }
81
+ out[k] = k in out ? mergeValues(out[k], v) : v;
82
+ }
83
+ return out;
84
+ }
85
+
86
+ /** Resolve a dot path against a values tree; undefined when absent. */
87
+ export function valueAtPath(values: unknown, path: string[]): unknown {
88
+ let v = values;
89
+ for (const seg of path) {
90
+ if (!isPlainObject(v)) return undefined;
91
+ v = v[seg];
92
+ }
93
+ return v;
94
+ }
95
+
96
+ /**
97
+ * One chart in the tree, with the values IT sees. An aliased dependency
98
+ * yields two instances over the same directory — each with its own value
99
+ * scope, which is why findings are evaluated per instance but reported per
100
+ * file.
101
+ */
102
+ export interface ChartInstance {
103
+ /** Directory of the (sub)chart. */
104
+ dir: string;
105
+ /** Directory relative to the root chart ("" for the root). */
106
+ relDir: string;
107
+ /** Values scope from the root, e.g. ["grafana"] or ["kidtwo"]; [] for root. */
108
+ scope: readonly string[];
109
+ /** Effective values as this instance sees them. */
110
+ values: unknown;
111
+ /**
112
+ * When a `condition:` on the dependency chain disables this instance under
113
+ * the supplied values: the first condition path that resolved false, in
114
+ * root coordinates. Constructs inside a disabled instance are gated by it.
115
+ */
116
+ disabledBy?: string;
117
+ }
118
+
119
+ /**
120
+ * Build the instance tree for a chart directory and the values the render
121
+ * was given. `suppliedValues` merges over the root chart's `values.yaml`
122
+ * exactly like `helm template --values`.
123
+ */
124
+ export function buildChartInstances(
125
+ chartDir: string,
126
+ suppliedValues?: unknown,
127
+ ): { instances: ChartInstance[]; warnings: string[] } {
128
+ const warnings: string[] = [];
129
+ const rootDefaults = readYamlFile(join(chartDir, "values.yaml")) ?? {};
130
+ const rootValues = mergeValues(rootDefaults, suppliedValues);
131
+ const instances: ChartInstance[] = [];
132
+
133
+ const visit = (
134
+ dir: string,
135
+ relDir: string,
136
+ scope: readonly string[],
137
+ values: unknown,
138
+ disabledBy: string | undefined,
139
+ ): void => {
140
+ instances.push({ dir, relDir, scope, values, disabledBy });
141
+ const meta = readChartMeta(dir);
142
+ const chartsDir = join(dir, "charts");
143
+ const childDirs = existsSync(chartsDir)
144
+ ? readdirSync(chartsDir).filter((e) => {
145
+ const p = join(chartsDir, e);
146
+ return statSync(p).isDirectory() && existsSync(join(p, "Chart.yaml"));
147
+ })
148
+ : [];
149
+ const declared = new Set<string>();
150
+
151
+ const visitChild = (dep: ChartDependency): void => {
152
+ const key = dep.alias ?? dep.name;
153
+ const childDir = join(chartsDir, dep.name);
154
+ if (!existsSync(join(childDir, "Chart.yaml"))) {
155
+ warnings.push(
156
+ `dependency ${dep.name} of ${relDir === "" ? "the chart" : relDir} is not vendored under charts/ — not scanned`,
157
+ );
158
+ return;
159
+ }
160
+ declared.add(dep.name);
161
+ // Condition paths are resolved against the TOP parent's values
162
+ // (helm's documented semantics); first present path wins, absent
163
+ // conditions leave the dependency enabled.
164
+ let childDisabledBy = disabledBy;
165
+ if (childDisabledBy === undefined && dep.condition !== undefined) {
166
+ for (const rawPath of dep.condition.split(",")) {
167
+ const path = rawPath.trim();
168
+ if (path === "") continue;
169
+ const v = valueAtPath(rootValues, path.split("."));
170
+ if (v === undefined) continue;
171
+ if (v === false) childDisabledBy = path;
172
+ break;
173
+ }
174
+ }
175
+ const childDefaults = readYamlFile(join(childDir, "values.yaml")) ?? {};
176
+ const parentSlice = isPlainObject(values) ? values[key] : undefined;
177
+ let childValues = mergeValues(childDefaults, parentSlice);
178
+ // Globals propagate into every subchart tree.
179
+ const parentGlobal = isPlainObject(values) ? values.global : undefined;
180
+ if (parentGlobal !== undefined && isPlainObject(childValues)) {
181
+ childValues = {
182
+ ...childValues,
183
+ global: mergeValues(childValues.global, parentGlobal),
184
+ };
185
+ }
186
+ visit(
187
+ childDir,
188
+ relDir === "" ? `charts/${dep.name}` : `${relDir}/charts/${dep.name}`,
189
+ [...scope, key],
190
+ childValues,
191
+ childDisabledBy,
192
+ );
193
+ };
194
+
195
+ for (const dep of meta.dependencies) visitChild(dep);
196
+ // charts/ directories not declared in Chart.yaml are implicit,
197
+ // always-enabled dependencies.
198
+ for (const name of childDirs) {
199
+ if (!declared.has(name)) visitChild({ name });
200
+ }
201
+ };
202
+
203
+ visit(chartDir, "", [], rootValues, undefined);
204
+ return { instances, warnings };
205
+ }
@@ -38,12 +38,14 @@ describe("helmPlugin", () => {
38
38
 
39
39
  test("provides post-synth checks", () => {
40
40
  const checks = helmPlugin.postSynthChecks!();
41
- expect(checks.length).toBe(21);
41
+ expect(checks.length).toBe(23);
42
42
  const ids = checks.map((c) => c.id);
43
43
  expect(ids).toContain("WHM005");
44
44
  expect(ids).toContain("WHM101");
45
45
  expect(ids).toContain("WHM105");
46
46
  expect(ids).toContain("WHM301");
47
+ expect(ids).toContain("WHM503");
48
+ expect(ids).toContain("WHM504");
47
49
  });
48
50
 
49
51
  test("detectTemplate identifies Chart.yaml data", () => {
package/src/plugin.ts CHANGED
@@ -6,6 +6,8 @@
6
6
  */
7
7
 
8
8
  import type { LexiconPlugin, IntrinsicDef, InitTemplateSet } from "@intentius/chant/lexicon";
9
+ import type { CommandGroup } from "@intentius/chant/cli/command-group";
10
+ import { helmCommandGroup } from "./commands";
9
11
  import { detectTemplate } from "./detect";
10
12
  import { discoverLintRules } from "@intentius/chant/lint/discover";
11
13
  import { postSynthChecks as postSynthCheckList } from "./lint/post-synth";
@@ -16,12 +18,35 @@ import { fileURLToPath } from "url";
16
18
  import { helmSerializer } from "./serializer";
17
19
  import { helmCompletions } from "./lsp/completions";
18
20
  import { helmHover } from "./lsp/hover";
21
+ import { helmConfigSchema } from "./config";
22
+ import { helmDeepNormalizationHooks } from "./deep-observe-hooks";
23
+ import { LABEL_OWNERSHIP_KEYS } from "@intentius/chant/ownership";
19
24
 
20
25
  export const helmPlugin: LexiconPlugin = {
21
26
  name: "helm",
27
+ // The `helm` namespace in chant.config.ts — capability profiles (#1235).
28
+ // Declaring the schema makes core validate the namespace at load, so a
29
+ // typo'd profile field fails the build instead of silently unpinning.
30
+ configSchema: helmConfigSchema,
31
+ // #1246 — helm resolves real ownership verdicts on the thin read (every row
32
+ // is release-scoped, so helm-managed = owned via release identity), and the
33
+ // deep read (#1247) delegates to the k8s reader, which resolves the shared
34
+ // label channel (`app.kubernetes.io/managed-by` + `chant.intentius.io/*`)
35
+ // on the live objects.
36
+ ownershipChannel: { keys: LABEL_OWNERSHIP_KEYS, reads: ["describeResources", "observeResourcesDeep"] },
22
37
  auditCatalog: () => helmAuditCatalog,
23
38
  serializer: helmSerializer,
24
39
 
40
+ // #1248 (epic #1228 Phase 6, via the #1078 seam) — `chant helm` mounts the
41
+ // pinned-render surface: the pinnability gate (#1234), the double-render
42
+ // localizer (#1236), and the recorded-render listing with digests (#1237).
43
+ // See ./commands.ts for the verb list; this plugin only mounts it. The
44
+ // diff verbs (#1249 render-to-render, #1250 render-to-live, both mounted on
45
+ // "diff") land there.
46
+ commands(): CommandGroup {
47
+ return helmCommandGroup();
48
+ },
49
+
25
50
  lintRules() {
26
51
  const rulesDir = join(dirname(fileURLToPath(import.meta.url)), "lint", "rules");
27
52
  return discoverLintRules(rulesDir, import.meta.url);
@@ -37,25 +62,63 @@ export const helmPlugin: LexiconPlugin = {
37
62
  completionProvider: helmCompletions,
38
63
  hoverProvider: helmHover,
39
64
 
65
+ // chant #1174 — `foldsAsCall: true` opts an intrinsic's PLAIN-CALL form
66
+ // into `chant build --fold` (chant #1044's mechanism, previously unused by
67
+ // this lexicon: every helm intrinsic is a plain call — helm has no tagged
68
+ // templates at all — so before this PR NONE of them folded, and a single
69
+ // `include(...)`/`printf(...)` anywhere in a chart's resources forced the
70
+ // whole file back to the run path).
71
+ //
72
+ // Audited one at a time against `IntrinsicDef.foldsAsCall`'s criterion
73
+ // (../../../packages/core/src/lexicon.ts): "calling the intrinsic is a
74
+ // pure function of its arguments that builds a deterministic data
75
+ // envelope". Every function below (`include`/`required`/`helmDefault`/
76
+ // `toYaml`/`quote`/`printf`/`tpl`/`lookup`/`filesGet`/`filesGlob`/
77
+ // `filesAsConfig`/`filesAsSecrets`, ./intrinsics.ts) does exactly that: it
78
+ // builds a `HelmTpl` (or, for `lookup`, a string) by interpolating its
79
+ // arguments into a Go template expression string — no I/O, no mutation, no
80
+ // dependency on anything but the arguments. `lookup`'s own side effect
81
+ // (a live cluster read) happens when HELM renders the resulting template
82
+ // at deploy time, not when this JS function runs at `chant build` time, so
83
+ // it is just as pure here as the rest.
84
+ //
85
+ // Left OUT, each for a specific reason rather than an oversight:
86
+ // - `values`/`Release`/`ChartRef`/`Capabilities`/`Template` are proxy
87
+ // OBJECTS, never called (`values(...)` isn't valid source) — nothing
88
+ // to opt in.
89
+ // - `withOrder`/`argoWave` return a PLAIN record meant to be spread
90
+ // (`{...withOrder(1)}`), not a `HelmTpl`. `fold()`'s object-spread
91
+ // handling (`Object.assign(obj, src)`, ../fold/fold.ts) runs
92
+ // SYNCHRONOUSLY on the immediately-folded value, before the async
93
+ // revival that would turn a folded call into its real result — opting
94
+ // these in would spread the folded `{__intrinsic, args}` envelope's
95
+ // own keys into the object instead of the real record. A real
96
+ // soundness hazard, not a "not yet audited."
97
+ // - `If`/`ElseIf`/`Range`/`With` take an arbitrary `body`/`elseBody`
98
+ // that is often an arrow function (`Range(list, (item) => ({...}))`)
99
+ // or a nested Declarable — `fold()` already rejects a function used as
100
+ // a value on its own terms, so opting these in buys nothing measured
101
+ // on this corpus and is left for a future PR to size against real
102
+ // usage rather than widened on speculation.
40
103
  intrinsics(): IntrinsicDef[] {
41
104
  return [
42
105
  { name: "values", description: "Proxy accessor for {{ .Values.x }} references", isTag: false },
43
106
  { name: "Release", description: "Built-in Release object (Name, Namespace, etc.)", isTag: false },
44
107
  { name: "ChartRef", description: "Built-in Chart object (Name, Version, AppVersion)", isTag: false },
45
- { name: "include", description: 'Include a named template: {{ include "name" . }}', isTag: false },
46
- { name: "required", description: 'Require a value: {{ required "msg" .Values.x }}', isTag: false },
47
- { name: "helmDefault", description: 'Default value: {{ default "def" .Values.x }}', isTag: false },
48
- { name: "toYaml", description: "Convert to YAML: {{ toYaml .Values.x | nindent N }}", isTag: false },
49
- { name: "quote", description: "Quote a value: {{ .Values.x | quote }}", isTag: false },
50
- { name: "printf", description: 'Format string: {{ printf "%s" .Values.x }}', isTag: false },
51
- { name: "tpl", description: "Evaluate template: {{ tpl .Values.x . }}", isTag: false },
52
- { name: "lookup", description: 'Lookup resource: {{ lookup "v1" "Secret" "ns" "name" }}', isTag: false },
108
+ { name: "include", description: 'Include a named template: {{ include "name" . }}', isTag: false, foldsAsCall: true },
109
+ { name: "required", description: 'Require a value: {{ required "msg" .Values.x }}', isTag: false, foldsAsCall: true },
110
+ { name: "helmDefault", description: 'Default value: {{ default "def" .Values.x }}', isTag: false, foldsAsCall: true },
111
+ { name: "toYaml", description: "Convert to YAML: {{ toYaml .Values.x | nindent N }}", isTag: false, foldsAsCall: true },
112
+ { name: "quote", description: "Quote a value: {{ .Values.x | quote }}", isTag: false, foldsAsCall: true },
113
+ { name: "printf", description: 'Format string: {{ printf "%s" .Values.x }}', isTag: false, foldsAsCall: true },
114
+ { name: "tpl", description: "Evaluate template: {{ tpl .Values.x . }}", isTag: false, foldsAsCall: true },
115
+ { name: "lookup", description: 'Lookup resource: {{ lookup "v1" "Secret" "ns" "name" }}', isTag: false, foldsAsCall: true },
53
116
  { name: "Capabilities", description: "Built-in Capabilities object (KubeVersion, APIVersions, HelmVersion)", isTag: false },
54
117
  { name: "Template", description: "Built-in Template object (Name, BasePath)", isTag: false },
55
- { name: "filesGet", description: '.Files.Get: {{ .Files.Get "path" }}', isTag: false },
56
- { name: "filesGlob", description: '.Files.Glob: {{ .Files.Glob "pattern" }}', isTag: false },
57
- { name: "filesAsConfig", description: ".Files.Glob.AsConfig for ConfigMap data", isTag: false },
58
- { name: "filesAsSecrets", description: ".Files.Glob.AsSecrets for Secret data", isTag: false },
118
+ { name: "filesGet", description: '.Files.Get: {{ .Files.Get "path" }}', isTag: false, foldsAsCall: true },
119
+ { name: "filesGlob", description: '.Files.Glob: {{ .Files.Glob "pattern" }}', isTag: false, foldsAsCall: true },
120
+ { name: "filesAsConfig", description: ".Files.Glob.AsConfig for ConfigMap data", isTag: false, foldsAsCall: true },
121
+ { name: "filesAsSecrets", description: ".Files.Glob.AsSecrets for Secret data", isTag: false, foldsAsCall: true },
59
122
  { name: "ElseIf", description: "Else-if chaining: {{- else if .Values.x }}...{{- end }}", isTag: false },
60
123
  { name: "If", description: "Conditional: {{- if .Values.x }}...{{- end }}", isTag: false },
61
124
  { name: "Range", description: "Range loop: {{- range .Values.x }}...{{- end }}", isTag: false },
@@ -383,6 +446,28 @@ export const service = new Service({
383
446
  return listArtifacts(options);
384
447
  },
385
448
 
449
+ // #1246 — per-resource observation of what each declared chart's release
450
+ // holds (`helm get manifest` + `helm get hooks`, both channels), with total
451
+ // tri-state verdicts. This is what gives `lifecycle diff --live` helm rows.
452
+ async describeResources(options) {
453
+ const { describeResources } = await import("./describe-resources");
454
+ return describeResources(options);
455
+ },
456
+
457
+ // #1247 — deep property-level trees for the release's resources, read live
458
+ // through the k8s lexicon's typed client and normalized by its hooks.
459
+ // Dynamic import for the same reason k8s's is: the reader reaches
460
+ // `@intentius/chant-k8s-client`, which must stay off the build path.
461
+ async observeResourcesDeep(options) {
462
+ const { observeResourcesDeepHelm } = await import("./deep-observe");
463
+ return observeResourcesDeepHelm(options);
464
+ },
465
+
466
+ // The k8s lexicon's hooks by reference, not a copy (#1247) — the objects a
467
+ // release deploys are Kubernetes objects, and core normalizes the declared
468
+ // tree with the same rules the reader used.
469
+ deepNormalizationHooks: helmDeepNormalizationHooks,
470
+
386
471
  // #1495 piece 4 — the deploy-unit observer for helm-upgrade units: a
387
472
  // release's presence and native status, read back by the same name the
388
473
  // capability deploys under. Dynamic import mirrors listArtifacts.