@intentius/chant-lexicon-k8s 0.57.0 → 0.58.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 (47) hide show
  1. package/dist/api/sweep-types.d.ts +1 -2
  2. package/dist/api/sweep-types.d.ts.map +1 -1
  3. package/dist/composites/operator-stack.d.ts +19 -18
  4. package/dist/composites/operator-stack.d.ts.map +1 -1
  5. package/dist/config.d.ts +4 -4
  6. package/dist/deep-observe-hooks.d.ts +4 -4
  7. package/dist/deep-observe.d.ts +23 -16
  8. package/dist/deep-observe.d.ts.map +1 -1
  9. package/dist/export-resources.d.ts +1 -1
  10. package/dist/integrity.json +3 -3
  11. package/dist/lint/audit-catalog.d.ts.map +1 -1
  12. package/dist/lint/audit-lineage.d.ts +10 -0
  13. package/dist/lint/audit-lineage.d.ts.map +1 -0
  14. package/dist/manifest.json +1 -1
  15. package/dist/op/activities/argo.d.ts +4 -4
  16. package/dist/op/activities/argo.d.ts.map +1 -1
  17. package/dist/op/activities/index.d.ts +9 -7
  18. package/dist/op/activities/index.d.ts.map +1 -1
  19. package/dist/op/activities/kubectl.d.ts +7 -7
  20. package/dist/op/activities/kubectl.d.ts.map +1 -1
  21. package/dist/op/activities/wait-for-ready.d.ts +6 -5
  22. package/dist/op/activities/wait-for-ready.d.ts.map +1 -1
  23. package/dist/op/builders.d.ts +13 -13
  24. package/dist/skills/chant-k8s-argo.md +27 -13
  25. package/package.json +3 -3
  26. package/src/api/sweep-types.ts +1 -2
  27. package/src/composites/composites.test.ts +13 -2
  28. package/src/composites/operator-stack.ts +22 -20
  29. package/src/config.ts +4 -4
  30. package/src/crd/crd-sources.ts +4 -4
  31. package/src/deep-observe-hooks.ts +4 -4
  32. package/src/deep-observe.test.ts +219 -46
  33. package/src/deep-observe.ts +28 -21
  34. package/src/export-resources.ts +1 -1
  35. package/src/lint/audit-catalog.ts +5 -1
  36. package/src/lint/audit-lineage.ts +127 -0
  37. package/src/op/activities/argo.test.ts +7 -8
  38. package/src/op/activities/argo.ts +4 -4
  39. package/src/op/activities/index.ts +9 -7
  40. package/src/op/activities/kubectl.test.ts +5 -4
  41. package/src/op/activities/kubectl.ts +52 -67
  42. package/src/op/activities/wait-for-ready.test.ts +2 -2
  43. package/src/op/activities/wait-for-ready.ts +7 -7
  44. package/src/op/builders.ts +13 -13
  45. package/src/plugin.ts +1 -1
  46. package/src/serializer.test.ts +26 -0
  47. package/src/skills/chant-k8s-argo.md +27 -13
@@ -9,10 +9,10 @@
9
9
  *
10
10
  * ## What managedFields decides, and what it does not
11
11
  *
12
- * AWS, Azure and Temporal's rows all prune by a **static, entityType-keyed**
13
- * table: an ARN always looks like an ARN, `provisioningState` is always
14
- * server-populated, a namespace's retention default is always the same
15
- * value. None of that needs the specific live object in hand — it is exactly
12
+ * The AWS and Azure rows both prune by a **static, entityType-keyed** table
13
+ * (`AWS_SERVICE_DEFAULTS`, `AZURE_SERVICE_DEFAULTS`): an ARN always looks like
14
+ * an ARN, `provisioningState` is always server-populated. Neither needs the
15
+ * specific live object in hand — it is exactly
16
16
  * what `./deep-observe-hooks.ts`'s `k8sDeepNormalizationHooks` is, and it
17
17
  * covers Kubernetes' *equivalent* static noise (`status`,
18
18
  * `metadata.{uid,resourceVersion,generation,creationTimestamp}`, a handful of
@@ -31,7 +31,7 @@
31
31
  * was controller noise. It silenced exactly the case the deep read exists
32
32
  * for — `kubectl label deploy web team=platform` is foreign-owned
33
33
  * (`kubectl-edit`) and undeclared, and never appeared in fieldDrift. The
34
- * policy now (recorded on #1202) is:
34
+ * policy now (recorded on #1202, refined by #2160) is:
35
35
  *
36
36
  * 1. **Chant-owned** (`chant`, `chant:<stack>` — chant #1075, matched on the
37
37
  * family so a stack rename does not stop recognizing its own history) is
@@ -41,18 +41,25 @@
41
41
  * write currently holds the field, and a foreign write that overrides a
42
42
  * value chant's manifest asks for is the thing `lifecycle diff --live`
43
43
  * exists to surface. It reports as `changed`.
44
- * 3. **Foreign-owned and undeclared** is reported as `undeclared` drift,
45
- * unless the accepted baseline already carries it at that value. The
46
- * baseline (`--update-baseline`) is the noise valve: an HPA that owns
47
- * `spec.replicas` on a Deployment whose source is silent about replicas
48
- * reports once, gets accepted, and stays quiet until the value moves.
44
+ * 3. **Undeclared** a path outside the declaration's claimed-field set
45
+ * (`@intentius/chant/claimed-fields`) is reported as unclaimed and
46
+ * is not drift at all. Before #2160 it was `undeclared` drift that the
47
+ * accepted baseline had to absorb one path at a time; the declaration
48
+ * answers on the first read instead, so an HPA that owns `spec.replicas`
49
+ * on a Deployment whose source is silent about replicas is quiet with
50
+ * nothing recorded anywhere.
49
51
  *
50
- * So ownership decides nothing about *whether* a path is compared. What it
51
- * contributes is `fieldOwners` (#1189): every reported drift names the
52
- * manager that holds the field live, which is how an operator tells
53
- * `hpa-controller` doing its job from somebody running `kubectl edit`. The
54
- * only fields subtracted because a foreign manager wrote them are the
55
- * well-known system ones on the static allowlist.
52
+ * So ownership decides nothing about *whether* a path is compared, and since
53
+ * #2160 it no longer decides the verdict either the declaration does. What
54
+ * it contributes is `fieldOwners` (#1189), and that is worth more here than
55
+ * anywhere else: on every other substrate a held field can only be reported
56
+ * as "not chant's", while here it is reported as `hpa-controller`'s or
57
+ * `kubectl-edit`'s by name, which is the difference between a controller
58
+ * doing its job and somebody bypassing the pipeline. Core takes the manager
59
+ * where this reader supplies one and falls back to the claim where it does
60
+ * not (a path inside a key-addressed list, which no `fieldsV1` entry names in
61
+ * that form). The only fields subtracted because a foreign manager wrote them
62
+ * are the well-known system ones on the static allowlist.
56
63
  *
57
64
  * ## Resolving a managedFields entry against a live array
58
65
  *
@@ -114,7 +121,7 @@ import { k8sDeepNormalizationHooks } from "./deep-observe-hooks";
114
121
 
115
122
  // Re-exported so a dynamic importer of this module (plugin.ts's
116
123
  // `observeResourcesDeep`, a test) can get the reader and its hooks from one
117
- // place, the same shape AWS/Azure/Temporal's single deep-observe.ts offers.
124
+ // place, the same shape the AWS and Azure deep-observe.ts modules offer.
118
125
  // `plugin.ts`'s own `deepNormalizationHooks` field imports the hooks
119
126
  // separately, directly from `./deep-observe-hooks` — that file has no
120
127
  // dependency on `@intentius/chant-k8s-client`, so it is safe to import
@@ -244,10 +251,10 @@ export async function observeResourcesDeepK8s(
244
251
  hooks: k8sDeepNormalizationHooks,
245
252
  }),
246
253
  // Who owns each path (#1189). Nothing is pruned by ownership any more
247
- // (#1191), so a foreign-owned undeclared field reaches the diff as
248
- // `undeclared` — and naming the manager is the whole question there:
249
- // an operator needs to tell `hpa-controller` doing its job from
250
- // somebody running `kubectl edit`.
254
+ // (#1191), so a foreign-owned undeclared field still reaches the diff
255
+ // as a held field since #2160 — and naming the manager is the whole
256
+ // question there: an operator needs to tell `hpa-controller` doing its
257
+ // job from somebody running `kubectl edit`.
251
258
  ...(sets.owners.size > 0 ? { fieldOwners: Object.fromEntries(sets.owners) } : {}),
252
259
  };
253
260
  } catch (err) {
@@ -28,7 +28,7 @@ import { DEFAULT_IMPORT_TYPES } from "./api/sweep-types";
28
28
  * reachable by naming it with `--selector type=...`. Defined in
29
29
  * `./api/sweep-types.ts` and re-exported here, its original home — chant
30
30
  * #1075's ownership-scoped prune needs the same list without pulling the
31
- * import parser into a Temporal worker.
31
+ * import parser in behind the apply activity.
32
32
  */
33
33
  export { DEFAULT_IMPORT_TYPES };
34
34
 
@@ -2,7 +2,8 @@
2
2
  * The k8s lexicon's chant audit catalog — metadata for its post-synth rules
3
3
  * (WK8 and ARGO Kubernetes-manifest rules). Contributed via k8sPlugin.auditCatalog() (#687).
4
4
  */
5
- import { auditRule, K8S_PSS, K8S_SECRETS, SCORECARD_PINNED, type RuleMeta } from "@intentius/chant/audit/catalog";
5
+ import { auditRule, K8S_PSS, K8S_SECRETS, SCORECARD_PINNED, type RuleMeta, applyLineage } from "@intentius/chant/audit/catalog";
6
+ import { k8sAuditLineage } from "./audit-lineage";
6
7
 
7
8
  export const k8sAuditCatalog: Record<string, RuleMeta> = {
8
9
  ARGO002: auditRule("ARGO002", "merge-worthy", "guidance", "Argo Application references an undeclared AppProject", "Declare the named AppProject or reference an existing project.", { category: "correctness" }),
@@ -46,3 +47,6 @@ export const k8sAuditCatalog: Record<string, RuleMeta> = {
46
47
  WK8504: auditRule("WK8504", "merge-worthy", "guidance", "committed-encrypted secret declaration does not resolve", "Point `file` at the committed ciphertext, make its metadata.name match the declaration, and re-run `sops -e -i` so every data/stringData value is ENC[...].", { authority: [K8S_SECRETS] }),
47
48
  WK8505: auditRule("WK8505", "report-only", "guidance", "committed-encrypted secret with no Flux decryption wiring", "Add decryption: \"sops\" to the FluxAppFor reconciling the path that carries the ciphertext sidecar, or ignore if that Kustomization is declared in a different build.", { category: "correctness" }),
48
49
  };
50
+
51
+ // Prior art credits live beside the rules in ./audit-lineage.ts (see core audit/prior-art.ts).
52
+ applyLineage(k8sAuditCatalog, k8sAuditLineage);
@@ -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
+ };
@@ -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";
@@ -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
@@ -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,
@@ -50,15 +49,15 @@ import { isGeneratedOnce } from "../../secret-labels";
50
49
  /**
51
50
  * How the apply treats chant-owned objects that are no longer declared. The
52
51
  * 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.
52
+ * `owned-only` only in that an approval gate runs before this activity does,
53
+ * which is the composite's business, not this activity's.
55
54
  */
56
55
  export type ApplyDeleteMode = "never" | "owned-only" | "gated";
57
56
 
58
57
  export interface KubectlApplyArgs {
59
58
  /**
60
59
  * 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
60
+ * this becomes only the human-facing label the step's log lines carry
62
61
  * (e.g. `kustomize:<dir>`), and nothing is read from disk.
63
62
  */
64
63
  manifest: string;
@@ -310,74 +309,61 @@ export async function applyManifest(
310
309
  ): Promise<ApplyManifestResult> {
311
310
  const documents = args.documents ?? readManifestDocuments(args.manifest);
312
311
  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
- });
312
+ const { client } = await connect({
313
+ ...(args.environment !== undefined ? { environment: args.environment } : {}),
314
+ ...(args.context !== undefined ? { context: args.context } : {}),
315
+ ...(args.cwd !== undefined ? { cwd: args.cwd } : {}),
316
+ });
323
317
 
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}`,
318
+ const applied: AppliedRef[] = [];
319
+ for (const document of documents) {
320
+ const stamped = stampOwnership(document as K8sObject, stack);
321
+ let result: K8sObject;
322
+ try {
323
+ result = await client.apply(stamped, {
324
+ fieldManager,
325
+ force: args.force ?? false,
326
+ dryRun: args.dryRun,
327
+ signal,
359
328
  });
360
- console.log(`${ref.apiVersion} ${ref.kind}/${ref.name} applied${args.dryRun ? " (dry run — nothing persisted)" : ""}`);
329
+ } catch (err) {
330
+ // "chant never forces a conflict on its own" is about taking fields
331
+ // from ANOTHER tool. A conflict where every contested field is owned
332
+ // by another `chant:*` manager is chant contesting itself — the
333
+ // ownership-stack → unit-stack label migration, or a renamed deploy
334
+ // unit — and refusing that forever would strand every estate applied
335
+ // before the rename with no non-force path back. Retake those fields
336
+ // deliberately, once, and only when no foreign manager is involved.
337
+ if (!isChantSelfConflict(err)) throw err;
338
+ result = await client.apply(stamped, { fieldManager, force: true, dryRun: args.dryRun, signal });
361
339
  }
340
+ const ref: AppliedRef = {
341
+ apiVersion: String(result.apiVersion ?? document.apiVersion ?? ""),
342
+ kind: String(result.kind ?? document.kind ?? ""),
343
+ name: String(result.metadata?.name ?? ""),
344
+ ...(result.metadata?.namespace !== undefined
345
+ ? { namespace: String(result.metadata.namespace) }
346
+ : {}),
347
+ };
348
+ applied.push(ref);
349
+ console.log(`${ref.apiVersion} ${ref.kind}/${ref.name} applied${args.dryRun ? " (dry run — nothing persisted)" : ""}`);
350
+ }
362
351
 
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
- });
352
+ const deleteMode = args.deleteMode ?? "never";
353
+ const { pruned, retained } =
354
+ deleteMode === "never" || args.dryRun
355
+ ? { pruned: [], retained: [] }
356
+ : await pruneOrphans(client, applied, {
357
+ ...(stack !== undefined ? { stack } : {}),
358
+ signal,
359
+ });
371
360
 
372
- return { fieldManager, applied, pruned, retained };
373
- } finally {
374
- clearInterval(heartbeatInterval);
375
- }
361
+ return { fieldManager, applied, pruned, retained };
376
362
  }
377
363
 
378
364
  /**
379
365
  * Apply every document in `args.manifest`.
380
- * Uses longInfra profile 20m timeout, heartbeat every 15s.
366
+ * Uses the longInfra profile: 20m timeout, three attempts backing off from 30s.
381
367
  */
382
368
  export async function kubectlApply(
383
369
  args: KubectlApplyArgs,
@@ -524,7 +510,6 @@ async function pruneOrphans(
524
510
  for (const ref of candidates) {
525
511
  await client.delete(ref, { ...(options.signal ? { signal: options.signal } : {}) });
526
512
  pruned.push(ref);
527
- safeHeartbeat({ step: "prune", pruned: `${ref.kind}/${ref.name}` });
528
513
  console.log(`${ref.apiVersion} ${ref.kind}/${ref.name} pruned (chant-owned, no longer declared)`);
529
514
  }
530
515
  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) {