@intentius/chant-lexicon-helm 0.124.31 → 0.124.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/dist/archive-render.js +76 -0
  2. package/dist/codegen/docs-cli.js +3 -0
  3. package/dist/codegen/docs.js +113 -0
  4. package/dist/codegen/generate-cli.js +19 -0
  5. package/dist/codegen/generate.js +210 -0
  6. package/dist/codegen/naming.js +29 -0
  7. package/dist/codegen/package.js +44 -0
  8. package/dist/codegen/values-type.js +87 -0
  9. package/dist/commands.js +413 -0
  10. package/dist/components/builders.js +9 -0
  11. package/dist/components/capability-plugin.js +17 -0
  12. package/dist/components/helm-upgrade.js +109 -0
  13. package/dist/components/index.js +11 -0
  14. package/dist/composites/catalog.js +1119 -0
  15. package/dist/composites/helm-batch-job.js +148 -0
  16. package/dist/composites/helm-crd-lifecycle.js +145 -0
  17. package/dist/composites/helm-cron-job.js +130 -0
  18. package/dist/composites/helm-daemon-set.js +125 -0
  19. package/dist/composites/helm-external-secret.js +63 -0
  20. package/dist/composites/helm-library.js +24 -0
  21. package/dist/composites/helm-microservice.js +260 -0
  22. package/dist/composites/helm-monitored-service.js +188 -0
  23. package/dist/composites/helm-namespace-env.js +116 -0
  24. package/dist/composites/helm-secure-ingress.js +86 -0
  25. package/dist/composites/helm-stateful-service.js +160 -0
  26. package/dist/composites/helm-web-app.js +201 -0
  27. package/dist/composites/helm-worker.js +164 -0
  28. package/dist/composites/index.js +13 -0
  29. package/dist/config.js +161 -0
  30. package/dist/coverage.js +37 -0
  31. package/dist/deep-observe-hooks.js +19 -0
  32. package/dist/deep-observe.js +55 -0
  33. package/dist/describe-resources.js +119 -0
  34. package/dist/describe-stack-status.js +66 -0
  35. package/dist/detect.js +11 -0
  36. package/dist/generated/index.js +11 -0
  37. package/dist/helpers.js +84 -0
  38. package/dist/import/generator.js +292 -0
  39. package/dist/import/parser.js +141 -0
  40. package/dist/import/template-stripper.js +86 -0
  41. package/dist/index.js +55 -0
  42. package/dist/integrity.json +2 -2
  43. package/dist/intrinsics.js +482 -0
  44. package/dist/lint/audit-catalog.js +33 -0
  45. package/dist/lint/audit-lineage.js +53 -0
  46. package/dist/lint/post-synth/helm-helpers.js +60 -0
  47. package/dist/lint/post-synth/index.js +48 -0
  48. package/dist/lint/post-synth/whm005-no-empty-wrapper.js +47 -0
  49. package/dist/lint/post-synth/whm101.js +38 -0
  50. package/dist/lint/post-synth/whm102.js +26 -0
  51. package/dist/lint/post-synth/whm103.js +51 -0
  52. package/dist/lint/post-synth/whm104.js +29 -0
  53. package/dist/lint/post-synth/whm105.js +23 -0
  54. package/dist/lint/post-synth/whm201.js +29 -0
  55. package/dist/lint/post-synth/whm202.js +42 -0
  56. package/dist/lint/post-synth/whm203.js +32 -0
  57. package/dist/lint/post-synth/whm204.js +51 -0
  58. package/dist/lint/post-synth/whm301.js +31 -0
  59. package/dist/lint/post-synth/whm302.js +33 -0
  60. package/dist/lint/post-synth/whm401.js +52 -0
  61. package/dist/lint/post-synth/whm402.js +38 -0
  62. package/dist/lint/post-synth/whm403.js +38 -0
  63. package/dist/lint/post-synth/whm404.js +30 -0
  64. package/dist/lint/post-synth/whm405.js +47 -0
  65. package/dist/lint/post-synth/whm406.js +27 -0
  66. package/dist/lint/post-synth/whm407.js +72 -0
  67. package/dist/lint/post-synth/whm501.js +89 -0
  68. package/dist/lint/post-synth/whm502.js +85 -0
  69. package/dist/lint/post-synth/whm503.js +82 -0
  70. package/dist/lint/post-synth/whm504.js +47 -0
  71. package/dist/lint/rules/chart-metadata.js +53 -0
  72. package/dist/lint/rules/no-hardcoded-image.js +50 -0
  73. package/dist/lint/rules/values-no-helm-tpl.js +78 -0
  74. package/dist/lint/rules/values-no-secrets.js +65 -0
  75. package/dist/list-artifacts.js +134 -0
  76. package/dist/lsp/completions.d.ts.map +1 -1
  77. package/dist/lsp/completions.js +18 -0
  78. package/dist/lsp/hover.d.ts.map +1 -1
  79. package/dist/lsp/hover.js +40 -0
  80. package/dist/manifest.json +1 -1
  81. package/dist/op/activities/cluster-probe.js +116 -0
  82. package/dist/op/activities/helm.js +449 -0
  83. package/dist/op/activities/index.js +10 -0
  84. package/dist/op/builders.js +45 -0
  85. package/dist/package-cli.js +21 -0
  86. package/dist/pinnability/actions.js +207 -0
  87. package/dist/pinnability/classify.js +416 -0
  88. package/dist/pinnability/conditions.js +0 -0
  89. package/dist/pinnability/index.js +16 -0
  90. package/dist/pinnability/localize.js +518 -0
  91. package/dist/pinnability/render-stream.js +53 -0
  92. package/dist/pinnability/values.js +151 -0
  93. package/dist/plugin.js +458 -0
  94. package/dist/project-codegen.js +215 -0
  95. package/dist/release-observe.js +287 -0
  96. package/dist/render-diff.js +279 -0
  97. package/dist/render-digest.js +0 -0
  98. package/dist/render-records.js +29 -0
  99. package/dist/render-store.js +401 -0
  100. package/dist/render-wrapper.js +164 -0
  101. package/dist/render.js +394 -0
  102. package/dist/resources.js +109 -0
  103. package/dist/serializer.js +903 -0
  104. package/dist/validate-cli.js +17 -0
  105. package/dist/validate.js +30 -0
  106. package/dist/values-probe.js +504 -0
  107. package/dist/wrapper-chart.js +95 -0
  108. package/package.json +11 -11
  109. package/src/codegen/package.ts +2 -2
  110. package/src/lsp/completions.ts +2 -1
  111. package/src/lsp/hover.ts +2 -1
@@ -0,0 +1,151 @@
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
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
20
+ import { join } from "node:path";
21
+ import yaml from "js-yaml";
22
+ export function readChartMeta(chartDir) {
23
+ const raw = readYamlFile(join(chartDir, "Chart.yaml"));
24
+ const deps = [];
25
+ if (Array.isArray(raw?.dependencies)) {
26
+ for (const d of raw.dependencies) {
27
+ if (d === null || typeof d !== "object")
28
+ continue;
29
+ const dep = d;
30
+ if (typeof dep.name !== "string")
31
+ continue;
32
+ deps.push({
33
+ name: dep.name,
34
+ alias: typeof dep.alias === "string" ? dep.alias : undefined,
35
+ condition: typeof dep.condition === "string" ? dep.condition : undefined,
36
+ });
37
+ }
38
+ }
39
+ return {
40
+ name: typeof raw?.name === "string" ? raw.name : "",
41
+ dependencies: deps,
42
+ };
43
+ }
44
+ export function readYamlFile(path) {
45
+ if (!existsSync(path))
46
+ return undefined;
47
+ return yaml.load(readFileSync(path, "utf8"));
48
+ }
49
+ function isPlainObject(v) {
50
+ return v !== null && typeof v === "object" && !Array.isArray(v);
51
+ }
52
+ /**
53
+ * Helm-style deep merge: `override` wins, maps merge recursively, a `null`
54
+ * override deletes the key, arrays replace wholesale.
55
+ */
56
+ export function mergeValues(base, override) {
57
+ if (override === undefined)
58
+ return base;
59
+ if (!isPlainObject(base) || !isPlainObject(override))
60
+ return override;
61
+ const out = { ...base };
62
+ for (const [k, v] of Object.entries(override)) {
63
+ if (v === null) {
64
+ delete out[k];
65
+ continue;
66
+ }
67
+ out[k] = k in out ? mergeValues(out[k], v) : v;
68
+ }
69
+ return out;
70
+ }
71
+ /** Resolve a dot path against a values tree; undefined when absent. */
72
+ export function valueAtPath(values, path) {
73
+ let v = values;
74
+ for (const seg of path) {
75
+ if (!isPlainObject(v))
76
+ return undefined;
77
+ v = v[seg];
78
+ }
79
+ return v;
80
+ }
81
+ /**
82
+ * Build the instance tree for a chart directory and the values the render
83
+ * was given. `suppliedValues` merges over the root chart's `values.yaml`
84
+ * exactly like `helm template --values`.
85
+ */
86
+ export function buildChartInstances(chartDir, suppliedValues) {
87
+ const warnings = [];
88
+ const rootDefaults = readYamlFile(join(chartDir, "values.yaml")) ?? {};
89
+ const rootValues = mergeValues(rootDefaults, suppliedValues);
90
+ const instances = [];
91
+ const visit = (dir, relDir, scope, values, disabledBy) => {
92
+ instances.push({ dir, relDir, scope, values, disabledBy });
93
+ const meta = readChartMeta(dir);
94
+ const chartsDir = join(dir, "charts");
95
+ const childDirs = existsSync(chartsDir)
96
+ ? readdirSync(chartsDir).filter((e) => {
97
+ const p = join(chartsDir, e);
98
+ return statSync(p).isDirectory() && existsSync(join(p, "Chart.yaml"));
99
+ })
100
+ : [];
101
+ const declared = new Set();
102
+ const visitChild = (dep) => {
103
+ const key = dep.alias ?? dep.name;
104
+ const childDir = join(chartsDir, dep.name);
105
+ if (!existsSync(join(childDir, "Chart.yaml"))) {
106
+ warnings.push(`dependency ${dep.name} of ${relDir === "" ? "the chart" : relDir} is not vendored under charts/ — not scanned`);
107
+ return;
108
+ }
109
+ declared.add(dep.name);
110
+ // Condition paths are resolved against the TOP parent's values
111
+ // (helm's documented semantics); first present path wins, absent
112
+ // conditions leave the dependency enabled.
113
+ let childDisabledBy = disabledBy;
114
+ if (childDisabledBy === undefined && dep.condition !== undefined) {
115
+ for (const rawPath of dep.condition.split(",")) {
116
+ const path = rawPath.trim();
117
+ if (path === "")
118
+ continue;
119
+ const v = valueAtPath(rootValues, path.split("."));
120
+ if (v === undefined)
121
+ continue;
122
+ if (v === false)
123
+ childDisabledBy = path;
124
+ break;
125
+ }
126
+ }
127
+ const childDefaults = readYamlFile(join(childDir, "values.yaml")) ?? {};
128
+ const parentSlice = isPlainObject(values) ? values[key] : undefined;
129
+ let childValues = mergeValues(childDefaults, parentSlice);
130
+ // Globals propagate into every subchart tree.
131
+ const parentGlobal = isPlainObject(values) ? values.global : undefined;
132
+ if (parentGlobal !== undefined && isPlainObject(childValues)) {
133
+ childValues = {
134
+ ...childValues,
135
+ global: mergeValues(childValues.global, parentGlobal),
136
+ };
137
+ }
138
+ visit(childDir, relDir === "" ? `charts/${dep.name}` : `${relDir}/charts/${dep.name}`, [...scope, key], childValues, childDisabledBy);
139
+ };
140
+ for (const dep of meta.dependencies)
141
+ visitChild(dep);
142
+ // charts/ directories not declared in Chart.yaml are implicit,
143
+ // always-enabled dependencies.
144
+ for (const name of childDirs) {
145
+ if (!declared.has(name))
146
+ visitChild({ name });
147
+ }
148
+ };
149
+ visit(chartDir, "", [], rootValues, undefined);
150
+ return { instances, warnings };
151
+ }
package/dist/plugin.js ADDED
@@ -0,0 +1,458 @@
1
+ /**
2
+ * Helm lexicon plugin.
3
+ *
4
+ * Implements the LexiconPlugin interface with the Helm chart serializer,
5
+ * Helm-specific intrinsics, lint rules, and post-synth checks.
6
+ */
7
+ import { compositeCatalog } from "./composites/catalog.js";
8
+ import { helmCommandGroup } from "./commands.js";
9
+ import { detectTemplate } from "./detect.js";
10
+ import { discoverLintRules } from "@intentius/chant/lint/discover";
11
+ import { postSynthChecks as postSynthCheckList } from "./lint/post-synth/index.js";
12
+ import { helmAuditCatalog } from "./lint/audit-catalog.js";
13
+ import { createSkillsLoader, createDiffTool } from "@intentius/chant/lexicon-plugin-helpers";
14
+ import { join, dirname } from "path";
15
+ import { fileURLToPath } from "url";
16
+ import { helmSerializer } from "./serializer.js";
17
+ import { helmCompletions } from "./lsp/completions.js";
18
+ import { helmHover } from "./lsp/hover.js";
19
+ import { helmConfigSchema } from "./config.js";
20
+ import { helmProjectCodegen } from "./project-codegen.js";
21
+ import { helmDeepNormalizationHooks } from "./deep-observe-hooks.js";
22
+ import { LABEL_OWNERSHIP_KEYS } from "@intentius/chant/ownership";
23
+ export const helmPlugin = {
24
+ name: "helm",
25
+ // The `helm` namespace in chant.config.ts — capability profiles (#1235).
26
+ // Declaring the schema makes core validate the namespace at load, so a
27
+ // typo'd profile field fails the build instead of silently unpinning.
28
+ configSchema: helmConfigSchema,
29
+ // Typed values and a render factory per chart in `helm.charts`, written by
30
+ // `chant generate` and checked for drift by every build. See
31
+ // ./project-codegen.ts.
32
+ projectCodegen: () => helmProjectCodegen(),
33
+ // #1246 — helm resolves real ownership verdicts on the thin read (every row
34
+ // is release-scoped, so helm-managed = owned via release identity), and the
35
+ // deep read (#1247) delegates to the k8s reader, which resolves the shared
36
+ // label channel (`app.kubernetes.io/managed-by` + `chant.intentius.io/*`)
37
+ // on the live objects.
38
+ ownershipChannel: { keys: LABEL_OWNERSHIP_KEYS, reads: ["describeResources", "observeResourcesDeep"] },
39
+ auditCatalog: () => helmAuditCatalog,
40
+ serializer: helmSerializer,
41
+ // #1248 (epic #1228 Phase 6, via the #1078 seam) — `chant helm` mounts the
42
+ // pinned-render surface: the pinnability gate (#1234), the double-render
43
+ // localizer (#1236), and the recorded-render listing with digests (#1237).
44
+ // See ./commands.ts for the verb list; this plugin only mounts it. The
45
+ // diff verbs (#1249 render-to-render, #1250 render-to-live, both mounted on
46
+ // "diff") land there.
47
+ commands() {
48
+ return helmCommandGroup();
49
+ },
50
+ lintRules() {
51
+ const rulesDir = join(dirname(fileURLToPath(import.meta.url)), "lint", "rules");
52
+ return discoverLintRules(rulesDir, import.meta.url);
53
+ },
54
+ postSynthChecks() {
55
+ return postSynthCheckList;
56
+ },
57
+ // The providers themselves have lived in src/lsp/ with tests since the
58
+ // lexicon was written; they were never registered here, so `chant serve lsp`
59
+ // never reached them (#1342).
60
+ completionProvider: helmCompletions,
61
+ hoverProvider: helmHover,
62
+ // chant #1174 — `foldsAsCall: true` opts an intrinsic's PLAIN-CALL form
63
+ // into `chant build --fold` (chant #1044's mechanism, previously unused by
64
+ // this lexicon: every helm intrinsic is a plain call — helm has no tagged
65
+ // templates at all — so before this PR NONE of them folded, and a single
66
+ // `include(...)`/`printf(...)` anywhere in a chart's resources forced the
67
+ // whole file back to the run path).
68
+ //
69
+ // Audited one at a time against `IntrinsicDef.foldsAsCall`'s criterion
70
+ // (../../../packages/core/src/lexicon.ts): "calling the intrinsic is a
71
+ // pure function of its arguments that builds a deterministic data
72
+ // envelope". Every function below (`include`/`required`/`helmDefault`/
73
+ // `toYaml`/`quote`/`printf`/`tpl`/`lookup`/`filesGet`/`filesGlob`/
74
+ // `filesAsConfig`/`filesAsSecrets`, ./intrinsics.ts) does exactly that: it
75
+ // builds a `HelmTpl` (or, for `lookup`, a string) by interpolating its
76
+ // arguments into a Go template expression string — no I/O, no mutation, no
77
+ // dependency on anything but the arguments. `lookup`'s own side effect
78
+ // (a live cluster read) happens when HELM renders the resulting template
79
+ // at deploy time, not when this JS function runs at `chant build` time, so
80
+ // it is just as pure here as the rest.
81
+ //
82
+ // Left OUT, each for a specific reason rather than an oversight:
83
+ // - `values`/`Release`/`ChartRef`/`Capabilities`/`Template` are proxy
84
+ // OBJECTS, never called (`values(...)` isn't valid source) — nothing
85
+ // to opt in.
86
+ // - `withOrder`/`argoWave` return a PLAIN record meant to be spread
87
+ // (`{...withOrder(1)}`), not a `HelmTpl`. `fold()`'s object-spread
88
+ // handling (`Object.assign(obj, src)`, ../fold/fold.ts) runs
89
+ // SYNCHRONOUSLY on the immediately-folded value, before the async
90
+ // revival that would turn a folded call into its real result — opting
91
+ // these in would spread the folded `{__intrinsic, args}` envelope's
92
+ // own keys into the object instead of the real record. A real
93
+ // soundness hazard, not a "not yet audited."
94
+ // - `If`/`ElseIf`/`Range`/`With` take an arbitrary `body`/`elseBody`
95
+ // that is often an arrow function (`Range(list, (item) => ({...}))`)
96
+ // or a nested Declarable — `fold()` already rejects a function used as
97
+ // a value on its own terms, so opting these in buys nothing measured
98
+ // on this corpus and is left for a future PR to size against real
99
+ // usage rather than widened on speculation.
100
+ intrinsics() {
101
+ return [
102
+ { name: "values", description: "Proxy accessor for {{ .Values.x }} references", isTag: false },
103
+ { name: "Release", description: "Built-in Release object (Name, Namespace, etc.)", isTag: false },
104
+ { name: "ChartRef", description: "Built-in Chart object (Name, Version, AppVersion)", isTag: false },
105
+ { name: "include", description: 'Include a named template: {{ include "name" . }}', isTag: false, foldsAsCall: true },
106
+ { name: "required", description: 'Require a value: {{ required "msg" .Values.x }}', isTag: false, foldsAsCall: true },
107
+ { name: "helmDefault", description: 'Default value: {{ default "def" .Values.x }}', isTag: false, foldsAsCall: true },
108
+ { name: "toYaml", description: "Convert to YAML: {{ toYaml .Values.x | nindent N }}", isTag: false, foldsAsCall: true },
109
+ { name: "quote", description: "Quote a value: {{ .Values.x | quote }}", isTag: false, foldsAsCall: true },
110
+ { name: "printf", description: 'Format string: {{ printf "%s" .Values.x }}', isTag: false, foldsAsCall: true },
111
+ { name: "tpl", description: "Evaluate template: {{ tpl .Values.x . }}", isTag: false, foldsAsCall: true },
112
+ { name: "lookup", description: 'Lookup resource: {{ lookup "v1" "Secret" "ns" "name" }}', isTag: false, foldsAsCall: true },
113
+ { name: "Capabilities", description: "Built-in Capabilities object (KubeVersion, APIVersions, HelmVersion)", isTag: false },
114
+ { name: "Template", description: "Built-in Template object (Name, BasePath)", isTag: false },
115
+ { name: "filesGet", description: '.Files.Get: {{ .Files.Get "path" }}', isTag: false, foldsAsCall: true },
116
+ { name: "filesGlob", description: '.Files.Glob: {{ .Files.Glob "pattern" }}', isTag: false, foldsAsCall: true },
117
+ { name: "filesAsConfig", description: ".Files.Glob.AsConfig for ConfigMap data", isTag: false, foldsAsCall: true },
118
+ { name: "filesAsSecrets", description: ".Files.Glob.AsSecrets for Secret data", isTag: false, foldsAsCall: true },
119
+ { name: "ElseIf", description: "Else-if chaining: {{- else if .Values.x }}...{{- end }}", isTag: false },
120
+ { name: "If", description: "Conditional: {{- if .Values.x }}...{{- end }}", isTag: false },
121
+ { name: "Range", description: "Range loop: {{- range .Values.x }}...{{- end }}", isTag: false },
122
+ { name: "With", description: "With scope: {{- with .Values.x }}...{{- end }}", isTag: false },
123
+ { name: "withOrder", description: "Helm hook annotations for resource ordering (pre-install/pre-upgrade)", isTag: false },
124
+ { name: "argoWave", description: "Argo CD sync wave annotation", isTag: false },
125
+ ];
126
+ },
127
+ detectTemplate,
128
+ composites() {
129
+ return compositeCatalog;
130
+ },
131
+ mcpTools() {
132
+ return [createDiffTool(helmSerializer, "Compare current Helm chart build output against previous output", "helm")];
133
+ },
134
+ mcpResources() {
135
+ return [
136
+ {
137
+ uri: "helm:resource-catalog",
138
+ name: "Helm Chart Resource Catalog",
139
+ description: "JSON list of all supported Helm chart resource types",
140
+ mimeType: "application/json",
141
+ async handler() {
142
+ const { readFileSync } = await import("fs");
143
+ const { join, dirname } = await import("path");
144
+ const { fileURLToPath } = await import("url");
145
+ const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
146
+ try {
147
+ const lexicon = JSON.parse(readFileSync(join(pkgDir, "dist", "meta.json"), "utf-8"));
148
+ const entries = Object.entries(lexicon).map(([className, entry]) => ({
149
+ className,
150
+ resourceType: entry.resourceType,
151
+ kind: entry.kind,
152
+ }));
153
+ return JSON.stringify(entries);
154
+ }
155
+ catch {
156
+ return JSON.stringify([{ className: "Chart", kind: "resource" }, { className: "Values", kind: "resource" }, { className: "HelmNotes", kind: "resource" }]);
157
+ }
158
+ },
159
+ },
160
+ {
161
+ uri: "examples/web-app",
162
+ name: "Web App Helm Chart Example",
163
+ description: "A basic web application Helm chart with Deployment, Service, and Ingress",
164
+ mimeType: "text/typescript",
165
+ async handler() {
166
+ return `import { Chart, Values, HelmNotes } from "@intentius/chant-lexicon-helm";
167
+ import { values, Release, include, toYaml, printf } from "@intentius/chant-lexicon-helm/intrinsics";
168
+ import { Deployment, Service, Ingress } from "@intentius/chant-lexicon-k8s";
169
+
170
+ export const chart = new Chart({
171
+ apiVersion: "v2",
172
+ name: "web-app",
173
+ version: "0.1.0",
174
+ appVersion: "1.0.0",
175
+ type: "application",
176
+ description: "A web application chart",
177
+ });
178
+
179
+ export const valuesSchema = new Values({
180
+ replicaCount: 2,
181
+ image: { repository: "nginx", tag: "latest", pullPolicy: "IfNotPresent" },
182
+ service: { type: "ClusterIP", port: 80 },
183
+ ingress: { enabled: false, host: "example.com" },
184
+ });
185
+
186
+ export const deployment = new Deployment({
187
+ metadata: { name: include("web-app.fullname"), labels: include("web-app.labels") },
188
+ spec: {
189
+ replicas: values.replicaCount,
190
+ selector: { matchLabels: include("web-app.selectorLabels") },
191
+ template: {
192
+ metadata: { labels: include("web-app.selectorLabels") },
193
+ spec: {
194
+ containers: [{
195
+ name: "web-app",
196
+ image: printf("%s:%s", values.image.repository, values.image.tag),
197
+ ports: [{ containerPort: values.service.port, name: "http" }],
198
+ }],
199
+ },
200
+ },
201
+ },
202
+ });
203
+
204
+ export const service = new Service({
205
+ metadata: { name: include("web-app.fullname"), labels: include("web-app.labels") },
206
+ spec: {
207
+ type: values.service.type,
208
+ ports: [{ port: values.service.port, targetPort: "http", protocol: "TCP", name: "http" }],
209
+ selector: include("web-app.selectorLabels"),
210
+ },
211
+ });
212
+ `;
213
+ },
214
+ },
215
+ ];
216
+ },
217
+ initTemplates(_template) {
218
+ if (_template === "stateful-service") {
219
+ return {
220
+ src: {
221
+ "chart.ts": `import { Chart, Values, HelmNotes } from "@intentius/chant-lexicon-helm";
222
+ import { values, Release, include, toYaml, printf } from "@intentius/chant-lexicon-helm/intrinsics";
223
+ import { StatefulSet, Service, PersistentVolumeClaim, ConfigMap } from "@intentius/chant-lexicon-k8s";
224
+
225
+ export const chart = new Chart({
226
+ apiVersion: "v2",
227
+ name: "my-stateful-app",
228
+ version: "0.1.0",
229
+ appVersion: "1.0.0",
230
+ type: "application",
231
+ description: "A Helm chart for a stateful application",
232
+ });
233
+
234
+ export const valuesSchema = new Values({
235
+ replicaCount: 3,
236
+ image: {
237
+ repository: "postgres",
238
+ tag: "16",
239
+ pullPolicy: "IfNotPresent",
240
+ },
241
+ service: {
242
+ port: 5432,
243
+ },
244
+ storage: {
245
+ size: "10Gi",
246
+ storageClass: "",
247
+ },
248
+ config: {
249
+ maxConnections: "100",
250
+ sharedBuffers: "256MB",
251
+ },
252
+ });
253
+
254
+ export const configMap = new ConfigMap({
255
+ metadata: {
256
+ name: include("my-stateful-app.fullname"),
257
+ labels: include("my-stateful-app.labels"),
258
+ },
259
+ data: {
260
+ "max_connections": values.config.maxConnections,
261
+ "shared_buffers": values.config.sharedBuffers,
262
+ },
263
+ });
264
+
265
+ export const headlessService = new Service({
266
+ metadata: {
267
+ name: printf("%s-headless", include("my-stateful-app.fullname")),
268
+ labels: include("my-stateful-app.labels"),
269
+ },
270
+ spec: {
271
+ type: "ClusterIP",
272
+ clusterIP: "None",
273
+ ports: [{ port: values.service.port, targetPort: "db", protocol: "TCP", name: "db" }],
274
+ selector: include("my-stateful-app.selectorLabels"),
275
+ },
276
+ });
277
+
278
+ export const statefulSet = new StatefulSet({
279
+ metadata: {
280
+ name: include("my-stateful-app.fullname"),
281
+ labels: include("my-stateful-app.labels"),
282
+ },
283
+ spec: {
284
+ serviceName: printf("%s-headless", include("my-stateful-app.fullname")),
285
+ replicas: values.replicaCount,
286
+ selector: { matchLabels: include("my-stateful-app.selectorLabels") },
287
+ template: {
288
+ metadata: { labels: include("my-stateful-app.selectorLabels") },
289
+ spec: {
290
+ containers: [{
291
+ name: "db",
292
+ image: printf("%s:%s", values.image.repository, values.image.tag),
293
+ ports: [{ containerPort: values.service.port, name: "db" }],
294
+ volumeMounts: [{ name: "data", mountPath: "/var/lib/postgresql/data" }],
295
+ }],
296
+ },
297
+ },
298
+ volumeClaimTemplates: [
299
+ new PersistentVolumeClaim({
300
+ metadata: { name: "data" },
301
+ spec: {
302
+ accessModes: ["ReadWriteOnce"],
303
+ resources: { requests: { storage: values.storage.size } },
304
+ },
305
+ }),
306
+ ],
307
+ },
308
+ });
309
+ `,
310
+ },
311
+ };
312
+ }
313
+ return {
314
+ src: {
315
+ "chart.ts": `import { Chart, Values, HelmNotes } from "@intentius/chant-lexicon-helm";
316
+ import { values, Release, include, toYaml, printf } from "@intentius/chant-lexicon-helm/intrinsics";
317
+ import { Deployment, Service } from "@intentius/chant-lexicon-k8s";
318
+
319
+ export const chart = new Chart({
320
+ apiVersion: "v2",
321
+ name: "my-app",
322
+ version: "0.1.0",
323
+ appVersion: "1.0.0",
324
+ type: "application",
325
+ description: "A Helm chart for my application",
326
+ });
327
+
328
+ export const valuesSchema = new Values({
329
+ replicaCount: 1,
330
+ image: {
331
+ repository: "nginx",
332
+ tag: "",
333
+ pullPolicy: "IfNotPresent",
334
+ },
335
+ service: {
336
+ type: "ClusterIP",
337
+ port: 80,
338
+ },
339
+ });
340
+
341
+ export const deployment = new Deployment({
342
+ metadata: {
343
+ name: include("my-app.fullname"),
344
+ labels: include("my-app.labels"),
345
+ },
346
+ spec: {
347
+ replicas: values.replicaCount,
348
+ selector: {
349
+ matchLabels: include("my-app.selectorLabels"),
350
+ },
351
+ template: {
352
+ metadata: {
353
+ labels: include("my-app.selectorLabels"),
354
+ },
355
+ spec: {
356
+ containers: [{
357
+ name: "my-app",
358
+ image: printf("%s:%s", values.image.repository, values.image.tag),
359
+ ports: [{ containerPort: values.service.port, name: "http" }],
360
+ }],
361
+ },
362
+ },
363
+ },
364
+ });
365
+
366
+ export const service = new Service({
367
+ metadata: {
368
+ name: include("my-app.fullname"),
369
+ labels: include("my-app.labels"),
370
+ },
371
+ spec: {
372
+ type: values.service.type,
373
+ ports: [{
374
+ port: values.service.port,
375
+ targetPort: "http",
376
+ protocol: "TCP",
377
+ name: "http",
378
+ }],
379
+ selector: include("my-app.selectorLabels"),
380
+ },
381
+ });
382
+ `,
383
+ },
384
+ };
385
+ },
386
+ skills: createSkillsLoader(import.meta.url, [
387
+ { file: "chant-helm.md", name: "chant-helm", description: "Build, validate, and package Helm charts from a chant project" },
388
+ { file: "chant-helm-patterns.md", name: "chant-helm-patterns", description: "Common Helm chart patterns and best practices using chant" },
389
+ { file: "chant-helm-security.md", name: "chant-helm-security", description: "Security best practices for Helm charts built with chant" },
390
+ ]),
391
+ async docs(options) {
392
+ const { generateDocs } = await import("./codegen/docs.js");
393
+ await generateDocs({ verbose: options?.verbose });
394
+ },
395
+ async generate(options) {
396
+ const { generate, writeGeneratedFiles } = await import("./codegen/generate.js");
397
+ const { dirname } = await import("path");
398
+ const { fileURLToPath } = await import("url");
399
+ const result = await generate({ verbose: options?.verbose ?? true });
400
+ const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
401
+ writeGeneratedFiles(result, pkgDir);
402
+ console.error(`Generated ${result.resources} resources, ${result.properties} property types`);
403
+ },
404
+ async validate(options) {
405
+ const { validate } = await import("./validate.js");
406
+ const { printValidationResult } = await import("@intentius/chant/codegen/validate");
407
+ const result = await validate();
408
+ printValidationResult(result);
409
+ },
410
+ async coverage(options) {
411
+ const { analyzeHelmCoverage } = await import("./coverage.js");
412
+ await analyzeHelmCoverage({
413
+ verbose: options?.verbose,
414
+ minOverall: options?.minOverall,
415
+ });
416
+ },
417
+ async package(options) {
418
+ const { packageLexicon } = await import("./codegen/package.js");
419
+ const { writeBundleSpec } = await import("@intentius/chant/codegen/package");
420
+ const { join, dirname } = await import("path");
421
+ const { fileURLToPath } = await import("url");
422
+ const { spec, stats } = await packageLexicon({ verbose: options?.verbose, force: options?.force });
423
+ const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
424
+ const distDir = join(pkgDir, "dist");
425
+ writeBundleSpec(spec, distDir);
426
+ console.error(`Packaged ${stats.resources} resources, ${stats.ruleCount} rules, ${stats.skillCount} skills`);
427
+ },
428
+ async listArtifacts(options) {
429
+ const { listArtifacts } = await import("./list-artifacts.js");
430
+ return listArtifacts(options);
431
+ },
432
+ // #1246 — per-resource observation of what each declared chart's release
433
+ // holds (`helm get manifest` + `helm get hooks`, both channels), with total
434
+ // tri-state verdicts. This is what gives `lifecycle diff --live` helm rows.
435
+ async describeResources(options) {
436
+ const { describeResources } = await import("./describe-resources.js");
437
+ return describeResources(options);
438
+ },
439
+ // #1247 — deep property-level trees for the release's resources, read live
440
+ // through the k8s lexicon's typed client and normalized by its hooks.
441
+ // Dynamic import for the same reason k8s's is: the reader reaches
442
+ // `@intentius/chant-k8s-client`, which must stay off the build path.
443
+ async observeResourcesDeep(options) {
444
+ const { observeResourcesDeepHelm } = await import("./deep-observe.js");
445
+ return observeResourcesDeepHelm(options);
446
+ },
447
+ // The k8s lexicon's hooks by reference, not a copy (#1247) — the objects a
448
+ // release deploys are Kubernetes objects, and core normalizes the declared
449
+ // tree with the same rules the reader used.
450
+ deepNormalizationHooks: helmDeepNormalizationHooks,
451
+ // #1495 piece 4 — the deploy-unit observer for helm-upgrade units: a
452
+ // release's presence and native status, read back by the same name the
453
+ // capability deploys under. Dynamic import mirrors listArtifacts.
454
+ async describeStackStatus(options) {
455
+ const { describeStackStatus } = await import("./describe-stack-status.js");
456
+ return describeStackStatus(options);
457
+ },
458
+ };