@intentius/chant-lexicon-cpln 0.53.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 (266) hide show
  1. package/README.md +85 -0
  2. package/dist/api.d.ts +77 -0
  3. package/dist/api.d.ts.map +1 -0
  4. package/dist/codegen/docs-cli.d.ts +3 -0
  5. package/dist/codegen/docs-cli.d.ts.map +1 -0
  6. package/dist/codegen/docs.d.ts +17 -0
  7. package/dist/codegen/docs.d.ts.map +1 -0
  8. package/dist/codegen/generate-cli.d.ts +6 -0
  9. package/dist/codegen/generate-cli.d.ts.map +1 -0
  10. package/dist/codegen/generate-lexicon.d.ts +36 -0
  11. package/dist/codegen/generate-lexicon.d.ts.map +1 -0
  12. package/dist/codegen/generate-typescript.d.ts +11 -0
  13. package/dist/codegen/generate-typescript.d.ts.map +1 -0
  14. package/dist/codegen/generate.d.ts +15 -0
  15. package/dist/codegen/generate.d.ts.map +1 -0
  16. package/dist/codegen/naming.d.ts +30 -0
  17. package/dist/codegen/naming.d.ts.map +1 -0
  18. package/dist/codegen/package.d.ts +17 -0
  19. package/dist/codegen/package.d.ts.map +1 -0
  20. package/dist/composites/cron-job.d.ts +60 -0
  21. package/dist/composites/cron-job.d.ts.map +1 -0
  22. package/dist/composites/gvc-environment.d.ts +47 -0
  23. package/dist/composites/gvc-environment.d.ts.map +1 -0
  24. package/dist/composites/index.d.ts +13 -0
  25. package/dist/composites/index.d.ts.map +1 -0
  26. package/dist/composites/public-domain.d.ts +68 -0
  27. package/dist/composites/public-domain.d.ts.map +1 -0
  28. package/dist/composites/secret-access.d.ts +78 -0
  29. package/dist/composites/secret-access.d.ts.map +1 -0
  30. package/dist/composites/serverless-service.d.ts +78 -0
  31. package/dist/composites/serverless-service.d.ts.map +1 -0
  32. package/dist/composites/stateful-service.d.ts +79 -0
  33. package/dist/composites/stateful-service.d.ts.map +1 -0
  34. package/dist/coverage.d.ts +59 -0
  35. package/dist/coverage.d.ts.map +1 -0
  36. package/dist/describe-resources.d.ts +71 -0
  37. package/dist/describe-resources.d.ts.map +1 -0
  38. package/dist/detect.d.ts +14 -0
  39. package/dist/detect.d.ts.map +1 -0
  40. package/dist/entity-props.d.ts +34 -0
  41. package/dist/entity-props.d.ts.map +1 -0
  42. package/dist/generated/index.d.ts +151 -0
  43. package/dist/generated/index.d.ts.map +1 -0
  44. package/dist/generated/runtime.d.ts +5 -0
  45. package/dist/generated/runtime.d.ts.map +1 -0
  46. package/dist/index.d.ts +8 -0
  47. package/dist/index.d.ts.map +1 -0
  48. package/dist/init-templates.d.ts +16 -0
  49. package/dist/init-templates.d.ts.map +1 -0
  50. package/dist/integrity.json +35 -0
  51. package/dist/kinds.d.ts +70 -0
  52. package/dist/kinds.d.ts.map +1 -0
  53. package/dist/lint/audit-catalog.d.ts +19 -0
  54. package/dist/lint/audit-catalog.d.ts.map +1 -0
  55. package/dist/lint/post-synth/cpl010-unrestricted-outbound.d.ts +12 -0
  56. package/dist/lint/post-synth/cpl010-unrestricted-outbound.d.ts.map +1 -0
  57. package/dist/lint/post-synth/cpl011-internal-firewall-scope.d.ts +11 -0
  58. package/dist/lint/post-synth/cpl011-internal-firewall-scope.d.ts.map +1 -0
  59. package/dist/lint/post-synth/cpl012-no-literal-credential-env.d.ts +11 -0
  60. package/dist/lint/post-synth/cpl012-no-literal-credential-env.d.ts.map +1 -0
  61. package/dist/lint/post-synth/cpl013-qualified-identity-principal.d.ts +11 -0
  62. package/dist/lint/post-synth/cpl013-qualified-identity-principal.d.ts.map +1 -0
  63. package/dist/lint/post-synth/cpl014-qualified-secret-ref.d.ts +14 -0
  64. package/dist/lint/post-synth/cpl014-qualified-secret-ref.d.ts.map +1 -0
  65. package/dist/lint/post-synth/cpl020-serverless-single-port.d.ts +10 -0
  66. package/dist/lint/post-synth/cpl020-serverless-single-port.d.ts.map +1 -0
  67. package/dist/lint/post-synth/cpl021-cron-job-shape.d.ts +11 -0
  68. package/dist/lint/post-synth/cpl021-cron-job-shape.d.ts.map +1 -0
  69. package/dist/lint/post-synth/cpl022-container-ports.d.ts +10 -0
  70. package/dist/lint/post-synth/cpl022-container-ports.d.ts.map +1 -0
  71. package/dist/lint/post-synth/cpl023-container-resources.d.ts +12 -0
  72. package/dist/lint/post-synth/cpl023-container-resources.d.ts.map +1 -0
  73. package/dist/lint/post-synth/cpl024-probe-handler.d.ts +10 -0
  74. package/dist/lint/post-synth/cpl024-probe-handler.d.ts.map +1 -0
  75. package/dist/lint/post-synth/cpl025-autoscaling-shape.d.ts +11 -0
  76. package/dist/lint/post-synth/cpl025-autoscaling-shape.d.ts.map +1 -0
  77. package/dist/lint/post-synth/cpl026-scale-to-zero.d.ts +11 -0
  78. package/dist/lint/post-synth/cpl026-scale-to-zero.d.ts.map +1 -0
  79. package/dist/lint/post-synth/cpl027-capacity-ai-conflict.d.ts +13 -0
  80. package/dist/lint/post-synth/cpl027-capacity-ai-conflict.d.ts.map +1 -0
  81. package/dist/lint/post-synth/cpl028-volume-set.d.ts +3 -0
  82. package/dist/lint/post-synth/cpl028-volume-set.d.ts.map +1 -0
  83. package/dist/lint/post-synth/cpl029-link-targets.d.ts +3 -0
  84. package/dist/lint/post-synth/cpl029-link-targets.d.ts.map +1 -0
  85. package/dist/lint/post-synth/cpl030-domain-routing.d.ts +3 -0
  86. package/dist/lint/post-synth/cpl030-domain-routing.d.ts.map +1 -0
  87. package/dist/lint/post-synth/cpl040-pinned-image.d.ts +11 -0
  88. package/dist/lint/post-synth/cpl040-pinned-image.d.ts.map +1 -0
  89. package/dist/lint/post-synth/cpl041-image-reference-form.d.ts +12 -0
  90. package/dist/lint/post-synth/cpl041-image-reference-form.d.ts.map +1 -0
  91. package/dist/lint/post-synth/cpl042-gvc-placement.d.ts +10 -0
  92. package/dist/lint/post-synth/cpl042-gvc-placement.d.ts.map +1 -0
  93. package/dist/lint/post-synth/cpl043-policy-scope.d.ts +16 -0
  94. package/dist/lint/post-synth/cpl043-policy-scope.d.ts.map +1 -0
  95. package/dist/lint/post-synth/helpers.d.ts +45 -0
  96. package/dist/lint/post-synth/helpers.d.ts.map +1 -0
  97. package/dist/lint/post-synth/index.d.ts +3 -0
  98. package/dist/lint/post-synth/index.d.ts.map +1 -0
  99. package/dist/lint/rules/cpl001-no-secret-literals.d.ts +3 -0
  100. package/dist/lint/rules/cpl001-no-secret-literals.d.ts.map +1 -0
  101. package/dist/lint/rules/cpl002-prefer-resource-reference.d.ts +3 -0
  102. package/dist/lint/rules/cpl002-prefer-resource-reference.d.ts.map +1 -0
  103. package/dist/lint/rules/index.d.ts +6 -0
  104. package/dist/lint/rules/index.d.ts.map +1 -0
  105. package/dist/lsp/completions.d.ts +7 -0
  106. package/dist/lsp/completions.d.ts.map +1 -0
  107. package/dist/lsp/hover.d.ts +4 -0
  108. package/dist/lsp/hover.d.ts.map +1 -0
  109. package/dist/manifest.json +6 -0
  110. package/dist/meta.json +2110 -0
  111. package/dist/okf/index.md +39 -0
  112. package/dist/okf/rules/CPL001.md +22 -0
  113. package/dist/okf/rules/CPL002.md +22 -0
  114. package/dist/okf/rules/CPL010.md +15 -0
  115. package/dist/okf/rules/CPL011.md +15 -0
  116. package/dist/okf/rules/CPL012.md +15 -0
  117. package/dist/okf/rules/CPL013.md +15 -0
  118. package/dist/okf/rules/CPL014.md +16 -0
  119. package/dist/okf/rules/CPL020.md +11 -0
  120. package/dist/okf/rules/CPL021.md +15 -0
  121. package/dist/okf/rules/CPL022.md +15 -0
  122. package/dist/okf/rules/CPL023.md +15 -0
  123. package/dist/okf/rules/CPL024.md +15 -0
  124. package/dist/okf/rules/CPL025.md +15 -0
  125. package/dist/okf/rules/CPL026.md +15 -0
  126. package/dist/okf/rules/CPL027.md +15 -0
  127. package/dist/okf/rules/CPL028.md +15 -0
  128. package/dist/okf/rules/CPL029.md +15 -0
  129. package/dist/okf/rules/CPL030.md +16 -0
  130. package/dist/okf/rules/CPL040.md +15 -0
  131. package/dist/okf/rules/CPL041.md +15 -0
  132. package/dist/okf/rules/CPL042.md +11 -0
  133. package/dist/okf/rules/CPL043.md +15 -0
  134. package/dist/okf/types/Domain.md +22 -0
  135. package/dist/okf/types/Gvc.md +21 -0
  136. package/dist/okf/types/Identity.md +29 -0
  137. package/dist/okf/types/IpSet.md +21 -0
  138. package/dist/okf/types/Policy.md +28 -0
  139. package/dist/okf/types/Secret.md +23 -0
  140. package/dist/okf/types/VolumeSet.md +22 -0
  141. package/dist/okf/types/Workload.md +38 -0
  142. package/dist/ownership.d.ts +23 -0
  143. package/dist/ownership.d.ts.map +1 -0
  144. package/dist/package-cli.d.ts +3 -0
  145. package/dist/package-cli.d.ts.map +1 -0
  146. package/dist/plugin.d.ts +3 -0
  147. package/dist/plugin.d.ts.map +1 -0
  148. package/dist/reference-catalog.d.ts +24 -0
  149. package/dist/reference-catalog.d.ts.map +1 -0
  150. package/dist/rules/cpl001-no-secret-literals.ts +83 -0
  151. package/dist/rules/cpl002-prefer-resource-reference.ts +71 -0
  152. package/dist/rules/cpl010-unrestricted-outbound.ts +41 -0
  153. package/dist/rules/cpl011-internal-firewall-scope.ts +38 -0
  154. package/dist/rules/cpl012-no-literal-credential-env.ts +58 -0
  155. package/dist/rules/cpl013-qualified-identity-principal.ts +45 -0
  156. package/dist/rules/cpl014-qualified-secret-ref.ts +54 -0
  157. package/dist/rules/cpl020-serverless-single-port.ts +58 -0
  158. package/dist/rules/cpl021-cron-job-shape.ts +64 -0
  159. package/dist/rules/cpl022-container-ports.ts +64 -0
  160. package/dist/rules/cpl023-container-resources.ts +87 -0
  161. package/dist/rules/cpl024-probe-handler.ts +51 -0
  162. package/dist/rules/cpl025-autoscaling-shape.ts +100 -0
  163. package/dist/rules/cpl026-scale-to-zero.ts +67 -0
  164. package/dist/rules/cpl027-capacity-ai-conflict.ts +78 -0
  165. package/dist/rules/cpl028-volume-set.ts +183 -0
  166. package/dist/rules/cpl029-link-targets.ts +155 -0
  167. package/dist/rules/cpl030-domain-routing.ts +137 -0
  168. package/dist/rules/cpl040-pinned-image.ts +48 -0
  169. package/dist/rules/cpl041-image-reference-form.ts +60 -0
  170. package/dist/rules/cpl042-gvc-placement.ts +57 -0
  171. package/dist/rules/cpl043-policy-scope.ts +0 -0
  172. package/dist/rules/helpers.ts +114 -0
  173. package/dist/serializer.d.ts +69 -0
  174. package/dist/serializer.d.ts.map +1 -0
  175. package/dist/skills/chant-cpln-secrets.md +110 -0
  176. package/dist/skills/chant-cpln-workloads.md +78 -0
  177. package/dist/skills/chant-cpln.md +103 -0
  178. package/dist/spec/fetch.d.ts +43 -0
  179. package/dist/spec/fetch.d.ts.map +1 -0
  180. package/dist/spec/parse.d.ts +79 -0
  181. package/dist/spec/parse.d.ts.map +1 -0
  182. package/dist/spec/snapshot-cli.d.ts +18 -0
  183. package/dist/spec/snapshot-cli.d.ts.map +1 -0
  184. package/dist/types/index.d.ts +1649 -0
  185. package/dist/validate-cli.d.ts +3 -0
  186. package/dist/validate-cli.d.ts.map +1 -0
  187. package/dist/validate.d.ts +26 -0
  188. package/dist/validate.d.ts.map +1 -0
  189. package/package.json +74 -0
  190. package/src/api.ts +134 -0
  191. package/src/codegen/docs-cli.ts +7 -0
  192. package/src/codegen/docs.ts +94 -0
  193. package/src/codegen/generate-cli.ts +34 -0
  194. package/src/codegen/generate-lexicon.ts +88 -0
  195. package/src/codegen/generate-typescript.ts +71 -0
  196. package/src/codegen/generate.ts +110 -0
  197. package/src/codegen/naming.ts +61 -0
  198. package/src/codegen/package.ts +35 -0
  199. package/src/composites/composites.test.ts +252 -0
  200. package/src/composites/cron-job.ts +124 -0
  201. package/src/composites/gvc-environment.ts +104 -0
  202. package/src/composites/index.ts +17 -0
  203. package/src/composites/public-domain.ts +144 -0
  204. package/src/composites/secret-access.ts +136 -0
  205. package/src/composites/serverless-service.ts +143 -0
  206. package/src/composites/stateful-service.ts +179 -0
  207. package/src/coverage.ts +149 -0
  208. package/src/describe-resources.test.ts +252 -0
  209. package/src/describe-resources.ts +310 -0
  210. package/src/detect.ts +58 -0
  211. package/src/entity-props.ts +98 -0
  212. package/src/generated/index.d.ts +1649 -0
  213. package/src/generated/index.ts +156 -0
  214. package/src/generated/lexicon-cpln.json +2110 -0
  215. package/src/generated/runtime.ts +4 -0
  216. package/src/index.ts +31 -0
  217. package/src/init-templates.ts +131 -0
  218. package/src/kinds.ts +173 -0
  219. package/src/lint/audit-catalog.ts +230 -0
  220. package/src/lint/post-synth/cpl010-unrestricted-outbound.ts +41 -0
  221. package/src/lint/post-synth/cpl011-internal-firewall-scope.ts +38 -0
  222. package/src/lint/post-synth/cpl012-no-literal-credential-env.ts +58 -0
  223. package/src/lint/post-synth/cpl013-qualified-identity-principal.ts +45 -0
  224. package/src/lint/post-synth/cpl014-qualified-secret-ref.ts +54 -0
  225. package/src/lint/post-synth/cpl020-serverless-single-port.ts +58 -0
  226. package/src/lint/post-synth/cpl021-cron-job-shape.ts +64 -0
  227. package/src/lint/post-synth/cpl022-container-ports.ts +64 -0
  228. package/src/lint/post-synth/cpl023-container-resources.ts +87 -0
  229. package/src/lint/post-synth/cpl024-probe-handler.ts +51 -0
  230. package/src/lint/post-synth/cpl025-autoscaling-shape.ts +100 -0
  231. package/src/lint/post-synth/cpl026-scale-to-zero.ts +67 -0
  232. package/src/lint/post-synth/cpl027-capacity-ai-conflict.ts +78 -0
  233. package/src/lint/post-synth/cpl028-volume-set.ts +183 -0
  234. package/src/lint/post-synth/cpl029-link-targets.ts +155 -0
  235. package/src/lint/post-synth/cpl030-domain-routing.ts +137 -0
  236. package/src/lint/post-synth/cpl040-pinned-image.ts +48 -0
  237. package/src/lint/post-synth/cpl041-image-reference-form.ts +60 -0
  238. package/src/lint/post-synth/cpl042-gvc-placement.ts +57 -0
  239. package/src/lint/post-synth/cpl043-policy-scope.ts +0 -0
  240. package/src/lint/post-synth/helpers.ts +114 -0
  241. package/src/lint/post-synth/index.ts +45 -0
  242. package/src/lint/post-synth/post-synth.test.ts +737 -0
  243. package/src/lint/rules/cpl001-no-secret-literals.ts +83 -0
  244. package/src/lint/rules/cpl002-prefer-resource-reference.ts +71 -0
  245. package/src/lint/rules/index.ts +9 -0
  246. package/src/lint/rules/rules.test.ts +108 -0
  247. package/src/lsp/completions.test.ts +44 -0
  248. package/src/lsp/completions.ts +27 -0
  249. package/src/lsp/hover.test.ts +53 -0
  250. package/src/lsp/hover.ts +130 -0
  251. package/src/ownership.ts +28 -0
  252. package/src/package-cli.ts +25 -0
  253. package/src/plugin.test.ts +131 -0
  254. package/src/plugin.ts +187 -0
  255. package/src/reference-catalog.ts +110 -0
  256. package/src/serializer.test.ts +233 -0
  257. package/src/serializer.ts +253 -0
  258. package/src/skills/chant-cpln-secrets.md +110 -0
  259. package/src/skills/chant-cpln-workloads.md +78 -0
  260. package/src/skills/chant-cpln.md +103 -0
  261. package/src/spec/cpln-openapi.snapshot.json +10651 -0
  262. package/src/spec/fetch.ts +77 -0
  263. package/src/spec/parse.ts +451 -0
  264. package/src/spec/snapshot-cli.ts +71 -0
  265. package/src/validate-cli.ts +8 -0
  266. package/src/validate.ts +102 -0
@@ -0,0 +1,310 @@
1
+ /**
2
+ * Live introspection of cpln resources — the read-back seam for chant's
3
+ * plan/drift machinery.
4
+ *
5
+ * Drives core's observer harness (`observeEntities`) rather than re-deriving
6
+ * its control flow: bind-or-not-observe-all with a typed reason, per-entity
7
+ * tri-state routing, and a read throw degrading to `read-failed` rather than a
8
+ * silent absence — which would classify as a spurious `create` and, on the
9
+ * delete path, as an orphan.
10
+ *
11
+ * The read is a lookup, not a fetch. Control Plane has a by-name endpoint for
12
+ * every kind, but it also has an **org-wide rollup** for the GVC-scoped ones
13
+ * (`/org/{org}/workload` alongside `/org/{org}/gvc/{gvc}/workload`), so listing
14
+ * each declared kind once and indexing it costs one request per kind instead of
15
+ * one per resource — or, for GVC-scoped kinds, one per GVC. The list promise is
16
+ * cached per kind, so concurrent reads share a request and a failed list marks
17
+ * only that kind's entities read-failed.
18
+ *
19
+ * Ownership is unusually clean here. Every kind carries a free-form `tags` map
20
+ * and every read path returns it, so the marker resolves on the thin read —
21
+ * which is not true of aws, where `describe-stack-resources` returns no tags
22
+ * and an `owned: true` thin read can only answer `unknown`. `describeResources`
23
+ * is therefore declared on the ownership channel.
24
+ *
25
+ * No secret value is ever read: the `-reveal` endpoint is not called from this
26
+ * path, and a `secret`'s `data` is not part of what is compared.
27
+ */
28
+
29
+ import type { ResourceMetadata } from "@intentius/chant/lexicon";
30
+ import type { ObservationResult } from "@intentius/chant/observation";
31
+ import {
32
+ observeEntities,
33
+ type DeclaredEntity,
34
+ type EntityObservation,
35
+ type ObserverAdapter,
36
+ type UnobservedReason,
37
+ } from "@intentius/chant/observation";
38
+ import { hasOwnershipMarker } from "@intentius/chant/ownership";
39
+ import {
40
+ CplnCredentialsError,
41
+ defaultCplnHttp,
42
+ listKind,
43
+ resolveConfig,
44
+ type CplnConfig,
45
+ type CplnHttp,
46
+ type CplnResource,
47
+ } from "./api";
48
+ import { kindByTypeName, type CplnKind } from "./kinds";
49
+ import { CPLN_TAG_OWNERSHIP_KEYS } from "./ownership";
50
+
51
+ export interface DescribeResourcesOptions {
52
+ environment: string;
53
+ buildOutput: string;
54
+ entityNames: string[];
55
+ entities: Map<string, { entityType: string; props: Record<string, unknown> }>;
56
+ /** Restrict the result to chant-owned resources. */
57
+ owned?: boolean;
58
+ /** Config overrides (tests, or an explicit org). */
59
+ config?: Partial<CplnConfig>;
60
+ /** Transport override (tests). Defaults to `fetch`. */
61
+ http?: CplnHttp;
62
+ }
63
+
64
+ /** The bound client: the resolved config, the transport, and the per-kind cache. */
65
+ interface CplnClient {
66
+ config: CplnConfig;
67
+ http: CplnHttp;
68
+ /** kind name → in-flight or settled list, so N entities of a kind cost one request. */
69
+ lists: Map<string, Promise<Map<string, CplnResource>>>;
70
+ }
71
+
72
+ /**
73
+ * Observe every declared cpln resource against the live org.
74
+ */
75
+ export async function describeResources(options: DescribeResourcesOptions): Promise<ObservationResult> {
76
+ const declared: DeclaredEntity[] = [];
77
+ for (const name of options.entityNames) {
78
+ const entity = options.entities.get(name);
79
+ if (!entity) continue;
80
+ declared.push({ name, type: entity.entityType, props: entity.props });
81
+ }
82
+
83
+ return observeEntities(declared, adapter(options));
84
+ }
85
+
86
+ function adapter(options: DescribeResourcesOptions): ObserverAdapter<CplnClient> {
87
+ return {
88
+ async bind(): Promise<CplnClient> {
89
+ const config = resolveConfig(options.config);
90
+ return { config, http: options.http ?? defaultCplnHttp(config), lists: new Map() };
91
+ },
92
+
93
+ classifyBindFailure(err: unknown): { reason: UnobservedReason; detail?: string } | "rethrow" {
94
+ if (err instanceof CplnCredentialsError) {
95
+ return { reason: "no-credentials", detail: err.message };
96
+ }
97
+ return { reason: "read-failed", detail: err instanceof Error ? err.message : String(err) };
98
+ },
99
+
100
+ async read(client: CplnClient, entity: DeclaredEntity): Promise<EntityObservation> {
101
+ const kind = kindByTypeName(entity.type);
102
+ if (!kind) {
103
+ return { unobserved: { reason: "unsupported-kind", detail: `${entity.type} is not a modelled cpln kind` } };
104
+ }
105
+
106
+ const index = await listFor(client, kind);
107
+ const declaredName = typeof entity.props.name === "string" ? entity.props.name : entity.name;
108
+ const live = index.get(declaredName);
109
+ if (!live) return { absent: true };
110
+
111
+ // A GVC-scoped kind is listed org-wide, so two GVCs may hold resources of
112
+ // the same name. Compare the live resource's own GVC before calling it a
113
+ // match — otherwise a workload in `staging` would be reported as the
114
+ // observed state of the one declared in `prod`.
115
+ if (kind.gvcScoped) {
116
+ const declaredGvc = typeof entity.props.gvc === "string" ? entity.props.gvc : undefined;
117
+ const liveGvc = gvcOf(live);
118
+ if (declaredGvc && liveGvc && declaredGvc !== liveGvc) return { absent: true };
119
+ }
120
+
121
+ const owned = hasOwnershipMarker(live.tags, CPLN_TAG_OWNERSHIP_KEYS);
122
+
123
+ // `owned: true` filters rather than hides: a foreign resource becomes a
124
+ // typed NOT-OBSERVED, so the caller can tell "not chant's" from "not
125
+ // there" — the latter would read as a create.
126
+ if (options.owned && !owned) {
127
+ return {
128
+ unobserved: {
129
+ reason: "filtered",
130
+ detail: `live ${kind.kind} "${declaredName}" carries no ${CPLN_TAG_OWNERSHIP_KEYS.managedBy} tag`,
131
+ },
132
+ };
133
+ }
134
+
135
+ return { present: toMetadata(kind, live, owned) };
136
+ },
137
+ };
138
+ }
139
+
140
+ /** List a kind once per observation, sharing the promise across entities. */
141
+ function listFor(client: CplnClient, kind: CplnKind): Promise<Map<string, CplnResource>> {
142
+ const cached = client.lists.get(kind.kind);
143
+ if (cached) return cached;
144
+ const pending = listKind(client.http, client.config, kind);
145
+ client.lists.set(kind.kind, pending);
146
+ return pending;
147
+ }
148
+
149
+ /**
150
+ * The GVC a live resource belongs to.
151
+ *
152
+ * The org-wide rollups do not repeat the GVC as a field; it is in the `links`
153
+ * array as a `gvc` relation (`/org/acme/gvc/prod`). `volumeset` and `identity`
154
+ * additionally carry a `gvc` of their own, in two different shapes, so those
155
+ * are read as a fallback.
156
+ */
157
+ export function gvcOf(resource: CplnResource): string | undefined {
158
+ for (const link of resource.links ?? []) {
159
+ if (link.rel !== "gvc" || typeof link.href !== "string") continue;
160
+ const name = link.href.split("/").filter(Boolean).pop();
161
+ if (name) return name;
162
+ }
163
+
164
+ const own = resource.gvc;
165
+ if (typeof own === "string" && own.length > 0) return own.split("/").filter(Boolean).pop();
166
+ if (own && typeof own === "object") {
167
+ const name = (own as { name?: unknown }).name;
168
+ if (typeof name === "string") return name;
169
+ }
170
+
171
+ return undefined;
172
+ }
173
+
174
+ /**
175
+ * Map a live resource onto chant's `ResourceMetadata`.
176
+ *
177
+ * `status` is a string in chant's model and an object in Control Plane's, so it
178
+ * is summarized rather than stringified: a workload's readiness where there is
179
+ * one, `"active"` otherwise. The full object stays available under `attributes`
180
+ * for anything that wants to look closer.
181
+ */
182
+ function toMetadata(kind: CplnKind, live: CplnResource, owned: boolean): ResourceMetadata {
183
+ return {
184
+ type: kind.typeName,
185
+ physicalId: live.id,
186
+ status: statusOf(live),
187
+ lastUpdated: live.lastModified,
188
+ ownership: owned ? "owned" : "foreign",
189
+ attributes: {
190
+ name: live.name,
191
+ version: live.version,
192
+ refs: referenceAttributes(kind, live),
193
+ ...(live.tags ? { tags: live.tags } : {}),
194
+ ...(kind.gvcScoped ? { gvc: gvcOf(live) } : {}),
195
+ ...(live.status ? { status: live.status } : {}),
196
+ },
197
+ };
198
+ }
199
+
200
+ /**
201
+ * Resolve this resource's outbound links down to the bare names they point at.
202
+ *
203
+ * Edge reconstruction matches identifiers exactly, and a Control Plane link
204
+ * (`//gvc/prod/identity/api`) never equals the `name` it references. Resolving
205
+ * here — where the whole live resource is in hand — means `reference-catalog.ts`
206
+ * is a plain table of paths rather than a table of paths plus extractors, and
207
+ * means no second enrichment pass over the observed set.
208
+ *
209
+ * Keys are stable regardless of kind; an absent relation is simply missing.
210
+ */
211
+ export function referenceAttributes(kind: CplnKind, live: CplnResource): Record<string, unknown> {
212
+ const refs: Record<string, unknown> = {};
213
+ const spec = (live.spec ?? {}) as Record<string, unknown>;
214
+
215
+ if (kind.gvcScoped) {
216
+ const gvc = gvcOf(live);
217
+ if (gvc) refs.gvc = gvc;
218
+ }
219
+
220
+ const identity = linkName(spec.identityLink);
221
+ if (identity) refs.identity = identity;
222
+
223
+ const pullSecrets = linkNames(spec.pullSecretLinks);
224
+ if (pullSecrets.length > 0) refs.pullSecrets = pullSecrets;
225
+
226
+ // A domain's GVC is a link rather than a scoping relation, so it is read from
227
+ // the spec even though the kind is org-scoped.
228
+ if (kind.kind === "domain") {
229
+ const gvc = linkName(spec.gvcLink);
230
+ if (gvc) refs.gvc = gvc;
231
+ }
232
+
233
+ const workloads = [
234
+ ...linkNames(spec.workloadLink),
235
+ ...linkNames(spec.link),
236
+ ...routeWorkloads(spec),
237
+ ].filter((name, index, all) => all.indexOf(name) === index);
238
+ if (workloads.length > 0) refs.workloads = workloads;
239
+
240
+ const targets = linkNames(live.targetLinks);
241
+ if (targets.length > 0) refs.targets = targets;
242
+
243
+ const volumeSets = mountedVolumeSets(spec);
244
+ if (volumeSets.length > 0) refs.volumeSets = volumeSets;
245
+
246
+ return refs;
247
+ }
248
+
249
+ /** The last path segment of a link (`//gvc/prod/identity/api` → `api`). */
250
+ function linkName(value: unknown): string | undefined {
251
+ if (typeof value !== "string" || value.length === 0) return undefined;
252
+ if (!value.startsWith("//") && !value.startsWith("/org/") && !value.startsWith("cpln://")) return undefined;
253
+ // A `cpln://secret/name.field` reference names a field after the resource.
254
+ const last = value.split("/").filter(Boolean).pop();
255
+ return last?.split(".")[0];
256
+ }
257
+
258
+ /** Resolve a link or a list of links to names, dropping anything unparseable. */
259
+ function linkNames(value: unknown): string[] {
260
+ const values = Array.isArray(value) ? value : [value];
261
+ return values.map(linkName).filter((name): name is string => !!name);
262
+ }
263
+
264
+ /** Workloads named by a domain's per-port routes. */
265
+ function routeWorkloads(spec: Record<string, unknown>): string[] {
266
+ const names: string[] = [];
267
+ const ports = Array.isArray(spec.ports) ? spec.ports : [];
268
+ for (const port of ports) {
269
+ const routes = (port as { routes?: unknown }).routes;
270
+ if (!Array.isArray(routes)) continue;
271
+ for (const route of routes) {
272
+ const name = linkName((route as { workloadLink?: unknown }).workloadLink);
273
+ if (name) names.push(name);
274
+ }
275
+ }
276
+ return names;
277
+ }
278
+
279
+ /** Volume sets a workload's containers mount, from their `cpln://volumeset/…` URIs. */
280
+ function mountedVolumeSets(spec: Record<string, unknown>): string[] {
281
+ const names: string[] = [];
282
+ const containers = Array.isArray(spec.containers) ? spec.containers : [];
283
+ for (const container of containers) {
284
+ const volumes = (container as { volumes?: unknown }).volumes;
285
+ if (!Array.isArray(volumes)) continue;
286
+ for (const volume of volumes) {
287
+ const uri = (volume as { uri?: unknown }).uri;
288
+ if (typeof uri !== "string" || !uri.startsWith("cpln://volumeset/")) continue;
289
+ names.push(uri.slice("cpln://volumeset/".length));
290
+ }
291
+ }
292
+ return names;
293
+ }
294
+
295
+ /** A one-word status for a live resource. */
296
+ function statusOf(live: CplnResource): string {
297
+ const health = live.health;
298
+ if (health && typeof health === "object") {
299
+ const readiness = (health as { readiness?: unknown }).readiness;
300
+ if (typeof readiness === "string" && readiness.length > 0) return readiness;
301
+ }
302
+
303
+ const status = live.status;
304
+ if (status && typeof status === "object") {
305
+ const ready = (status as { ready?: unknown }).ready;
306
+ if (typeof ready === "boolean") return ready ? "ready" : "not-ready";
307
+ }
308
+
309
+ return "active";
310
+ }
package/src/detect.ts ADDED
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Detect whether a template is a Control Plane manifest.
3
+ *
4
+ * cpln manifests are structurally light — `kind`, `name`, and usually `spec` —
5
+ * which is not a lot to go on, and `kind`/`name`/`spec` alone would also match a
6
+ * Kubernetes manifest. The discriminator is that a cpln manifest's `kind` is one
7
+ * of a small closed set of lowercase strings and it has **no `apiVersion`**,
8
+ * which every Kubernetes object has and no cpln manifest does.
9
+ */
10
+
11
+ import { KIND_NAMES } from "./kinds";
12
+
13
+ const KINDS = new Set(KIND_NAMES);
14
+
15
+ /**
16
+ * Every kind `cpln apply` accepts, not only the ones this lexicon models — a
17
+ * file of `group` and `serviceaccount` documents is still recognisably a
18
+ * Control Plane manifest, and answering "no" to it would be wrong.
19
+ */
20
+ const ALL_CPLN_KINDS = new Set([
21
+ ...KIND_NAMES,
22
+ "agent",
23
+ "auditctx",
24
+ "cloudaccount",
25
+ "group",
26
+ "image",
27
+ "location",
28
+ "mk8s",
29
+ "org",
30
+ "quota",
31
+ "serviceaccount",
32
+ "user",
33
+ ]);
34
+
35
+ /** True when `data` is a cpln manifest document, or a list of them. */
36
+ export function detectCplnTemplate(data: unknown): boolean {
37
+ if (Array.isArray(data)) {
38
+ return data.length > 0 && data.every((doc) => detectCplnTemplate(doc));
39
+ }
40
+
41
+ if (!data || typeof data !== "object") return false;
42
+ const doc = data as Record<string, unknown>;
43
+
44
+ // A Kubernetes object. Same three keys, different platform.
45
+ if (typeof doc.apiVersion === "string") return false;
46
+
47
+ const kind = doc.kind;
48
+ if (typeof kind !== "string" || !ALL_CPLN_KINDS.has(kind)) return false;
49
+
50
+ // `name` is the one field every cpln manifest carries. A bare `{ kind }` is
51
+ // more likely a fragment than a manifest.
52
+ return typeof doc.name === "string";
53
+ }
54
+
55
+ /** True when the document is a kind this lexicon can generate types for. */
56
+ export function isModelledKind(kind: string): boolean {
57
+ return KINDS.has(kind);
58
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Read an entity's authored props regardless of representation, and walk the
3
+ * shapes cpln's checks keep needing.
4
+ *
5
+ * Real runtime instances (`createResource`/`createProperty`) keep authored
6
+ * props under a non-enumerable `.props` — enumerating the instance yields its
7
+ * `AttrRef`s, not its inputs. Plain objects (test fixtures, parsed manifests)
8
+ * carry props as own fields. Every cpln consumer goes through here so both
9
+ * shapes behave identically.
10
+ *
11
+ * A cpln spec is also nested property declarables all the way down: a
12
+ * `Workload`'s `spec.containers[0].readinessProbe` may be a plain object, a
13
+ * `WorkloadSpecContainersReadinessProbe` instance, or a mix, depending on how
14
+ * the author wrote it. {@link readProp} unwraps at every level so a check can
15
+ * be written against one shape.
16
+ */
17
+
18
+ import type { Declarable } from "@intentius/chant/declarable";
19
+
20
+ /** Reserved instance fields core installs; never authored props. */
21
+ const RESERVED = new Set(["entityType", "lexicon", "kind", "attributes", "props", "Ref"]);
22
+
23
+ /** Read an entity's or property declarable's authored props. */
24
+ export function propsOf(entity: unknown): Record<string, unknown> {
25
+ if (!entity || typeof entity !== "object") return {};
26
+ const anyEntity = entity as Record<string, unknown>;
27
+ if (anyEntity.props && typeof anyEntity.props === "object" && !Array.isArray(anyEntity.props)) {
28
+ return anyEntity.props as Record<string, unknown>;
29
+ }
30
+ const out: Record<string, unknown> = {};
31
+ for (const [key, value] of Object.entries(anyEntity)) {
32
+ if (RESERVED.has(key)) continue;
33
+ out[key] = value;
34
+ }
35
+ return out;
36
+ }
37
+
38
+ /** Read a named property, unwrapping a nested property declarable. */
39
+ export function readProp(source: unknown, key: string): unknown {
40
+ const props = propsOf(source);
41
+ const value = props[key];
42
+ return isDeclarableLike(value) ? propsOf(value) : value;
43
+ }
44
+
45
+ /** Read a nested path, unwrapping declarables at each step. `readPath(w, "spec", "job")`. */
46
+ export function readPath(source: unknown, ...keys: string[]): unknown {
47
+ let current: unknown = source;
48
+ for (const key of keys) {
49
+ if (current === undefined || current === null) return undefined;
50
+ current = readProp(current, key);
51
+ }
52
+ return current;
53
+ }
54
+
55
+ /** Read a property expected to be an array, unwrapping each element. */
56
+ export function readArray(source: unknown, ...keys: string[]): unknown[] {
57
+ const value = readPath(source, ...keys);
58
+ if (!Array.isArray(value)) return [];
59
+ return value.map((item) => (isDeclarableLike(item) ? propsOf(item) : item));
60
+ }
61
+
62
+ /** Read a property expected to be a string. */
63
+ export function readString(source: unknown, ...keys: string[]): string | undefined {
64
+ const value = readPath(source, ...keys);
65
+ return typeof value === "string" ? value : undefined;
66
+ }
67
+
68
+ /** Read a property expected to be a number. */
69
+ export function readNumber(source: unknown, ...keys: string[]): number | undefined {
70
+ const value = readPath(source, ...keys);
71
+ return typeof value === "number" ? value : undefined;
72
+ }
73
+
74
+ /** Read a property expected to be a boolean. */
75
+ export function readBoolean(source: unknown, ...keys: string[]): boolean | undefined {
76
+ const value = readPath(source, ...keys);
77
+ return typeof value === "boolean" ? value : undefined;
78
+ }
79
+
80
+ /** A value that carries authored props under `.props` (a declarable instance). */
81
+ function isDeclarableLike(value: unknown): boolean {
82
+ return (
83
+ !!value &&
84
+ typeof value === "object" &&
85
+ !Array.isArray(value) &&
86
+ typeof (value as Record<string, unknown>).props === "object"
87
+ );
88
+ }
89
+
90
+ /** Iterate the entities of one cpln kind, yielding `[name, entity]`. */
91
+ export function* entitiesOfType(
92
+ entities: Map<string, Declarable>,
93
+ typeName: string,
94
+ ): Generator<[string, Declarable]> {
95
+ for (const [name, entity] of entities) {
96
+ if (entity.entityType === typeName) yield [name, entity];
97
+ }
98
+ }