@intentius/chant-lexicon-k8s 0.57.0 → 0.59.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 (89) hide show
  1. package/dist/api/fake-cluster.d.ts +7 -6
  2. package/dist/api/fake-cluster.d.ts.map +1 -1
  3. package/dist/api/sweep-types.d.ts +1 -2
  4. package/dist/api/sweep-types.d.ts.map +1 -1
  5. package/dist/codegen/docs.d.ts.map +1 -1
  6. package/dist/composites/cron-schedule.d.ts +26 -0
  7. package/dist/composites/cron-schedule.d.ts.map +1 -0
  8. package/dist/composites/cron-workload.d.ts +1 -1
  9. package/dist/composites/cron-workload.d.ts.map +1 -1
  10. package/dist/composites/operator-stack.d.ts +20 -19
  11. package/dist/composites/operator-stack.d.ts.map +1 -1
  12. package/dist/config-schema.d.ts +3 -0
  13. package/dist/config-schema.d.ts.map +1 -1
  14. package/dist/config.d.ts +26 -4
  15. package/dist/config.d.ts.map +1 -1
  16. package/dist/deep-observe-hooks.d.ts +4 -4
  17. package/dist/deep-observe.d.ts +23 -16
  18. package/dist/deep-observe.d.ts.map +1 -1
  19. package/dist/describe-resources.d.ts.map +1 -1
  20. package/dist/effect-receipt-row.d.ts +164 -0
  21. package/dist/effect-receipt-row.d.ts.map +1 -0
  22. package/dist/export-resources.d.ts +1 -1
  23. package/dist/index.d.ts +2 -0
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/integrity.json +4 -4
  26. package/dist/lint/audit-catalog.d.ts.map +1 -1
  27. package/dist/lint/audit-lineage.d.ts +10 -0
  28. package/dist/lint/audit-lineage.d.ts.map +1 -0
  29. package/dist/lint/post-synth/wk8505.d.ts +3 -1
  30. package/dist/lint/post-synth/wk8505.d.ts.map +1 -1
  31. package/dist/manifest.json +1 -1
  32. package/dist/okf/index.md +1 -1
  33. package/dist/okf/rules/WK8505.md +2 -2
  34. package/dist/okf/types/Kustomization.md +1 -1
  35. package/dist/op/activities/argo.d.ts +4 -4
  36. package/dist/op/activities/argo.d.ts.map +1 -1
  37. package/dist/op/activities/index.d.ts +15 -7
  38. package/dist/op/activities/index.d.ts.map +1 -1
  39. package/dist/op/activities/kubectl.d.ts +7 -7
  40. package/dist/op/activities/kubectl.d.ts.map +1 -1
  41. package/dist/op/activities/wait-for-ready.d.ts +6 -5
  42. package/dist/op/activities/wait-for-ready.d.ts.map +1 -1
  43. package/dist/op/builders.d.ts +13 -13
  44. package/dist/plugin.d.ts.map +1 -1
  45. package/dist/receipt-store.d.ts +113 -0
  46. package/dist/receipt-store.d.ts.map +1 -0
  47. package/dist/rules/wk8505.ts +4 -2
  48. package/dist/serializer.d.ts.map +1 -1
  49. package/dist/skills/chant-k8s-argo.md +27 -13
  50. package/dist/subscribe-changes.d.ts +84 -0
  51. package/dist/subscribe-changes.d.ts.map +1 -0
  52. package/package.json +3 -3
  53. package/src/api/fake-cluster.ts +17 -4
  54. package/src/api/sweep-types.ts +1 -2
  55. package/src/codegen/docs.ts +7 -0
  56. package/src/composites/composites.test.ts +72 -3
  57. package/src/composites/cron-schedule.ts +47 -0
  58. package/src/composites/cron-workload.ts +4 -1
  59. package/src/composites/operator-stack.ts +25 -24
  60. package/src/config-schema.ts +5 -0
  61. package/src/config.ts +27 -4
  62. package/src/crd/crd-sources.ts +4 -4
  63. package/src/deep-observe-hooks.ts +4 -4
  64. package/src/deep-observe.test.ts +219 -46
  65. package/src/deep-observe.ts +54 -28
  66. package/src/describe-resources.ts +31 -7
  67. package/src/effect-receipt-row.test.ts +285 -0
  68. package/src/effect-receipt-row.ts +268 -0
  69. package/src/export-resources.ts +1 -1
  70. package/src/index.ts +22 -0
  71. package/src/lint/audit-catalog.ts +6 -2
  72. package/src/lint/audit-lineage.ts +127 -0
  73. package/src/lint/post-synth/wk8505.ts +4 -2
  74. package/src/op/activities/argo.test.ts +7 -8
  75. package/src/op/activities/argo.ts +4 -4
  76. package/src/op/activities/index.ts +23 -7
  77. package/src/op/activities/kubectl.test.ts +43 -4
  78. package/src/op/activities/kubectl.ts +67 -67
  79. package/src/op/activities/wait-for-ready.test.ts +2 -2
  80. package/src/op/activities/wait-for-ready.ts +7 -7
  81. package/src/op/builders.ts +13 -13
  82. package/src/plugin.ts +16 -1
  83. package/src/receipt-store.test.ts +380 -0
  84. package/src/receipt-store.ts +290 -0
  85. package/src/serializer.test.ts +26 -0
  86. package/src/serializer.ts +92 -1
  87. package/src/skills/chant-k8s-argo.md +27 -13
  88. package/src/subscribe-changes.test.ts +368 -0
  89. package/src/subscribe-changes.ts +210 -0
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Prior art for these audit rules: the open-source tools whose checks cover the
3
+ * same condition, credited per rule. See packages/core/src/audit/prior-art.ts for
4
+ * the registry, the relation vocabulary, and why this is credit rather than
5
+ * authority. Kept by hand; the prior-art sweep (scripts/prior-art-sweep.ts) reports
6
+ * when a credited tool's index no longer lists a rule cited here.
7
+ */
8
+ import type { Lineage } from "@intentius/chant/audit/catalog";
9
+
10
+ export const k8sAuditLineage: Record<string, Lineage[]> = {
11
+ FLUX002: [
12
+ { tool: "flux-docs", rule: ".spec.sourceRef", url: "https://fluxcd.io/flux/components/kustomize/kustomizations/#source-reference", relation: "overlaps" },
13
+ ],
14
+ FLUX003: [
15
+ { tool: "flux-docs", rule: ".spec.dependsOn", url: "https://fluxcd.io/flux/components/kustomize/kustomizations/#dependencies", relation: "overlaps" },
16
+ ],
17
+ WK8005: [
18
+ { tool: "polaris", rule: "sensitiveContainerEnvVar", url: "https://polaris.docs.fairwinds.com/checks/security/", relation: "overlaps" },
19
+ { tool: "kube-linter", rule: "env-var-secret", url: "https://github.com/stackrox/kube-linter/blob/main/docs/generated/checks.md#env-var-secret", relation: "overlaps" },
20
+ ],
21
+ WK8006: [
22
+ { tool: "kube-linter", rule: "latest-tag", url: "https://github.com/stackrox/kube-linter/blob/main/docs/generated/checks.md#latest-tag", relation: "equivalent" },
23
+ { tool: "polaris", rule: "tagNotSpecified", url: "https://polaris.docs.fairwinds.com/checks/reliability/", relation: "equivalent" },
24
+ { tool: "checkov", rule: "CKV_K8S_14", url: "https://www.checkov.io/5.Policy%20Index/kubernetes.html", relation: "equivalent" },
25
+ ],
26
+ WK8041: [
27
+ { tool: "polaris", rule: "sensitiveContainerEnvVar", url: "https://polaris.docs.fairwinds.com/checks/security/", relation: "overlaps" },
28
+ { tool: "kube-linter", rule: "env-var-secret", url: "https://github.com/stackrox/kube-linter/blob/main/docs/generated/checks.md#env-var-secret", relation: "overlaps" },
29
+ { tool: "datree", rule: "prevent-exposed-secrets-aws", url: "https://hub.datree.io/built-in-rules/prevent-exposed-secrets-aws", relation: "overlaps" },
30
+ ],
31
+ WK8042: [
32
+ { tool: "polaris", rule: "sensitiveConfigmapContent", url: "https://polaris.docs.fairwinds.com/checks/security/", relation: "overlaps" },
33
+ { tool: "datree", rule: "prevent-exposed-secrets-privatekey", url: "https://hub.datree.io/built-in-rules/prevent-exposed-secrets-privatekey", relation: "overlaps" },
34
+ ],
35
+ WK8101: [
36
+ { tool: "kube-linter", rule: "mismatching-selector", url: "https://github.com/stackrox/kube-linter/blob/main/docs/generated/checks.md#mismatching-selector", relation: "equivalent" },
37
+ { tool: "kube-score", rule: "deployment-pod-selector-labels-match-template-metadata-labels", url: "https://github.com/zegl/kube-score/blob/master/README_CHECKS.md", relation: "equivalent" },
38
+ ],
39
+ WK8105: [
40
+ { tool: "polaris", rule: "pullPolicyNotAlways", url: "https://polaris.docs.fairwinds.com/checks/reliability/", relation: "overlaps" },
41
+ { tool: "kube-score", rule: "container-image-pull-policy", url: "https://github.com/zegl/kube-score/blob/master/README_CHECKS.md", relation: "overlaps" },
42
+ { tool: "checkov", rule: "CKV_K8S_15", url: "https://www.checkov.io/5.Policy%20Index/kubernetes.html", relation: "overlaps" },
43
+ ],
44
+ WK8201: [
45
+ { tool: "polaris", rule: "cpuLimitsMissing", url: "https://polaris.docs.fairwinds.com/checks/efficiency/", relation: "overlaps" },
46
+ { tool: "checkov", rule: "CKV_K8S_13", url: "https://www.checkov.io/5.Policy%20Index/kubernetes.html", relation: "overlaps" },
47
+ { tool: "kube-score", rule: "container-resources", url: "https://github.com/zegl/kube-score/blob/master/README_CHECKS.md", relation: "overlaps" },
48
+ ],
49
+ WK8202: [
50
+ { tool: "kube-linter", rule: "privileged-container", url: "https://github.com/stackrox/kube-linter/blob/main/docs/generated/checks.md#privileged-container", relation: "equivalent" },
51
+ { tool: "polaris", rule: "runAsPrivileged", url: "https://polaris.docs.fairwinds.com/checks/security/", relation: "equivalent" },
52
+ { tool: "checkov", rule: "CKV_K8S_16", url: "https://www.checkov.io/5.Policy%20Index/kubernetes.html", relation: "equivalent" },
53
+ ],
54
+ WK8203: [
55
+ { tool: "kube-linter", rule: "no-read-only-root-fs", url: "https://github.com/stackrox/kube-linter/blob/main/docs/generated/checks.md#no-read-only-root-fs", relation: "equivalent" },
56
+ { tool: "polaris", rule: "notReadOnlyRootFilesystem", url: "https://polaris.docs.fairwinds.com/checks/security/", relation: "equivalent" },
57
+ { tool: "checkov", rule: "CKV_K8S_22", url: "https://www.checkov.io/5.Policy%20Index/kubernetes.html", relation: "equivalent" },
58
+ ],
59
+ WK8204: [
60
+ { tool: "polaris", rule: "runAsRootAllowed", url: "https://polaris.docs.fairwinds.com/checks/security/", relation: "equivalent" },
61
+ { tool: "checkov", rule: "CKV_K8S_23", url: "https://www.checkov.io/5.Policy%20Index/kubernetes.html", relation: "equivalent" },
62
+ { tool: "kube-linter", rule: "run-as-non-root", url: "https://github.com/stackrox/kube-linter/blob/main/docs/generated/checks.md#run-as-non-root", relation: "overlaps" },
63
+ ],
64
+ WK8205: [
65
+ { tool: "kubesec", rule: "containers[] .securityContext .capabilities .drop | index(\"ALL\")", url: "https://kubesec.io/basics/containers-securitycontext-capabilities-drop-index-all/", relation: "equivalent" },
66
+ { tool: "kics", rule: "No Drop Capabilities for Containers", url: "https://docs.kics.io/latest/queries/kubernetes-queries/268ca686-7fb7-4ae9-b129-955a2a89064e/", relation: "overlaps" },
67
+ { tool: "kube-linter", rule: "drop-net-raw-capability", url: "https://github.com/stackrox/kube-linter/blob/main/docs/generated/checks.md#drop-net-raw-capability", relation: "overlaps" },
68
+ ],
69
+ WK8207: [
70
+ { tool: "kube-linter", rule: "host-network", url: "https://github.com/stackrox/kube-linter/blob/main/docs/generated/checks.md#host-network", relation: "equivalent" },
71
+ { tool: "polaris", rule: "hostNetworkSet", url: "https://polaris.docs.fairwinds.com/checks/security/", relation: "equivalent" },
72
+ { tool: "checkov", rule: "CKV_K8S_19", url: "https://www.checkov.io/5.Policy%20Index/kubernetes.html", relation: "equivalent" },
73
+ ],
74
+ WK8208: [
75
+ { tool: "kube-linter", rule: "host-pid", url: "https://github.com/stackrox/kube-linter/blob/main/docs/generated/checks.md#host-pid", relation: "equivalent" },
76
+ { tool: "polaris", rule: "hostPIDSet", url: "https://polaris.docs.fairwinds.com/checks/security/", relation: "equivalent" },
77
+ { tool: "checkov", rule: "CKV_K8S_17", url: "https://www.checkov.io/5.Policy%20Index/kubernetes.html", relation: "equivalent" },
78
+ ],
79
+ WK8209: [
80
+ { tool: "kube-linter", rule: "host-ipc", url: "https://github.com/stackrox/kube-linter/blob/main/docs/generated/checks.md#host-ipc", relation: "equivalent" },
81
+ { tool: "polaris", rule: "hostIPCSet", url: "https://polaris.docs.fairwinds.com/checks/security/", relation: "equivalent" },
82
+ { tool: "checkov", rule: "CKV_K8S_18", url: "https://www.checkov.io/5.Policy%20Index/kubernetes.html", relation: "equivalent" },
83
+ ],
84
+ WK8301: [
85
+ { tool: "kube-score", rule: "pod-probes", url: "https://github.com/zegl/kube-score/blob/master/README_CHECKS.md", relation: "overlaps" },
86
+ { tool: "polaris", rule: "readinessProbeMissing", url: "https://polaris.docs.fairwinds.com/checks/reliability/", relation: "overlaps" },
87
+ { tool: "checkov", rule: "CKV_K8S_8", url: "https://www.checkov.io/5.Policy%20Index/kubernetes.html", relation: "overlaps" },
88
+ ],
89
+ WK8302: [
90
+ { tool: "polaris", rule: "deploymentMissingReplicas", url: "https://polaris.docs.fairwinds.com/checks/reliability/", relation: "equivalent" },
91
+ { tool: "kube-score", rule: "deployment-replicas", url: "https://github.com/zegl/kube-score/blob/master/README_CHECKS.md", relation: "equivalent" },
92
+ { tool: "datree", rule: "ensure-minimum-two-replicas", url: "https://hub.datree.io/built-in-rules/ensure-minimum-two-replicas", relation: "equivalent" },
93
+ ],
94
+ WK8303: [
95
+ { tool: "kube-score", rule: "deployment-has-poddisruptionbudget", url: "https://github.com/zegl/kube-score/blob/master/README_CHECKS.md", relation: "overlaps" },
96
+ { tool: "polaris", rule: "missingPodDisruptionBudget", url: "https://polaris.docs.fairwinds.com/checks/reliability/", relation: "overlaps" },
97
+ { tool: "kics", rule: "Deployment Without PodDisruptionBudget", url: "https://docs.kics.io/latest/queries/kubernetes-queries/b23e9b98-0cb6-4fc9-b257-1f3270442678/", relation: "overlaps" },
98
+ ],
99
+ WK8304: [
100
+ { tool: "polaris", rule: "tlsSettingsMissing", url: "https://polaris.docs.fairwinds.com/checks/security/", relation: "overlaps" },
101
+ ],
102
+ WK8305: [
103
+ { tool: "kube-linter", rule: "dangling-ingress", url: "https://github.com/stackrox/kube-linter/blob/main/docs/generated/checks.md#dangling-ingress", relation: "overlaps" },
104
+ { tool: "kube-score", rule: "ingress-targets-service", url: "https://github.com/zegl/kube-score/blob/master/README_CHECKS.md", relation: "overlaps" },
105
+ ],
106
+ WK8405: [
107
+ { tool: "kube-score", rule: "deployment-has-poddisruptionbudget", url: "https://github.com/zegl/kube-score/blob/master/README_CHECKS.md", relation: "overlaps" },
108
+ { tool: "polaris", rule: "missingPodDisruptionBudget", url: "https://polaris.docs.fairwinds.com/checks/reliability/", relation: "overlaps" },
109
+ ],
110
+ WK8406: [
111
+ { tool: "checkov", rule: "CKV_K8S_13", url: "https://www.checkov.io/5.Policy%20Index/kubernetes.html", relation: "overlaps" },
112
+ { tool: "kube-score", rule: "container-resources", url: "https://github.com/zegl/kube-score/blob/master/README_CHECKS.md", relation: "overlaps" },
113
+ { tool: "kube-linter", rule: "unset-memory-requirements", url: "https://github.com/stackrox/kube-linter/blob/main/docs/generated/checks.md#unset-memory-requirements", relation: "overlaps" },
114
+ ],
115
+ WK8501: [
116
+ { tool: "kube-linter", rule: "schema-validation", url: "https://github.com/stackrox/kube-linter/blob/main/docs/generated/checks.md#schema-validation", relation: "overlaps" },
117
+ ],
118
+ WK8502: [
119
+ { tool: "kube-linter", rule: "schema-validation", url: "https://github.com/stackrox/kube-linter/blob/main/docs/generated/checks.md#schema-validation", relation: "overlaps" },
120
+ ],
121
+ WK8503: [
122
+ { tool: "kube-linter", rule: "env-value-from", url: "https://github.com/stackrox/kube-linter/blob/main/docs/generated/checks.md#env-value-from", relation: "overlaps" },
123
+ ],
124
+ WK8505: [
125
+ { tool: "flux-docs", rule: ".spec.decryption", url: "https://fluxcd.io/flux/components/kustomize/kustomizations/#decryption", relation: "overlaps" },
126
+ ],
127
+ };
@@ -15,7 +15,9 @@
15
15
  * invocations, so "no Kustomization in THIS build sets spec.decryption" can
16
16
  * mean "wired up in the other build" as easily as "forgotten" — the design
17
17
  * doc's §4 explains why no path-to-build-target join exists to tell those
18
- * apart. Promote to error if that join ever becomes available.
18
+ * apart. Single-build-root scoped, tracked by chant #1939 as a candidate for
19
+ * an opt-in project-level mode; promote to error if that join ever becomes
20
+ * available.
19
21
  *
20
22
  * Fires only on claims that actually resolved (`problems.length === 0`, via
21
23
  * `resolveEncryptedSecretClaims`), not on every raw declaration. An
@@ -39,7 +41,7 @@ function fluxKustomizations(manifests: K8sManifest[]): K8sManifest[] {
39
41
  export const wk8505: PostSynthCheck = {
40
42
  id: "WK8505",
41
43
  description:
42
- "committed-encrypted secret with no Flux decryption wiring — add decryption: 'sops' to the FluxAppFor reconciling the path that carries it",
44
+ "committed-encrypted secret with no Flux decryption wiring — add decryption: 'sops' to the FluxAppFor reconciling the path that carries it. Sees one build root at a time, so it goes silent when that Kustomization lives in a different build root (chant #1939).",
43
45
 
44
46
  check(ctx: PostSynthContext): PostSynthDiagnostic[] {
45
47
  const kustomizations = fluxKustomizations(allManifests(ctx));
@@ -5,9 +5,9 @@ import {
5
5
  type ArgoAppStatus,
6
6
  type ArgoStatusFetcher,
7
7
  } from "./argo";
8
- // Activity profiles live centrally in the temporal lexicon (loadProfiles reads
9
- // them there); argoSync marks ArgoSyncFailedError non-retryable for this activity.
10
- import { TEMPORAL_ACTIVITY_PROFILES } from "@intentius/chant-lexicon-temporal/config";
8
+ // Activity profiles live centrally in core (loadProfiles serves this table);
9
+ // argoSync marks ArgoSyncFailedError non-retryable for this activity.
10
+ import { ACTIVITY_PROFILES } from "@intentius/chant/op/activity-profiles";
11
11
 
12
12
  /** A fetcher that returns a scripted sequence of statuses, repeating the last. */
13
13
  function scriptedFetcher(sequence: ArgoAppStatus[]): ArgoStatusFetcher {
@@ -66,14 +66,13 @@ describe("waitForArgoSync", () => {
66
66
  });
67
67
 
68
68
  describe("argoSync profile", () => {
69
- test("is exported with a long timeout and 60s heartbeat", () => {
70
- const p = TEMPORAL_ACTIVITY_PROFILES.argoSync;
71
- expect(p.startToCloseTimeout).toBe("30m");
72
- expect(p.heartbeatTimeout).toBe("60s");
69
+ test("is exported with a long timeout", () => {
70
+ const p = ACTIVITY_PROFILES.argoSync;
71
+ expect(p.timeout).toBe("30m");
73
72
  });
74
73
 
75
74
  test("treats ArgoSyncFailedError as non-retryable", () => {
76
- expect(TEMPORAL_ACTIVITY_PROFILES.argoSync.retry?.nonRetryableErrorTypes).toContain(
75
+ expect(ACTIVITY_PROFILES.argoSync.retry?.nonRetryableErrorTypes).toContain(
77
76
  "ArgoSyncFailedError",
78
77
  );
79
78
  });
@@ -16,8 +16,8 @@ const execAsync = promisify(exec);
16
16
  * `ResourceFetcher` for `waitForReady`.
17
17
  *
18
18
  * It stays **dependency-light** — primitives-only signature (app name /
19
- * namespace / server), no generated Argo CRD types — so a Temporal worker loads
20
- * it cheaply.
19
+ * namespace / server), no generated Argo CRD types — so importing the lexicon's
20
+ * activity module at run time stays cheap.
21
21
  */
22
22
 
23
23
  export interface WaitForArgoSyncArgs {
@@ -36,7 +36,7 @@ export interface WaitForArgoSyncArgs {
36
36
  insecure?: boolean;
37
37
  /** kubectl context (used when `server` is not set). */
38
38
  context?: string;
39
- /** Poll interval in ms (default 15000). Heartbeats every poll. */
39
+ /** Poll interval in ms (default 15000). */
40
40
  intervalMs?: number;
41
41
  }
42
42
 
@@ -113,7 +113,7 @@ export const defaultArgoStatusFetcher: ArgoStatusFetcher = (args, signal) =>
113
113
  * `ArgoSyncFailedError` if it reaches a terminal unhealthy state (Degraded /
114
114
  * Missing).
115
115
  *
116
- * Delegates the poll loop, heartbeat, and ready/terminal evaluation to the
116
+ * Delegates the poll loop and the ready/terminal evaluation to the
117
117
  * generic `waitForReady` using the shared `argoproj.io/Application` readiness
118
118
  * spec. The Argo `ArgoStatusFetcher` is adapted into a `ResourceFetcher` that
119
119
  * shapes `{health, sync}` into the `status.health.status` / `status.sync.status`
@@ -1,21 +1,23 @@
1
1
  /**
2
2
  * k8s Op activities — resolved by the core activity registry when a project's
3
- * `chant.config.ts` lists the `k8s` lexicon. Relocated from the temporal lexicon
3
+ * `chant.config.ts` lists the `k8s` lexicon. Relocated from the hosting lexicon
4
4
  * (#809) so Kubernetes-facing imperative activities live with their product:
5
5
  * - kubectlApply — server-side apply a rendered manifest (and, since chant
6
6
  * #1075, prune chant-owned objects it no longer declares; `applyManifest`
7
- * is the same work with a report of what it did, which is what the
8
- * Temporal lexicon's `nativeApply` dispatcher calls for a kubectl target)
7
+ * is the same work with a report of what it did, which is what core's
8
+ * `nativeApply` dispatcher calls for a kubectl target)
9
9
  * - waitForArgoSync — block until an Argo CD Application is Healthy && Synced
10
10
  *
11
11
  * k3dUp / k3dDown moved again, to the k3d lexicon (chant #1410) — a lexicon
12
12
  * owns its own product's activities, and k3d is its own product now. Projects
13
13
  * using them list `k3d` in `lexicons`; loadActivities(["k3d"]) provides them.
14
14
  *
15
- * The step builders (kubectlApply, k3dUp, k3dDown) stay in core, re-exported from
16
- * the temporal Op-authoring barrel like the other core builders. Each activity is
17
- * dependency-light — it shells out to a CLI and does not import the k8s declarable
18
- * surface — so a Temporal worker loads it cheaply.
15
+ * The step builders (kubectlApply, k3dUp, k3dDown) stay in core and reach
16
+ * authors through `@intentius/chant/op` like the other core builders. Each
17
+ * activity is dependency-light — it shells out to a CLI and does not import the
18
+ * k8s declarable surface — so `loadActivities`
19
+ * (`packages/core/src/op/activity-registry.ts`), which imports this module at
20
+ * run time, pulls in nothing expensive.
19
21
  */
20
22
  export { kubectlApply, applyManifest, readManifestDocuments } from "./kubectl";
21
23
  export type { KubectlApplyArgs, ApplyManifestResult, AppliedRef, ApplyDeleteMode } from "./kubectl";
@@ -61,3 +63,17 @@ export type {
61
63
  // truth — see `lexicons/k8s/src/config.ts` for the config shape.
62
64
  export { resolveClusterTarget, ClusterBindingMismatchError } from "@intentius/chant/kubectl-context";
63
65
  export type { ResolvedClusterTarget, K8sClusterProfile, K8sConfigShape } from "@intentius/chant/kubectl-context";
66
+
67
+ // Effect-receipt activities (#2074): core's receipt seam (#1834) bound to
68
+ // this lexicon's ConfigMap-backed store (../../receipt-store.ts), the same
69
+ // way the aws lexicon binds its SSM store (#1835). Re-exported individually:
70
+ // `receiptRead`/`receiptWrite` serve the `effect()` step's
71
+ // read-compare-run-write, `receiptStaleness` serves WatchOp's read-only
72
+ // staleness reporting.
73
+ import { receiptActivities } from "@intentius/chant/op/receipt-store";
74
+ import { k8sReceiptStore } from "../../receipt-store";
75
+
76
+ const boundReceiptActivities = receiptActivities(k8sReceiptStore());
77
+ export const receiptRead = boundReceiptActivities.receiptRead;
78
+ export const receiptWrite = boundReceiptActivities.receiptWrite;
79
+ export const receiptStaleness = boundReceiptActivities.receiptStaleness;
@@ -1,10 +1,11 @@
1
1
  /**
2
2
  * `kubectlApply` over the typed API client (chant #1074, #1075).
3
3
  *
4
- * The activity contract is what Temporal workers register, so the shape of the
5
- * arguments and the `Promise<void>` return are asserted alongside the new
6
- * behavior. Nothing here spawns a process or reads an ambient kubeconfig,
7
- * which is the acceptance criterion: a worker image needs no `kubectl` binary.
4
+ * The activity contract is what an Op step names and core's registry resolves,
5
+ * so the shape of the arguments and the `Promise<void>` return are asserted
6
+ * alongside the new behavior. Nothing here spawns a process or reads an ambient
7
+ * kubeconfig, which is the acceptance criterion: the image a run happens in
8
+ * needs no `kubectl` binary.
8
9
  *
9
10
  * chant #1075 adds two things to assert: that the field manager is derived
10
11
  * from the project's `ownership.stack` rather than hardcoded, and that the
@@ -493,6 +494,44 @@ describe("the ownership-scoped prune (chant #1075)", () => {
493
494
  expect(deletes).toEqual(["/api/v1/namespaces/prod/secrets/stale-config"]);
494
495
  });
495
496
 
497
+ test("an effect receipt ConfigMap is excluded from the prunable set and reported retained (#2074)", async () => {
498
+ const cluster = fakeCluster({
499
+ respond: echoApplies,
500
+ objects: {
501
+ [objectKey("apps/v1", "Deployment", "web", "prod")]: ownedObject("apps/v1", "Deployment", "web", "prod"),
502
+ [objectKey("v1", "ConfigMap", "chant-receipt.demo.prod.db-seed", "prod")]: ownedObject(
503
+ "v1",
504
+ "ConfigMap",
505
+ "chant-receipt.demo.prod.db-seed",
506
+ "prod",
507
+ {
508
+ metadata: {
509
+ labels: {
510
+ "app.kubernetes.io/managed-by": "chant",
511
+ "chant.intentius.io/effect-receipt": "db-seed",
512
+ },
513
+ },
514
+ },
515
+ ),
516
+ [objectKey("v1", "ConfigMap", "stale-config", "prod")]: ownedObject("v1", "ConfigMap", "stale-config", "prod"),
517
+ },
518
+ });
519
+ const result = await applyManifest(
520
+ { manifest: manifest(), deleteMode: "owned-only" },
521
+ undefined,
522
+ cluster.connector,
523
+ );
524
+
525
+ expect(result.retained).toEqual([
526
+ { apiVersion: "v1", kind: "ConfigMap", name: "chant-receipt.demo.prod.db-seed", namespace: "prod" },
527
+ ]);
528
+ expect(result.pruned).toEqual([
529
+ { apiVersion: "v1", kind: "ConfigMap", name: "stale-config", namespace: "prod" },
530
+ ]);
531
+ const deletes = cluster.layer.requests.filter((r) => r.method === "DELETE").map((r) => r.path);
532
+ expect(deletes).toEqual(["/api/v1/namespaces/prod/configmaps/stale-config"]);
533
+ });
534
+
496
535
  test("an object without chant's marker is never a candidate", async () => {
497
536
  const cluster = clusterWithOrphans();
498
537
  await applyManifest({ manifest: manifest(), deleteMode: "owned-only" }, undefined, cluster.connector);
@@ -3,9 +3,9 @@
3
3
  *
4
4
  * chant #1074 moved this off `kubectl apply -f`. The activity contract is
5
5
  * unchanged (a manifest path, an optional context, `Promise<void>`, the
6
- * `longInfra` profile's 15s heartbeat) because Temporal workers register it by
7
- * that signature; what changed is underneath. The name is kept for the same
8
- * reason.
6
+ * `longInfra` profile) because an Op step names the activity by its export
7
+ * name and core's registry resolves it there; what changed is underneath. The
8
+ * name is kept for the same reason.
9
9
  *
10
10
  * chant #1075 finished the job on two axes:
11
11
  *
@@ -34,7 +34,6 @@
34
34
  import { readFileSync, readdirSync, statSync } from "node:fs";
35
35
  import { join } from "node:path";
36
36
  import { loadAll } from "js-yaml";
37
- import { safeHeartbeat } from "@intentius/chant/op";
38
37
  import { loadChantConfig, resolveOwnershipStack } from "@intentius/chant/config";
39
38
  import {
40
39
  hasOwnershipMarker,
@@ -46,19 +45,20 @@ import { defaultK8sConnector, type K8sConnector } from "../../api/connect";
46
45
  import { operationFor } from "../../api/operation-surface";
47
46
  import { DEFAULT_IMPORT_TYPES } from "../../api/sweep-types";
48
47
  import { isGeneratedOnce } from "../../secret-labels";
48
+ import { isEffectReceiptObject } from "../../effect-receipt-row";
49
49
 
50
50
  /**
51
51
  * How the apply treats chant-owned objects that are no longer declared. The
52
52
  * same three values `nativeApply` has always taken; `gated` differs from
53
- * `owned-only` only in that the workflow pauses for approval first, which is
54
- * the composite's business, not this activity's.
53
+ * `owned-only` only in that an approval gate runs before this activity does,
54
+ * which is the composite's business, not this activity's.
55
55
  */
56
56
  export type ApplyDeleteMode = "never" | "owned-only" | "gated";
57
57
 
58
58
  export interface KubectlApplyArgs {
59
59
  /**
60
60
  * Path to a manifest file, or a directory of them. With `documents` given,
61
- * this becomes only the human-facing label the heartbeats and logs carry
61
+ * this becomes only the human-facing label the step's log lines carry
62
62
  * (e.g. `kustomize:<dir>`), and nothing is read from disk.
63
63
  */
64
64
  manifest: string;
@@ -310,74 +310,61 @@ export async function applyManifest(
310
310
  ): Promise<ApplyManifestResult> {
311
311
  const documents = args.documents ?? readManifestDocuments(args.manifest);
312
312
  const { fieldManager, stack } = await resolveApplyIdentity(args);
313
- const heartbeatInterval = setInterval(() => {
314
- safeHeartbeat({ step: "kubectl apply", manifest: args.manifest });
315
- }, 15_000);
316
-
317
- try {
318
- const { client } = await connect({
319
- ...(args.environment !== undefined ? { environment: args.environment } : {}),
320
- ...(args.context !== undefined ? { context: args.context } : {}),
321
- ...(args.cwd !== undefined ? { cwd: args.cwd } : {}),
322
- });
313
+ const { client } = await connect({
314
+ ...(args.environment !== undefined ? { environment: args.environment } : {}),
315
+ ...(args.context !== undefined ? { context: args.context } : {}),
316
+ ...(args.cwd !== undefined ? { cwd: args.cwd } : {}),
317
+ });
323
318
 
324
- const applied: AppliedRef[] = [];
325
- for (const document of documents) {
326
- const stamped = stampOwnership(document as K8sObject, stack);
327
- let result: K8sObject;
328
- try {
329
- result = await client.apply(stamped, {
330
- fieldManager,
331
- force: args.force ?? false,
332
- dryRun: args.dryRun,
333
- signal,
334
- });
335
- } catch (err) {
336
- // "chant never forces a conflict on its own" is about taking fields
337
- // from ANOTHER tool. A conflict where every contested field is owned
338
- // by another `chant:*` manager is chant contesting itself — the
339
- // ownership-stack → unit-stack label migration, or a renamed deploy
340
- // unit — and refusing that forever would strand every estate applied
341
- // before the rename with no non-force path back. Retake those fields
342
- // deliberately, once, and only when no foreign manager is involved.
343
- if (!isChantSelfConflict(err)) throw err;
344
- result = await client.apply(stamped, { fieldManager, force: true, dryRun: args.dryRun, signal });
345
- }
346
- const ref: AppliedRef = {
347
- apiVersion: String(result.apiVersion ?? document.apiVersion ?? ""),
348
- kind: String(result.kind ?? document.kind ?? ""),
349
- name: String(result.metadata?.name ?? ""),
350
- ...(result.metadata?.namespace !== undefined
351
- ? { namespace: String(result.metadata.namespace) }
352
- : {}),
353
- };
354
- applied.push(ref);
355
- safeHeartbeat({
356
- step: "kubectl apply",
357
- manifest: args.manifest,
358
- applied: `${ref.kind}/${ref.name}`,
319
+ const applied: AppliedRef[] = [];
320
+ for (const document of documents) {
321
+ const stamped = stampOwnership(document as K8sObject, stack);
322
+ let result: K8sObject;
323
+ try {
324
+ result = await client.apply(stamped, {
325
+ fieldManager,
326
+ force: args.force ?? false,
327
+ dryRun: args.dryRun,
328
+ signal,
359
329
  });
360
- console.log(`${ref.apiVersion} ${ref.kind}/${ref.name} applied${args.dryRun ? " (dry run — nothing persisted)" : ""}`);
330
+ } catch (err) {
331
+ // "chant never forces a conflict on its own" is about taking fields
332
+ // from ANOTHER tool. A conflict where every contested field is owned
333
+ // by another `chant:*` manager is chant contesting itself — the
334
+ // ownership-stack → unit-stack label migration, or a renamed deploy
335
+ // unit — and refusing that forever would strand every estate applied
336
+ // before the rename with no non-force path back. Retake those fields
337
+ // deliberately, once, and only when no foreign manager is involved.
338
+ if (!isChantSelfConflict(err)) throw err;
339
+ result = await client.apply(stamped, { fieldManager, force: true, dryRun: args.dryRun, signal });
361
340
  }
341
+ const ref: AppliedRef = {
342
+ apiVersion: String(result.apiVersion ?? document.apiVersion ?? ""),
343
+ kind: String(result.kind ?? document.kind ?? ""),
344
+ name: String(result.metadata?.name ?? ""),
345
+ ...(result.metadata?.namespace !== undefined
346
+ ? { namespace: String(result.metadata.namespace) }
347
+ : {}),
348
+ };
349
+ applied.push(ref);
350
+ console.log(`${ref.apiVersion} ${ref.kind}/${ref.name} applied${args.dryRun ? " (dry run — nothing persisted)" : ""}`);
351
+ }
362
352
 
363
- const deleteMode = args.deleteMode ?? "never";
364
- const { pruned, retained } =
365
- deleteMode === "never" || args.dryRun
366
- ? { pruned: [], retained: [] }
367
- : await pruneOrphans(client, applied, {
368
- ...(stack !== undefined ? { stack } : {}),
369
- signal,
370
- });
353
+ const deleteMode = args.deleteMode ?? "never";
354
+ const { pruned, retained } =
355
+ deleteMode === "never" || args.dryRun
356
+ ? { pruned: [], retained: [] }
357
+ : await pruneOrphans(client, applied, {
358
+ ...(stack !== undefined ? { stack } : {}),
359
+ signal,
360
+ });
371
361
 
372
- return { fieldManager, applied, pruned, retained };
373
- } finally {
374
- clearInterval(heartbeatInterval);
375
- }
362
+ return { fieldManager, applied, pruned, retained };
376
363
  }
377
364
 
378
365
  /**
379
366
  * Apply every document in `args.manifest`.
380
- * Uses longInfra profile 20m timeout, heartbeat every 15s.
367
+ * Uses the longInfra profile: 20m timeout, three attempts backing off from 30s.
381
368
  */
382
369
  export async function kubectlApply(
383
370
  args: KubectlApplyArgs,
@@ -517,6 +504,20 @@ async function pruneOrphans(
517
504
  );
518
505
  continue;
519
506
  }
507
+ // An effect receipt never enters the prunable set either (#2074, epic
508
+ // #1703 decision 3). It is chant-owned and is never in ANY apply set,
509
+ // the `effect()` step being its sole writer, which is exactly the shape
510
+ // this sweep deletes. Pruning one would tell the next plan that an effect
511
+ // never ran and re-fire it, or worse, be re-stamped by nothing and leave
512
+ // the estate re-running a migration on every apply.
513
+ if (target.kind === "ConfigMap" && isEffectReceiptObject(item.metadata?.labels)) {
514
+ retained.push(ref);
515
+ console.log(
516
+ `${ref.apiVersion} ${ref.kind}/${ref.name} retained (effect receipt: owned and never declared ` +
517
+ `in an apply set, but never pruned; the effect() step is its sole writer)`,
518
+ );
519
+ continue;
520
+ }
520
521
  candidates.push(ref);
521
522
  }
522
523
 
@@ -524,7 +525,6 @@ async function pruneOrphans(
524
525
  for (const ref of candidates) {
525
526
  await client.delete(ref, { ...(options.signal ? { signal: options.signal } : {}) });
526
527
  pruned.push(ref);
527
- safeHeartbeat({ step: "prune", pruned: `${ref.kind}/${ref.name}` });
528
528
  console.log(`${ref.apiVersion} ${ref.kind}/${ref.name} pruned (chant-owned, no longer declared)`);
529
529
  }
530
530
  return { pruned, retained };
@@ -11,7 +11,7 @@ import {
11
11
  type ReadinessSpec,
12
12
  } from "./wait-for-ready";
13
13
  // The k8sWait profile marks ReadinessFailedError non-retryable for this activity.
14
- import { TEMPORAL_ACTIVITY_PROFILES } from "@intentius/chant-lexicon-temporal/config";
14
+ import { ACTIVITY_PROFILES } from "@intentius/chant/op/activity-profiles";
15
15
  import { fakeCluster, objectKey } from "../../api/fake-cluster";
16
16
 
17
17
  /** A fetcher returning a scripted sequence of objects, repeating the last. */
@@ -156,7 +156,7 @@ describe("waitForReady", () => {
156
156
  });
157
157
 
158
158
  test("k8sWait marks ReadinessFailedError non-retryable", () => {
159
- expect(TEMPORAL_ACTIVITY_PROFILES.k8sWait.retry?.nonRetryableErrorTypes).toContain("ReadinessFailedError");
159
+ expect(ACTIVITY_PROFILES.k8sWait.retry?.nonRetryableErrorTypes).toContain("ReadinessFailedError");
160
160
  });
161
161
 
162
162
  test("explicit spec wins over the registry", async () => {
@@ -1,4 +1,4 @@
1
- import { safeHeartbeat, sleep } from "@intentius/chant/op";
1
+ import { sleep } from "@intentius/chant/op";
2
2
  import { defaultK8sConnector, type K8sConnector } from "../../api/connect";
3
3
 
4
4
  /**
@@ -6,8 +6,8 @@ import { defaultK8sConnector, type K8sConnector } from "../../api/connect";
6
6
  * ready, driven by a data-only **readiness spec** rather than per-CRD code.
7
7
  *
8
8
  * Like `waitForArgoSync`, this activity is intentionally **dependency-light**:
9
- * its signature is primitives + a plain readiness spec, so a Temporal worker
10
- * loads it without importing the generated CRD declarable surface. It reads the
9
+ * its signature is primitives + a plain readiness spec, so the activity module
10
+ * loads without pulling in the generated CRD declarable surface. It reads the
11
11
  * resource and evaluates the spec's predicates. It generalizes the bespoke
12
12
  * `waitForArgoSync` / `waitForStack` waits — see #365.
13
13
  *
@@ -271,7 +271,7 @@ export interface WaitForReadyArgs {
271
271
  group?: string;
272
272
  /** Explicit readiness spec — wins over the registry/default. */
273
273
  spec?: ReadinessSpec;
274
- /** Poll interval in ms (default 15000). Heartbeats every poll. */
274
+ /** Poll interval in ms (default 15000). */
275
275
  intervalMs?: number;
276
276
  }
277
277
 
@@ -335,8 +335,9 @@ export const defaultResourceFetcher: ResourceFetcher = (args, signal) => apiReso
335
335
 
336
336
  /**
337
337
  * Poll until the resource satisfies its readiness spec. Throws
338
- * `ReadinessFailedError` on a terminal state. Heartbeats every poll so the
339
- * `k8sWait` profile's 60s heartbeat timeout never trips.
338
+ * `ReadinessFailedError` on a terminal state, which the `k8sWait` profile lists
339
+ * as non-retryable, so a resource that will never become ready fails on the
340
+ * first attempt instead of burning the profile's 15m timeout three times.
340
341
  *
341
342
  * @param fetcher injectable reader (defaults to kubectl). Tests pass a fake to
342
343
  * drive not-ready → ready / terminal transitions.
@@ -355,7 +356,6 @@ export async function waitForReady(
355
356
  attempt++;
356
357
 
357
358
  const obj = await fetcher(args, signal);
358
- safeHeartbeat({ step: "waitForReady", kind: args.kind, name: args.name, attempt });
359
359
 
360
360
  const term = firstTerminal(obj, spec);
361
361
  if (term) {
@@ -18,20 +18,20 @@
18
18
  * earlier step's declared output.
19
19
  *
20
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
21
+ * `@intentius/chant/op`, the lexicon-agnostic single-import barrel) are
22
+ * UNCHANGED and produce byte-identical `ActivityStep` output for the same
23
+ * inputs these are purely additive. Deliberately not swapped into core's
24
+ * barrel: that would make `@intentius/chant` depend on this package (and on
25
+ * helm, for `helmInstall`) at runtime, so a project that never touches
26
+ * Kubernetes would install both just to import anything from `op`. That is
27
+ * exactly the "core stays product-agnostic" property the
28
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.
29
+ * (activities live in per-product lexicons for this reason; the step-builder
30
+ * layer shouldn't reintroduce the coupling from the other direction). An
31
+ * author who wants the typed surface imports it from here —
32
+ * `@intentius/chant-lexicon-k8s` — which a project using `kubectlApply`
33
+ * already depends on; existing `@intentius/chant/op` imports keep working
34
+ * exactly as before, opting in only if the import is changed.
35
35
  */
36
36
 
37
37
  import {