@intentius/chant-lexicon-k8s 0.49.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 (120) hide show
  1. package/dist/composites/index.d.ts +2 -0
  2. package/dist/composites/index.d.ts.map +1 -1
  3. package/dist/composites/operator-stack.d.ts +183 -0
  4. package/dist/composites/operator-stack.d.ts.map +1 -0
  5. package/dist/index.d.ts +2 -2
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/integrity.json +35 -35
  8. package/dist/lint/post-synth/argo-helpers.d.ts.map +1 -1
  9. package/dist/lint/post-synth/crd-schema-helpers.d.ts.map +1 -1
  10. package/dist/lint/post-synth/k8s-helpers.d.ts +28 -5
  11. package/dist/lint/post-synth/k8s-helpers.d.ts.map +1 -1
  12. package/dist/lint/post-synth/wk8005.d.ts.map +1 -1
  13. package/dist/lint/post-synth/wk8006.d.ts.map +1 -1
  14. package/dist/lint/post-synth/wk8041.d.ts.map +1 -1
  15. package/dist/lint/post-synth/wk8042.d.ts.map +1 -1
  16. package/dist/lint/post-synth/wk8101.d.ts.map +1 -1
  17. package/dist/lint/post-synth/wk8102.d.ts.map +1 -1
  18. package/dist/lint/post-synth/wk8103.d.ts.map +1 -1
  19. package/dist/lint/post-synth/wk8104.d.ts.map +1 -1
  20. package/dist/lint/post-synth/wk8105.d.ts.map +1 -1
  21. package/dist/lint/post-synth/wk8201.d.ts.map +1 -1
  22. package/dist/lint/post-synth/wk8202.d.ts.map +1 -1
  23. package/dist/lint/post-synth/wk8203.d.ts.map +1 -1
  24. package/dist/lint/post-synth/wk8204.d.ts.map +1 -1
  25. package/dist/lint/post-synth/wk8205.d.ts.map +1 -1
  26. package/dist/lint/post-synth/wk8207.d.ts.map +1 -1
  27. package/dist/lint/post-synth/wk8208.d.ts.map +1 -1
  28. package/dist/lint/post-synth/wk8209.d.ts.map +1 -1
  29. package/dist/lint/post-synth/wk8301.d.ts.map +1 -1
  30. package/dist/lint/post-synth/wk8302.d.ts.map +1 -1
  31. package/dist/lint/post-synth/wk8303.d.ts.map +1 -1
  32. package/dist/lint/post-synth/wk8304.d.ts.map +1 -1
  33. package/dist/lint/post-synth/wk8305.d.ts.map +1 -1
  34. package/dist/lint/post-synth/wk8306.d.ts.map +1 -1
  35. package/dist/lint/post-synth/wk8401.d.ts.map +1 -1
  36. package/dist/lint/post-synth/wk8402.d.ts.map +1 -1
  37. package/dist/lint/post-synth/wk8403.d.ts.map +1 -1
  38. package/dist/lint/post-synth/wk8404.d.ts.map +1 -1
  39. package/dist/lint/post-synth/wk8405.d.ts.map +1 -1
  40. package/dist/lint/post-synth/wk8406.d.ts.map +1 -1
  41. package/dist/lint/post-synth/wk8407.d.ts.map +1 -1
  42. package/dist/manifest.json +1 -1
  43. package/dist/op/builders.d.ts +69 -0
  44. package/dist/op/builders.d.ts.map +1 -0
  45. package/dist/rules/argo-helpers.ts +2 -6
  46. package/dist/rules/crd-schema-helpers.ts +4 -6
  47. package/dist/rules/k8s-helpers.ts +32 -23
  48. package/dist/rules/wk8005.ts +27 -32
  49. package/dist/rules/wk8006.ts +33 -38
  50. package/dist/rules/wk8041.ts +19 -24
  51. package/dist/rules/wk8042.ts +17 -22
  52. package/dist/rules/wk8101.ts +29 -34
  53. package/dist/rules/wk8102.ts +13 -18
  54. package/dist/rules/wk8103.ts +17 -22
  55. package/dist/rules/wk8104.ts +30 -35
  56. package/dist/rules/wk8105.ts +16 -21
  57. package/dist/rules/wk8201.ts +26 -31
  58. package/dist/rules/wk8202.ts +17 -22
  59. package/dist/rules/wk8203.ts +17 -22
  60. package/dist/rules/wk8204.ts +46 -51
  61. package/dist/rules/wk8205.ts +27 -32
  62. package/dist/rules/wk8207.ts +14 -19
  63. package/dist/rules/wk8208.ts +14 -19
  64. package/dist/rules/wk8209.ts +14 -19
  65. package/dist/rules/wk8301.ts +25 -30
  66. package/dist/rules/wk8302.ts +19 -24
  67. package/dist/rules/wk8303.ts +23 -26
  68. package/dist/rules/wk8304.ts +35 -40
  69. package/dist/rules/wk8305.ts +39 -42
  70. package/dist/rules/wk8306.ts +20 -25
  71. package/dist/rules/wk8401.ts +59 -64
  72. package/dist/rules/wk8402.ts +13 -18
  73. package/dist/rules/wk8403.ts +32 -37
  74. package/dist/rules/wk8404.ts +15 -21
  75. package/dist/rules/wk8405.ts +15 -18
  76. package/dist/rules/wk8406.ts +29 -34
  77. package/dist/rules/wk8407.ts +17 -22
  78. package/dist/serializer.d.ts.map +1 -1
  79. package/package.json +3 -3
  80. package/src/composites/composites.test.ts +283 -0
  81. package/src/composites/index.ts +4 -0
  82. package/src/composites/operator-stack.ts +385 -0
  83. package/src/index.ts +11 -0
  84. package/src/lint/post-synth/argo-helpers.ts +2 -6
  85. package/src/lint/post-synth/crd-schema-helpers.ts +4 -6
  86. package/src/lint/post-synth/k8s-helpers.test.ts +26 -9
  87. package/src/lint/post-synth/k8s-helpers.ts +32 -23
  88. package/src/lint/post-synth/wk8005.ts +27 -32
  89. package/src/lint/post-synth/wk8006.ts +33 -38
  90. package/src/lint/post-synth/wk8041.ts +19 -24
  91. package/src/lint/post-synth/wk8042.ts +17 -22
  92. package/src/lint/post-synth/wk8101.ts +29 -34
  93. package/src/lint/post-synth/wk8102.ts +13 -18
  94. package/src/lint/post-synth/wk8103.ts +17 -22
  95. package/src/lint/post-synth/wk8104.ts +30 -35
  96. package/src/lint/post-synth/wk8105.ts +16 -21
  97. package/src/lint/post-synth/wk8201.ts +26 -31
  98. package/src/lint/post-synth/wk8202.ts +17 -22
  99. package/src/lint/post-synth/wk8203.ts +17 -22
  100. package/src/lint/post-synth/wk8204.ts +46 -51
  101. package/src/lint/post-synth/wk8205.ts +27 -32
  102. package/src/lint/post-synth/wk8207.ts +14 -19
  103. package/src/lint/post-synth/wk8208.ts +14 -19
  104. package/src/lint/post-synth/wk8209.ts +14 -19
  105. package/src/lint/post-synth/wk8301.ts +25 -30
  106. package/src/lint/post-synth/wk8302.ts +19 -24
  107. package/src/lint/post-synth/wk8303.ts +23 -26
  108. package/src/lint/post-synth/wk8304.ts +35 -40
  109. package/src/lint/post-synth/wk8305.ts +39 -42
  110. package/src/lint/post-synth/wk8306.ts +20 -25
  111. package/src/lint/post-synth/wk8401.ts +59 -64
  112. package/src/lint/post-synth/wk8402.ts +13 -18
  113. package/src/lint/post-synth/wk8403.ts +32 -37
  114. package/src/lint/post-synth/wk8404.ts +15 -21
  115. package/src/lint/post-synth/wk8405.ts +15 -18
  116. package/src/lint/post-synth/wk8406.ts +29 -34
  117. package/src/lint/post-synth/wk8407.ts +17 -22
  118. package/src/op/builders.test.ts +82 -0
  119. package/src/op/builders.ts +95 -0
  120. package/src/serializer.ts +9 -1
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
10
- import { getPrimaryOutput, parseK8sManifests, extractRayVersion } from "./k8s-helpers";
10
+ import { docsToManifests, extractRayVersion } from "./k8s-helpers";
11
11
 
12
12
  export const wk8403: PostSynthCheck = {
13
13
  id: "WK8403",
@@ -16,42 +16,37 @@ export const wk8403: PostSynthCheck = {
16
16
  check(ctx: PostSynthContext): PostSynthDiagnostic[] {
17
17
  const diagnostics: PostSynthDiagnostic[] = [];
18
18
 
19
- for (const [, output] of ctx.outputs) {
20
- const yaml = getPrimaryOutput(output);
21
- const manifests = parseK8sManifests(yaml);
22
-
23
- for (const manifest of manifests) {
24
- if (manifest.kind !== "RayCluster") continue;
25
-
26
- const clusterName = manifest.metadata?.name ?? "RayCluster";
27
- const spec = manifest.spec as Record<string, unknown> | undefined;
28
- if (!spec) continue;
29
-
30
- const rayVersion = spec.rayVersion as string | undefined;
31
- if (!rayVersion) continue; // WK8402 covers the missing case
32
-
33
- // Extract version from head container image
34
- const headGroupSpec = spec.headGroupSpec as Record<string, unknown> | undefined;
35
- const tmpl = headGroupSpec?.template as Record<string, unknown> | undefined;
36
- const podSpec = tmpl?.spec as Record<string, unknown> | undefined;
37
- const containers = podSpec?.containers as Array<Record<string, unknown>> | undefined;
38
- if (!Array.isArray(containers) || containers.length === 0) continue;
39
-
40
- const image = containers[0].image as string | undefined;
41
- if (!image) continue;
42
-
43
- const imageVersion = extractRayVersion(image);
44
- if (!imageVersion) continue; // Can't determine version from tag
45
-
46
- if (imageVersion !== rayVersion) {
47
- diagnostics.push({
48
- checkId: "WK8403",
49
- severity: "warning",
50
- message: `RayCluster "${clusterName}": spec.rayVersion "${rayVersion}" does not match head image tag "${imageVersion}" — autoscaler may run a mismatched Ray version`,
51
- entity: clusterName,
52
- lexicon: "k8s",
53
- });
54
- }
19
+ for (const manifest of docsToManifests(ctx)) {
20
+ if (manifest.kind !== "RayCluster") continue;
21
+
22
+ const clusterName = manifest.metadata?.name ?? "RayCluster";
23
+ const spec = manifest.spec as Record<string, unknown> | undefined;
24
+ if (!spec) continue;
25
+
26
+ const rayVersion = spec.rayVersion as string | undefined;
27
+ if (!rayVersion) continue; // WK8402 covers the missing case
28
+
29
+ // Extract version from head container image
30
+ const headGroupSpec = spec.headGroupSpec as Record<string, unknown> | undefined;
31
+ const tmpl = headGroupSpec?.template as Record<string, unknown> | undefined;
32
+ const podSpec = tmpl?.spec as Record<string, unknown> | undefined;
33
+ const containers = podSpec?.containers as Array<Record<string, unknown>> | undefined;
34
+ if (!Array.isArray(containers) || containers.length === 0) continue;
35
+
36
+ const image = containers[0].image as string | undefined;
37
+ if (!image) continue;
38
+
39
+ const imageVersion = extractRayVersion(image);
40
+ if (!imageVersion) continue; // Can't determine version from tag
41
+
42
+ if (imageVersion !== rayVersion) {
43
+ diagnostics.push({
44
+ checkId: "WK8403",
45
+ severity: "warning",
46
+ message: `RayCluster "${clusterName}": spec.rayVersion "${rayVersion}" does not match head image tag "${imageVersion}" — autoscaler may run a mismatched Ray version`,
47
+ entity: clusterName,
48
+ lexicon: "k8s",
49
+ });
55
50
  }
56
51
  }
57
52
 
@@ -10,8 +10,7 @@
10
10
 
11
11
  import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
12
12
  import {
13
- getPrimaryOutput,
14
- parseK8sManifests,
13
+ docsToManifests,
15
14
  extractContainers,
16
15
  extractPodSpec,
17
16
  requestsGpu,
@@ -26,28 +25,23 @@ export const wk8404: PostSynthCheck = {
26
25
  check(ctx: PostSynthContext): PostSynthDiagnostic[] {
27
26
  const diagnostics: PostSynthDiagnostic[] = [];
28
27
 
29
- for (const [, output] of ctx.outputs) {
30
- const yaml = getPrimaryOutput(output);
31
- const manifests = parseK8sManifests(yaml);
28
+ for (const manifest of docsToManifests(ctx)) {
29
+ if (!manifest.kind || !GPU_POD_KINDS.has(manifest.kind)) continue;
32
30
 
33
- for (const manifest of manifests) {
34
- if (!manifest.kind || !GPU_POD_KINDS.has(manifest.kind)) continue;
31
+ const containers = extractContainers(manifest);
32
+ if (!requestsGpu(containers)) continue;
35
33
 
36
- const containers = extractContainers(manifest);
37
- if (!requestsGpu(containers)) continue;
34
+ const podSpec = extractPodSpec(manifest);
35
+ if (toleratesGpu(podSpec?.tolerations)) continue;
38
36
 
39
- const podSpec = extractPodSpec(manifest);
40
- if (toleratesGpu(podSpec?.tolerations)) continue;
41
-
42
- const resourceName = manifest.metadata?.name ?? manifest.kind;
43
- diagnostics.push({
44
- checkId: "WK8404",
45
- severity: "error",
46
- message: `${manifest.kind} "${resourceName}" requests nvidia.com/gpu but has no toleration for the nvidia.com/gpu taint — it will not schedule onto a tainted GPU node pool`,
47
- entity: resourceName,
48
- lexicon: "k8s",
49
- });
50
- }
37
+ const resourceName = manifest.metadata?.name ?? manifest.kind;
38
+ diagnostics.push({
39
+ checkId: "WK8404",
40
+ severity: "error",
41
+ message: `${manifest.kind} "${resourceName}" requests nvidia.com/gpu but has no toleration for the nvidia.com/gpu taint — it will not schedule onto a tainted GPU node pool`,
42
+ entity: resourceName,
43
+ lexicon: "k8s",
44
+ });
51
45
  }
52
46
 
53
47
  return diagnostics;
@@ -10,7 +10,7 @@
10
10
  */
11
11
 
12
12
  import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
13
- import { getPrimaryOutput, parseK8sManifests } from "./k8s-helpers";
13
+ import { docsToManifests } from "./k8s-helpers";
14
14
  import type { K8sManifest } from "./k8s-helpers";
15
15
 
16
16
  /** `app.kubernetes.io/component` values this lexicon's composites use for serving workloads. */
@@ -30,27 +30,24 @@ export const wk8405: PostSynthCheck = {
30
30
  check(ctx: PostSynthContext): PostSynthDiagnostic[] {
31
31
  const diagnostics: PostSynthDiagnostic[] = [];
32
32
 
33
- for (const [, output] of ctx.outputs) {
34
- const yaml = getPrimaryOutput(output);
35
- const manifests = parseK8sManifests(yaml);
33
+ const manifests = docsToManifests(ctx);
36
34
 
37
- const pdbSelectors = collectPdbSelectors(manifests);
35
+ const pdbSelectors = collectPdbSelectors(manifests);
38
36
 
39
- for (const manifest of manifests) {
40
- if (!isServingWorkload(manifest)) continue;
37
+ for (const manifest of manifests) {
38
+ if (!isServingWorkload(manifest)) continue;
41
39
 
42
- const labels = manifest.metadata?.labels;
43
- const resourceName = manifest.metadata?.name ?? manifest.kind ?? "workload";
40
+ const labels = manifest.metadata?.labels;
41
+ const resourceName = manifest.metadata?.name ?? manifest.kind ?? "workload";
44
42
 
45
- if (!labels || !hasCoveringPdb(labels, pdbSelectors)) {
46
- diagnostics.push({
47
- checkId: "WK8405",
48
- severity: "info",
49
- message: `${manifest.kind} "${resourceName}" is a serving workload with no PodDisruptionBudget — add a PDB so a node drain or cluster upgrade cannot take down every replica at once`,
50
- entity: resourceName,
51
- lexicon: "k8s",
52
- });
53
- }
43
+ if (!labels || !hasCoveringPdb(labels, pdbSelectors)) {
44
+ diagnostics.push({
45
+ checkId: "WK8405",
46
+ severity: "info",
47
+ message: `${manifest.kind} "${resourceName}" is a serving workload with no PodDisruptionBudget — add a PDB so a node drain or cluster upgrade cannot take down every replica at once`,
48
+ entity: resourceName,
49
+ lexicon: "k8s",
50
+ });
54
51
  }
55
52
  }
56
53
 
@@ -9,7 +9,7 @@
9
9
  */
10
10
 
11
11
  import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
12
- import { getPrimaryOutput, parseK8sManifests, extractContainers, GPU_POD_KINDS, NVIDIA_GPU_RESOURCE } from "./k8s-helpers";
12
+ import { docsToManifests, extractContainers, GPU_POD_KINDS, NVIDIA_GPU_RESOURCE } from "./k8s-helpers";
13
13
 
14
14
  export const wk8406: PostSynthCheck = {
15
15
  id: "WK8406",
@@ -18,39 +18,34 @@ export const wk8406: PostSynthCheck = {
18
18
  check(ctx: PostSynthContext): PostSynthDiagnostic[] {
19
19
  const diagnostics: PostSynthDiagnostic[] = [];
20
20
 
21
- for (const [, output] of ctx.outputs) {
22
- const yaml = getPrimaryOutput(output);
23
- const manifests = parseK8sManifests(yaml);
24
-
25
- for (const manifest of manifests) {
26
- if (!manifest.kind || !GPU_POD_KINDS.has(manifest.kind)) continue;
27
-
28
- const containers = extractContainers(manifest);
29
- const resourceName = manifest.metadata?.name ?? manifest.kind;
30
-
31
- for (const container of containers) {
32
- const requests = container.resources?.requests as Record<string, unknown> | undefined;
33
- const limits = container.resources?.limits as Record<string, unknown> | undefined;
34
- const requestsGpu = Boolean(requests?.[NVIDIA_GPU_RESOURCE]) || Boolean(limits?.[NVIDIA_GPU_RESOURCE]);
35
- if (!requestsGpu) continue;
36
-
37
- const missing: string[] = [];
38
- if (!limits || typeof limits !== "object") {
39
- missing.push("cpu", "memory");
40
- } else {
41
- if (!limits.cpu) missing.push("cpu");
42
- if (!limits.memory) missing.push("memory");
43
- }
44
-
45
- if (missing.length > 0) {
46
- diagnostics.push({
47
- checkId: "WK8406",
48
- severity: "warning",
49
- message: `GPU container "${container.name ?? "(unnamed)"}" in ${manifest.kind} "${resourceName}" is missing resource limits for ${missing.join(", ")} — a GPU pod without cpu/memory limits can starve its (scarce, expensive) node`,
50
- entity: resourceName,
51
- lexicon: "k8s",
52
- });
53
- }
21
+ for (const manifest of docsToManifests(ctx)) {
22
+ if (!manifest.kind || !GPU_POD_KINDS.has(manifest.kind)) continue;
23
+
24
+ const containers = extractContainers(manifest);
25
+ const resourceName = manifest.metadata?.name ?? manifest.kind;
26
+
27
+ for (const container of containers) {
28
+ const requests = container.resources?.requests as Record<string, unknown> | undefined;
29
+ const limits = container.resources?.limits as Record<string, unknown> | undefined;
30
+ const requestsGpu = Boolean(requests?.[NVIDIA_GPU_RESOURCE]) || Boolean(limits?.[NVIDIA_GPU_RESOURCE]);
31
+ if (!requestsGpu) continue;
32
+
33
+ const missing: string[] = [];
34
+ if (!limits || typeof limits !== "object") {
35
+ missing.push("cpu", "memory");
36
+ } else {
37
+ if (!limits.cpu) missing.push("cpu");
38
+ if (!limits.memory) missing.push("memory");
39
+ }
40
+
41
+ if (missing.length > 0) {
42
+ diagnostics.push({
43
+ checkId: "WK8406",
44
+ severity: "warning",
45
+ message: `GPU container "${container.name ?? "(unnamed)"}" in ${manifest.kind} "${resourceName}" is missing resource limits for ${missing.join(", ")} — a GPU pod without cpu/memory limits can starve its (scarce, expensive) node`,
46
+ entity: resourceName,
47
+ lexicon: "k8s",
48
+ });
54
49
  }
55
50
  }
56
51
  }
@@ -12,7 +12,7 @@
12
12
  */
13
13
 
14
14
  import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
15
- import { getPrimaryOutput, parseK8sManifests } from "./k8s-helpers";
15
+ import { docsToManifests } from "./k8s-helpers";
16
16
 
17
17
  /** Trailing path segments that mean "no explicit version" even though present. */
18
18
  const FLOATING_TAGS = new Set(["latest", "main", "master", "head"]);
@@ -40,29 +40,24 @@ export const wk8407: PostSynthCheck = {
40
40
  check(ctx: PostSynthContext): PostSynthDiagnostic[] {
41
41
  const diagnostics: PostSynthDiagnostic[] = [];
42
42
 
43
- for (const [, output] of ctx.outputs) {
44
- const yaml = getPrimaryOutput(output);
45
- const manifests = parseK8sManifests(yaml);
43
+ for (const manifest of docsToManifests(ctx)) {
44
+ if (manifest.kind !== "InferenceService") continue;
46
45
 
47
- for (const manifest of manifests) {
48
- if (manifest.kind !== "InferenceService") continue;
46
+ const spec = manifest.spec as Record<string, unknown> | undefined;
47
+ const predictor = spec?.predictor as Record<string, unknown> | undefined;
48
+ const model = predictor?.model as Record<string, unknown> | undefined;
49
+ const storageUri = model?.storageUri as string | undefined;
50
+ if (!storageUri) continue;
49
51
 
50
- const spec = manifest.spec as Record<string, unknown> | undefined;
51
- const predictor = spec?.predictor as Record<string, unknown> | undefined;
52
- const model = predictor?.model as Record<string, unknown> | undefined;
53
- const storageUri = model?.storageUri as string | undefined;
54
- if (!storageUri) continue;
55
-
56
- if (!isPinnedStorageUri(storageUri)) {
57
- const resourceName = manifest.metadata?.name ?? "InferenceService";
58
- diagnostics.push({
59
- checkId: "WK8407",
60
- severity: "warning",
61
- message: `InferenceService "${resourceName}": model storageUri "${storageUri}" has no explicit version segment — pin it (e.g. via the Model composite's required \`version\`) so a redeploy can't silently pick up different weights`,
62
- entity: resourceName,
63
- lexicon: "k8s",
64
- });
65
- }
52
+ if (!isPinnedStorageUri(storageUri)) {
53
+ const resourceName = manifest.metadata?.name ?? "InferenceService";
54
+ diagnostics.push({
55
+ checkId: "WK8407",
56
+ severity: "warning",
57
+ message: `InferenceService "${resourceName}": model storageUri "${storageUri}" has no explicit version segment — pin it (e.g. via the Model composite's required \`version\`) so a redeploy can't silently pick up different weights`,
58
+ entity: resourceName,
59
+ lexicon: "k8s",
60
+ });
66
61
  }
67
62
  }
68
63
 
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Typed step-builder wrappers (chant #1288 Stage 2). Two things asserted:
3
+ *
4
+ * - **Backward compat**: for every input core's original untyped builder
5
+ * accepted, the new typed twin here produces a byte-identical
6
+ * `ActivityStep` (own enumerable fields — `.out` is a non-enumerable
7
+ * getter on both, per `builders.ts`'s `activity()`, so `toEqual` never
8
+ * sees it). Same construction path (`activity()` + `takeProfileAndId`),
9
+ * so this is really asserting the two call sites agree, not re-deriving
10
+ * the logic.
11
+ * - **Step-output refs compose**: a {@link StepOutputRef} in a typed slot
12
+ * type-checks and lands in the step's `args` untouched (chant #1950).
13
+ *
14
+ * The `@ts-expect-error` block at the bottom is a compile-time-only check —
15
+ * never executed, just walked by `npm run typecheck` (tsc), which fails if a
16
+ * marked line does NOT error (an "unused '@ts-expect-error' directive").
17
+ */
18
+
19
+ import { describe, test, expect } from "vitest";
20
+ import {
21
+ kubectlApply as kubectlApplyOld,
22
+ waitForReady as waitForReadyOld,
23
+ ensureSecret as ensureSecretOld,
24
+ stepOutput,
25
+ type StepOutputRef,
26
+ } from "@intentius/chant/op";
27
+ import { kubectlApply, waitForReady, ensureSecret } from "./builders";
28
+
29
+ describe("k8s typed step builders (#1288 Stage 2)", () => {
30
+ test("kubectlApply: identical ActivityStep to core's original for a minimal call", () => {
31
+ expect(kubectlApply("dist/k8s.yaml")).toEqual(kubectlApplyOld("dist/k8s.yaml"));
32
+ });
33
+
34
+ test("kubectlApply: identical ActivityStep with opts (profile override, force, deleteMode)", () => {
35
+ const opts = { context: "kind-dev", force: true, deleteMode: "owned-only" as const, profile: "fastIdempotent" as const };
36
+ expect(kubectlApply("dist/k8s.yaml", opts)).toEqual(kubectlApplyOld("dist/k8s.yaml", opts));
37
+ });
38
+
39
+ test("kubectlApply: accepts a StepOutputRef in a typed slot", () => {
40
+ const ref = stepOutput("build-step", "manifestPath");
41
+ const step = kubectlApply("dist/k8s.yaml", { context: ref });
42
+ expect(step.args?.context).toBe(ref);
43
+ });
44
+
45
+ test("waitForReady: identical ActivityStep to core's original", () => {
46
+ const opts = { namespace: "prod", intervalMs: 5000 };
47
+ expect(waitForReady("certificate", "my-cert", opts)).toEqual(waitForReadyOld("certificate", "my-cert", opts));
48
+ });
49
+
50
+ test("ensureSecret: identical ActivityStep to core's original", () => {
51
+ const opts = { metadata: { team: "platform" } };
52
+ expect(ensureSecret("db-creds", ["password"], opts)).toEqual(ensureSecretOld("db-creds", ["password"], opts));
53
+ });
54
+
55
+ test("kubectlApply: .out is reachable when an id is given", () => {
56
+ const step = kubectlApply("dist/k8s.yaml", { id: "apply" });
57
+ const ref: StepOutputRef = step.out.fieldManager;
58
+ expect(ref.step).toBe("apply");
59
+ expect(ref.path).toBe("fieldManager");
60
+ });
61
+ });
62
+
63
+ // ── Compile-time-only: authoring-time type errors (never executed) ──────────
64
+ function _typeChecksOnly(): void {
65
+ // @ts-expect-error — "nameSpace" is not a key of KubectlApplyArgs (the
66
+ // exact typo class chant #1288 names: helmInstall("api", "./chart", {
67
+ // nameSpace: "prod" }) silently no-opped before this).
68
+ kubectlApply("dist/k8s.yaml", { nameSpace: "prod" });
69
+
70
+ // @ts-expect-error — deleteMode must be one of ApplyDeleteMode's literals.
71
+ kubectlApply("dist/k8s.yaml", { deleteMode: "always" });
72
+
73
+ // @ts-expect-error — force must be boolean, not a string.
74
+ kubectlApply("dist/k8s.yaml", { force: "yes" });
75
+
76
+ // @ts-expect-error — waitForReady's readiness spec field is misspelled.
77
+ waitForReady("certificate", "my-cert", { specc: {} });
78
+
79
+ // @ts-expect-error — ensureSecret's keys must be string[], not a single string.
80
+ ensureSecret("db-creds", "password");
81
+ }
82
+ void _typeChecksOnly;
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Typed step-builder wrappers for this lexicon's activities (chant #1288
3
+ * Stage 2 — "regenerate the step builders as fully typed wrappers"). Core
4
+ * cannot import a lexicon's `*Args` types (lexicons depend on core, not the
5
+ * other way around — see `packages/core/src/op/activity-contract.ts`'s module
6
+ * doc for the same layering constraint on Stage 1's contracts), so a fully
7
+ * typed `kubectlApply`/`waitForReady`/`ensureSecret` has to live beside the
8
+ * `*Args` interface it derives from: here, next to `./activities/kubectl.ts`
9
+ * etc. `opts`'s type in each wrapper below IS the activity's own `*Args`
10
+ * interface (via `Omit`/`WithStepRefs`) — never a hand-restated mirror — so
11
+ * `kubectlApply`/`waitForReady`/`ensureSecret`'s full option surface (the
12
+ * dozen-plus cross-field options on `kubectlApply` that Stage 1's zod
13
+ * contracts deliberately skipped rather than duplicate) is authoring-time
14
+ * typed for free, and stays in sync with the implementation by construction.
15
+ *
16
+ * Every field also accepts a {@link StepOutputRef} in its place
17
+ * ({@link WithStepRefs}, chant #1950) — a later step's args can consume an
18
+ * earlier step's declared output.
19
+ *
20
+ * `core`'s own `kubectlApply`/`waitForReady`/`ensureSecret` (in
21
+ * `@intentius/chant/op`, re-exported from `@intentius/chant-lexicon-temporal`
22
+ * for lexicon-agnostic single-import convenience) are UNCHANGED and produce
23
+ * byte-identical `ActivityStep` output for the same inputs — these are purely
24
+ * additive. Deliberately not swapped into the temporal barrel: that would
25
+ * make `@intentius/chant-lexicon-temporal` depend on this package (and on
26
+ * helm, for `helmInstall`) at runtime, which is exactly the "a worker image
27
+ * needs no kubectl binary" / "temporal stays product-agnostic" property the
28
+ * `op/activities/index.ts` module docs across this repo call out on purpose
29
+ * (activities were moved OUT of temporal into per-product lexicons for this
30
+ * reason; the step-builder layer shouldn't reintroduce the coupling from the
31
+ * other direction). An author who wants the typed surface imports it from
32
+ * here — `@intentius/chant-lexicon-k8s` — which a project using `kubectlApply`
33
+ * already depends on; existing `@intentius/chant-lexicon-temporal` imports
34
+ * keep working exactly as before, opting in only if the import is changed.
35
+ */
36
+
37
+ import {
38
+ activity,
39
+ takeProfileAndId,
40
+ type ActivityStep,
41
+ type NamedActivityStep,
42
+ type WithStepRefs,
43
+ } from "@intentius/chant/op";
44
+ import type { KubectlApplyArgs } from "./activities/kubectl";
45
+ import type { WaitForReadyArgs } from "./activities/wait-for-ready";
46
+ import type { EnsureSecretActivityArgs } from "./activities/ensure-secret";
47
+
48
+ /** Extra opts every wrapper below accepts alongside its activity's own fields. */
49
+ type StepOpts = { profile?: ActivityStep["profile"]; id?: string };
50
+
51
+ /**
52
+ * Run `kubectl apply -f <manifest>` — the fully typed twin of core's
53
+ * `kubectlApply` step-builder shortcut (chant #1288 Stage 2). `opts` is
54
+ * {@link KubectlApplyArgs} itself (minus the positional `manifest`) — every
55
+ * field `applyManifest` accepts, including `deleteMode`, `force`,
56
+ * `fieldManager`, `documents`, is authoring-time typed here, not just the
57
+ * subset a hand-written zod mirror would bother restating. Defaults to the
58
+ * `longInfra` profile (override via `opts.profile`).
59
+ */
60
+ export const kubectlApply = (
61
+ manifest: string,
62
+ opts?: WithStepRefs<Omit<KubectlApplyArgs, "manifest">> & StepOpts,
63
+ ): NamedActivityStep => {
64
+ const { args, profile, id } = takeProfileAndId(opts as Record<string, unknown> | undefined);
65
+ return activity("kubectlApply", { manifest, ...args }, { profile: profile ?? "longInfra", ...(id ? { id } : {}) });
66
+ };
67
+
68
+ /**
69
+ * Poll a Kubernetes resource until it reports ready, driven by a data-only
70
+ * readiness spec (CRD-aware; #365) — the fully typed twin of core's
71
+ * `waitForReady`. `opts` is {@link WaitForReadyArgs} itself, minus the
72
+ * positional `kind`/`name`. Defaults to the `k8sWait` profile.
73
+ */
74
+ export const waitForReady = (
75
+ kind: string,
76
+ name: string,
77
+ opts?: WithStepRefs<Omit<WaitForReadyArgs, "kind" | "name">> & StepOpts,
78
+ ): NamedActivityStep => {
79
+ const { args, profile, id } = takeProfileAndId(opts as Record<string, unknown> | undefined);
80
+ return activity("waitForReady", { kind, name, ...args }, { profile: profile ?? "k8sWait", ...(id ? { id } : {}) });
81
+ };
82
+
83
+ /**
84
+ * Ensure a `generated-once` secret exists in the target store (#1829) — the
85
+ * fully typed twin of core's `ensureSecret`. `opts` is
86
+ * {@link EnsureSecretActivityArgs} itself, minus the positional `name`/`keys`.
87
+ */
88
+ export const ensureSecret = (
89
+ name: string,
90
+ keys: string[],
91
+ opts?: WithStepRefs<Omit<EnsureSecretActivityArgs, "name" | "keys">> & StepOpts,
92
+ ): NamedActivityStep => {
93
+ const { args, profile, id } = takeProfileAndId(opts as Record<string, unknown> | undefined);
94
+ return activity("ensureSecret", { name, keys, ...args }, { profile, ...(id ? { id } : {}) });
95
+ };
package/src/serializer.ts CHANGED
@@ -258,6 +258,8 @@ export const k8sSerializer: Serializer = {
258
258
  const otherDocs: string[] = [];
259
259
  /** Sidecar files — committed ciphertext, copied byte-for-byte. */
260
260
  const files: Record<string, string> = {};
261
+ /** Basenames within `files` that must never be JSON.parse round-tripped (chant#1937). */
262
+ const verbatimFiles: string[] = [];
261
263
  const warnings: string[] = [];
262
264
 
263
265
  for (const [name, entity] of entities) {
@@ -280,6 +282,7 @@ export const k8sSerializer: Serializer = {
280
282
  );
281
283
  }
282
284
  files[entity.filename] = entity.text;
285
+ if (!verbatimFiles.includes(entity.filename)) verbatimFiles.push(entity.filename);
283
286
  continue;
284
287
  }
285
288
 
@@ -417,7 +420,12 @@ export const k8sSerializer: Serializer = {
417
420
  // A bare string when there is nothing extra to write, so the common case
418
421
  // stays byte-identical to what every existing consumer already reads.
419
422
  if (Object.keys(files).length === 0 && warnings.length === 0) return primary;
420
- return { primary, files, ...(warnings.length > 0 ? { warnings } : {}) };
423
+ return {
424
+ primary,
425
+ files,
426
+ ...(verbatimFiles.length > 0 ? { verbatimFiles } : {}),
427
+ ...(warnings.length > 0 ? { warnings } : {}),
428
+ };
421
429
  },
422
430
  };
423
431