typekro 0.33.9 → 0.34.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 (352) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/compositions/typekro-runtime/typekro-runtime.d.ts +0 -33
  3. package/dist/compositions/typekro-runtime/typekro-runtime.d.ts.map +1 -1
  4. package/dist/compositions/typekro-runtime/typekro-runtime.js +63 -1
  5. package/dist/compositions/typekro-runtime/typekro-runtime.js.map +1 -1
  6. package/dist/compositions/typekro-runtime/types.d.ts +8 -0
  7. package/dist/compositions/typekro-runtime/types.d.ts.map +1 -1
  8. package/dist/compositions/typekro-runtime/types.js +1 -0
  9. package/dist/compositions/typekro-runtime/types.js.map +1 -1
  10. package/dist/core/aspects/values-merge.d.ts +17 -0
  11. package/dist/core/aspects/values-merge.d.ts.map +1 -1
  12. package/dist/core/aspects/values-merge.js +67 -8
  13. package/dist/core/aspects/values-merge.js.map +1 -1
  14. package/dist/core/deployment/direct-factory.d.ts +9 -0
  15. package/dist/core/deployment/direct-factory.d.ts.map +1 -1
  16. package/dist/core/deployment/direct-factory.js +37 -3
  17. package/dist/core/deployment/direct-factory.js.map +1 -1
  18. package/dist/core/deployment/engine.d.ts +34 -0
  19. package/dist/core/deployment/engine.d.ts.map +1 -1
  20. package/dist/core/deployment/engine.js +173 -29
  21. package/dist/core/deployment/engine.js.map +1 -1
  22. package/dist/core/deployment/index.d.ts +2 -0
  23. package/dist/core/deployment/index.d.ts.map +1 -1
  24. package/dist/core/deployment/index.js +1 -0
  25. package/dist/core/deployment/index.js.map +1 -1
  26. package/dist/core/deployment/k8s-helpers.d.ts +73 -0
  27. package/dist/core/deployment/k8s-helpers.d.ts.map +1 -1
  28. package/dist/core/deployment/k8s-helpers.js +198 -4
  29. package/dist/core/deployment/k8s-helpers.js.map +1 -1
  30. package/dist/core/deployment/kro-factory.d.ts.map +1 -1
  31. package/dist/core/deployment/kro-factory.js +21 -1
  32. package/dist/core/deployment/kro-factory.js.map +1 -1
  33. package/dist/core/deployment/status-leaf-resolution.d.ts +101 -0
  34. package/dist/core/deployment/status-leaf-resolution.d.ts.map +1 -0
  35. package/dist/core/deployment/status-leaf-resolution.js +232 -0
  36. package/dist/core/deployment/status-leaf-resolution.js.map +1 -0
  37. package/dist/core/deployment/strategies/base-strategy.d.ts.map +1 -1
  38. package/dist/core/deployment/strategies/base-strategy.js +31 -3
  39. package/dist/core/deployment/strategies/base-strategy.js.map +1 -1
  40. package/dist/core/deployment/strategies/kro-strategy.d.ts.map +1 -1
  41. package/dist/core/deployment/strategies/kro-strategy.js +8 -1
  42. package/dist/core/deployment/strategies/kro-strategy.js.map +1 -1
  43. package/dist/core/expressions/composition/composition-analyzer-helpers.d.ts.map +1 -1
  44. package/dist/core/expressions/composition/composition-analyzer-helpers.js +20 -2
  45. package/dist/core/expressions/composition/composition-analyzer-helpers.js.map +1 -1
  46. package/dist/core/kro/index.d.ts +6 -0
  47. package/dist/core/kro/index.d.ts.map +1 -0
  48. package/dist/core/kro/index.js +6 -0
  49. package/dist/core/kro/index.js.map +1 -0
  50. package/dist/core/kro/label-guard-capability.d.ts +141 -0
  51. package/dist/core/kro/label-guard-capability.d.ts.map +1 -0
  52. package/dist/core/kro/label-guard-capability.js +257 -0
  53. package/dist/core/kro/label-guard-capability.js.map +1 -0
  54. package/dist/core/kro/labels.d.ts +56 -0
  55. package/dist/core/kro/labels.d.ts.map +1 -0
  56. package/dist/core/kro/labels.js +62 -0
  57. package/dist/core/kro/labels.js.map +1 -0
  58. package/dist/core/kubernetes/api-capability.d.ts +196 -0
  59. package/dist/core/kubernetes/api-capability.d.ts.map +1 -0
  60. package/dist/core/kubernetes/api-capability.js +301 -0
  61. package/dist/core/kubernetes/api-capability.js.map +1 -0
  62. package/dist/core/kubernetes/index.d.ts +2 -0
  63. package/dist/core/kubernetes/index.d.ts.map +1 -1
  64. package/dist/core/kubernetes/index.js +2 -0
  65. package/dist/core/kubernetes/index.js.map +1 -1
  66. package/dist/core/kubernetes/naming.d.ts +100 -0
  67. package/dist/core/kubernetes/naming.d.ts.map +1 -0
  68. package/dist/core/kubernetes/naming.js +93 -0
  69. package/dist/core/kubernetes/naming.js.map +1 -0
  70. package/dist/core/planning/direct-runtime-adapter.d.ts.map +1 -1
  71. package/dist/core/planning/direct-runtime-adapter.js +54 -17
  72. package/dist/core/planning/direct-runtime-adapter.js.map +1 -1
  73. package/dist/core/planning/planner.d.ts.map +1 -1
  74. package/dist/core/planning/planner.js +17 -0
  75. package/dist/core/planning/planner.js.map +1 -1
  76. package/dist/core/planning/schema.d.ts.map +1 -1
  77. package/dist/core/planning/schema.js +7 -3
  78. package/dist/core/planning/schema.js.map +1 -1
  79. package/dist/core/references/cel-lexical-scanner.d.ts +100 -1
  80. package/dist/core/references/cel-lexical-scanner.d.ts.map +1 -1
  81. package/dist/core/references/cel-lexical-scanner.js +196 -2
  82. package/dist/core/references/cel-lexical-scanner.js.map +1 -1
  83. package/dist/core/references/cel.d.ts +249 -1
  84. package/dist/core/references/cel.d.ts.map +1 -1
  85. package/dist/core/references/cel.js +289 -8
  86. package/dist/core/references/cel.js.map +1 -1
  87. package/dist/core/serialization/arktype-ast.d.ts +21 -0
  88. package/dist/core/serialization/arktype-ast.d.ts.map +1 -0
  89. package/dist/core/serialization/arktype-ast.js +29 -0
  90. package/dist/core/serialization/arktype-ast.js.map +1 -0
  91. package/dist/core/serialization/cel-references.d.ts +133 -32
  92. package/dist/core/serialization/cel-references.d.ts.map +1 -1
  93. package/dist/core/serialization/cel-references.js +908 -109
  94. package/dist/core/serialization/cel-references.js.map +1 -1
  95. package/dist/core/serialization/core.d.ts.map +1 -1
  96. package/dist/core/serialization/core.js +13 -2
  97. package/dist/core/serialization/core.js.map +1 -1
  98. package/dist/core/serialization/schema.d.ts +16 -2
  99. package/dist/core/serialization/schema.d.ts.map +1 -1
  100. package/dist/core/serialization/schema.js +113 -21
  101. package/dist/core/serialization/schema.js.map +1 -1
  102. package/dist/core/types/composable.d.ts +7 -1
  103. package/dist/core/types/composable.d.ts.map +1 -1
  104. package/dist/core/types/deployment.d.ts +32 -2
  105. package/dist/core/types/deployment.d.ts.map +1 -1
  106. package/dist/core/types/serialization.d.ts +37 -0
  107. package/dist/core/types/serialization.d.ts.map +1 -1
  108. package/dist/core/validation/cel-dialect.d.ts +246 -0
  109. package/dist/core/validation/cel-dialect.d.ts.map +1 -0
  110. package/dist/core/validation/cel-dialect.js +1997 -0
  111. package/dist/core/validation/cel-dialect.js.map +1 -0
  112. package/dist/core/validation/cel-validator.d.ts.map +1 -1
  113. package/dist/core/validation/cel-validator.js +8 -1
  114. package/dist/core/validation/cel-validator.js.map +1 -1
  115. package/dist/core/validation/literal-status.d.ts +107 -0
  116. package/dist/core/validation/literal-status.d.ts.map +1 -0
  117. package/dist/core/validation/literal-status.js +346 -0
  118. package/dist/core/validation/literal-status.js.map +1 -0
  119. package/dist/core/validation/structural-spec-dependence.d.ts +179 -0
  120. package/dist/core/validation/structural-spec-dependence.d.ts.map +1 -0
  121. package/dist/core/validation/structural-spec-dependence.js +911 -0
  122. package/dist/core/validation/structural-spec-dependence.js.map +1 -0
  123. package/dist/factories/apisix/compositions/apisix-bootstrap.d.ts.map +1 -1
  124. package/dist/factories/apisix/compositions/apisix-bootstrap.js +9 -0
  125. package/dist/factories/apisix/compositions/apisix-bootstrap.js.map +1 -1
  126. package/dist/factories/clickhouse/compositions/clickhouse-cluster.d.ts +6 -0
  127. package/dist/factories/clickhouse/compositions/clickhouse-cluster.d.ts.map +1 -1
  128. package/dist/factories/clickhouse/compositions/clickhouse-cluster.js +273 -23
  129. package/dist/factories/clickhouse/compositions/clickhouse-cluster.js.map +1 -1
  130. package/dist/factories/clickhouse/resources/index.d.ts +1 -0
  131. package/dist/factories/clickhouse/resources/index.d.ts.map +1 -1
  132. package/dist/factories/clickhouse/resources/index.js +1 -0
  133. package/dist/factories/clickhouse/resources/index.js.map +1 -1
  134. package/dist/factories/clickhouse/resources/installation.d.ts.map +1 -1
  135. package/dist/factories/clickhouse/resources/installation.js +31 -4
  136. package/dist/factories/clickhouse/resources/installation.js.map +1 -1
  137. package/dist/factories/clickhouse/resources/s3-backup.d.ts +215 -0
  138. package/dist/factories/clickhouse/resources/s3-backup.d.ts.map +1 -0
  139. package/dist/factories/clickhouse/resources/s3-backup.js +410 -0
  140. package/dist/factories/clickhouse/resources/s3-backup.js.map +1 -0
  141. package/dist/factories/clickhouse/types.d.ts +416 -20
  142. package/dist/factories/clickhouse/types.d.ts.map +1 -1
  143. package/dist/factories/clickhouse/types.js +235 -11
  144. package/dist/factories/clickhouse/types.js.map +1 -1
  145. package/dist/factories/clickhouse/utils/helm-values-mapper.d.ts +42 -0
  146. package/dist/factories/clickhouse/utils/helm-values-mapper.d.ts.map +1 -1
  147. package/dist/factories/clickhouse/utils/helm-values-mapper.js +48 -0
  148. package/dist/factories/clickhouse/utils/helm-values-mapper.js.map +1 -1
  149. package/dist/factories/clickhouse/utils/index.d.ts +2 -0
  150. package/dist/factories/clickhouse/utils/index.d.ts.map +1 -1
  151. package/dist/factories/clickhouse/utils/index.js +2 -0
  152. package/dist/factories/clickhouse/utils/index.js.map +1 -1
  153. package/dist/factories/clickhouse/utils/s3-storage.d.ts +503 -0
  154. package/dist/factories/clickhouse/utils/s3-storage.d.ts.map +1 -0
  155. package/dist/factories/clickhouse/utils/s3-storage.js +1139 -0
  156. package/dist/factories/clickhouse/utils/s3-storage.js.map +1 -0
  157. package/dist/factories/clickhouse/utils/validation.d.ts +103 -0
  158. package/dist/factories/clickhouse/utils/validation.d.ts.map +1 -1
  159. package/dist/factories/clickhouse/utils/validation.js +129 -0
  160. package/dist/factories/clickhouse/utils/validation.js.map +1 -1
  161. package/dist/factories/clickhouse/utils/xml.d.ts +87 -0
  162. package/dist/factories/clickhouse/utils/xml.d.ts.map +1 -0
  163. package/dist/factories/clickhouse/utils/xml.js +151 -0
  164. package/dist/factories/clickhouse/utils/xml.js.map +1 -0
  165. package/dist/factories/clickstack/compositions/clickstack-bootstrap.d.ts +57 -0
  166. package/dist/factories/clickstack/compositions/clickstack-bootstrap.d.ts.map +1 -1
  167. package/dist/factories/clickstack/compositions/clickstack-bootstrap.js +266 -7
  168. package/dist/factories/clickstack/compositions/clickstack-bootstrap.js.map +1 -1
  169. package/dist/factories/clickstack/types.d.ts +205 -26
  170. package/dist/factories/clickstack/types.d.ts.map +1 -1
  171. package/dist/factories/clickstack/types.js +106 -8
  172. package/dist/factories/clickstack/types.js.map +1 -1
  173. package/dist/factories/clickstack/utils/collector-config.d.ts +104 -0
  174. package/dist/factories/clickstack/utils/collector-config.d.ts.map +1 -0
  175. package/dist/factories/clickstack/utils/collector-config.js +266 -0
  176. package/dist/factories/clickstack/utils/collector-config.js.map +1 -0
  177. package/dist/factories/clickstack/utils/helm-values-mapper.d.ts +26 -0
  178. package/dist/factories/clickstack/utils/helm-values-mapper.d.ts.map +1 -1
  179. package/dist/factories/clickstack/utils/helm-values-mapper.js +53 -12
  180. package/dist/factories/clickstack/utils/helm-values-mapper.js.map +1 -1
  181. package/dist/factories/clickstack/utils/index.d.ts +2 -0
  182. package/dist/factories/clickstack/utils/index.d.ts.map +1 -1
  183. package/dist/factories/clickstack/utils/index.js +2 -0
  184. package/dist/factories/clickstack/utils/index.js.map +1 -1
  185. package/dist/factories/clickstack/utils/storage.d.ts +449 -0
  186. package/dist/factories/clickstack/utils/storage.d.ts.map +1 -0
  187. package/dist/factories/clickstack/utils/storage.js +698 -0
  188. package/dist/factories/clickstack/utils/storage.js.map +1 -0
  189. package/dist/factories/cnpg/resources/helm.d.ts.map +1 -1
  190. package/dist/factories/cnpg/resources/helm.js +4 -4
  191. package/dist/factories/cnpg/resources/helm.js.map +1 -1
  192. package/dist/factories/cnpg/types.d.ts +10 -1
  193. package/dist/factories/cnpg/types.d.ts.map +1 -1
  194. package/dist/factories/cnpg/types.js.map +1 -1
  195. package/dist/factories/cnpg/utils/helm-values-mapper.d.ts +19 -2
  196. package/dist/factories/cnpg/utils/helm-values-mapper.d.ts.map +1 -1
  197. package/dist/factories/cnpg/utils/helm-values-mapper.js +22 -4
  198. package/dist/factories/cnpg/utils/helm-values-mapper.js.map +1 -1
  199. package/dist/factories/envoy-ai-gateway/compositions/gateway.d.ts.map +1 -1
  200. package/dist/factories/envoy-ai-gateway/compositions/gateway.js +16 -3
  201. package/dist/factories/envoy-ai-gateway/compositions/gateway.js.map +1 -1
  202. package/dist/factories/envoy-ai-gateway/constants.d.ts +5 -2
  203. package/dist/factories/envoy-ai-gateway/constants.d.ts.map +1 -1
  204. package/dist/factories/envoy-ai-gateway/constants.js +5 -2
  205. package/dist/factories/envoy-ai-gateway/constants.js.map +1 -1
  206. package/dist/factories/envoy-ai-gateway/resources/gateway.d.ts +12 -0
  207. package/dist/factories/envoy-ai-gateway/resources/gateway.d.ts.map +1 -1
  208. package/dist/factories/envoy-ai-gateway/resources/gateway.js +27 -104
  209. package/dist/factories/envoy-ai-gateway/resources/gateway.js.map +1 -1
  210. package/dist/factories/envoy-ai-gateway/types.d.ts +15 -47
  211. package/dist/factories/envoy-ai-gateway/types.d.ts.map +1 -1
  212. package/dist/factories/envoy-ai-gateway/types.js.map +1 -1
  213. package/dist/factories/gateway-api/constants.d.ts +27 -0
  214. package/dist/factories/gateway-api/constants.d.ts.map +1 -0
  215. package/dist/factories/gateway-api/constants.js +27 -0
  216. package/dist/factories/gateway-api/constants.js.map +1 -0
  217. package/dist/factories/gateway-api/index.d.ts +14 -0
  218. package/dist/factories/gateway-api/index.d.ts.map +1 -0
  219. package/dist/factories/gateway-api/index.js +14 -0
  220. package/dist/factories/gateway-api/index.js.map +1 -0
  221. package/dist/factories/gateway-api/readiness.d.ts +67 -0
  222. package/dist/factories/gateway-api/readiness.d.ts.map +1 -0
  223. package/dist/factories/gateway-api/readiness.js +213 -0
  224. package/dist/factories/gateway-api/readiness.js.map +1 -0
  225. package/dist/factories/gateway-api/resources/gateway.d.ts +91 -0
  226. package/dist/factories/gateway-api/resources/gateway.d.ts.map +1 -0
  227. package/dist/factories/gateway-api/resources/gateway.js +99 -0
  228. package/dist/factories/gateway-api/resources/gateway.js.map +1 -0
  229. package/dist/factories/gateway-api/resources/index.d.ts +2 -0
  230. package/dist/factories/gateway-api/resources/index.d.ts.map +1 -0
  231. package/dist/factories/gateway-api/resources/index.js +2 -0
  232. package/dist/factories/gateway-api/resources/index.js.map +1 -0
  233. package/dist/factories/gateway-api/types.d.ts +1269 -0
  234. package/dist/factories/gateway-api/types.d.ts.map +1 -0
  235. package/dist/factories/gateway-api/types.js +417 -0
  236. package/dist/factories/gateway-api/types.js.map +1 -0
  237. package/dist/factories/helm/readiness-evaluators.d.ts +21 -3
  238. package/dist/factories/helm/readiness-evaluators.d.ts.map +1 -1
  239. package/dist/factories/helm/readiness-evaluators.js +117 -11
  240. package/dist/factories/helm/readiness-evaluators.js.map +1 -1
  241. package/dist/factories/helm/types.d.ts +28 -0
  242. package/dist/factories/helm/types.d.ts.map +1 -1
  243. package/dist/factories/index.d.ts +2 -0
  244. package/dist/factories/index.d.ts.map +1 -1
  245. package/dist/factories/index.js +8 -0
  246. package/dist/factories/index.js.map +1 -1
  247. package/dist/factories/inngest/compositions/inngest-bootstrap.d.ts.map +1 -1
  248. package/dist/factories/inngest/compositions/inngest-bootstrap.js +10 -0
  249. package/dist/factories/inngest/compositions/inngest-bootstrap.js.map +1 -1
  250. package/dist/factories/kubernetes/admission/index.d.ts +2 -0
  251. package/dist/factories/kubernetes/admission/index.d.ts.map +1 -1
  252. package/dist/factories/kubernetes/admission/index.js +2 -0
  253. package/dist/factories/kubernetes/admission/index.js.map +1 -1
  254. package/dist/factories/kubernetes/admission/label-propagation-guard.d.ts +108 -0
  255. package/dist/factories/kubernetes/admission/label-propagation-guard.d.ts.map +1 -0
  256. package/dist/factories/kubernetes/admission/label-propagation-guard.js +247 -0
  257. package/dist/factories/kubernetes/admission/label-propagation-guard.js.map +1 -0
  258. package/dist/factories/kubernetes/admission/mutating-admission-policy.d.ts +286 -0
  259. package/dist/factories/kubernetes/admission/mutating-admission-policy.d.ts.map +1 -0
  260. package/dist/factories/kubernetes/admission/mutating-admission-policy.js +157 -0
  261. package/dist/factories/kubernetes/admission/mutating-admission-policy.js.map +1 -0
  262. package/dist/factories/opensearch/compositions/cluster.d.ts.map +1 -1
  263. package/dist/factories/opensearch/compositions/cluster.js +6 -1
  264. package/dist/factories/opensearch/compositions/cluster.js.map +1 -1
  265. package/dist/factories/ory/compositions/ory-identity-stack.d.ts +8 -8
  266. package/dist/factories/ory/types.d.ts +16 -16
  267. package/dist/factories/rook/compositions/object-storage-claim.d.ts +1 -1
  268. package/dist/factories/rook/compositions/rook-ceph-platform.js +8 -1
  269. package/dist/factories/rook/compositions/rook-ceph-platform.js.map +1 -1
  270. package/dist/factories/rook/types.d.ts +1 -1
  271. package/dist/factories/searxng/compositions/searxng-bootstrap.d.ts +1 -1
  272. package/dist/factories/searxng/compositions/searxng-bootstrap.d.ts.map +1 -1
  273. package/dist/factories/searxng/compositions/searxng-bootstrap.js +18 -13
  274. package/dist/factories/searxng/compositions/searxng-bootstrap.js.map +1 -1
  275. package/dist/factories/searxng/index.d.ts +3 -0
  276. package/dist/factories/searxng/index.d.ts.map +1 -1
  277. package/dist/factories/searxng/index.js +3 -0
  278. package/dist/factories/searxng/index.js.map +1 -1
  279. package/dist/factories/searxng/types.js +1 -1
  280. package/dist/factories/searxng/types.js.map +1 -1
  281. package/dist/factories/traefik/compositions/index.d.ts +6 -0
  282. package/dist/factories/traefik/compositions/index.d.ts.map +1 -0
  283. package/dist/factories/traefik/compositions/index.js +6 -0
  284. package/dist/factories/traefik/compositions/index.js.map +1 -0
  285. package/dist/factories/traefik/compositions/traefik-bootstrap.d.ts +185 -0
  286. package/dist/factories/traefik/compositions/traefik-bootstrap.d.ts.map +1 -0
  287. package/dist/factories/traefik/compositions/traefik-bootstrap.js +339 -0
  288. package/dist/factories/traefik/compositions/traefik-bootstrap.js.map +1 -0
  289. package/dist/factories/traefik/compositions/traefik-helm-repository.d.ts +16 -0
  290. package/dist/factories/traefik/compositions/traefik-helm-repository.d.ts.map +1 -0
  291. package/dist/factories/traefik/compositions/traefik-helm-repository.js +31 -0
  292. package/dist/factories/traefik/compositions/traefik-helm-repository.js.map +1 -0
  293. package/dist/factories/traefik/constants.d.ts +78 -0
  294. package/dist/factories/traefik/constants.d.ts.map +1 -0
  295. package/dist/factories/traefik/constants.js +104 -0
  296. package/dist/factories/traefik/constants.js.map +1 -0
  297. package/dist/factories/traefik/index.d.ts +25 -0
  298. package/dist/factories/traefik/index.d.ts.map +1 -0
  299. package/dist/factories/traefik/index.js +25 -0
  300. package/dist/factories/traefik/index.js.map +1 -0
  301. package/dist/factories/traefik/resources/common.d.ts +68 -0
  302. package/dist/factories/traefik/resources/common.d.ts.map +1 -0
  303. package/dist/factories/traefik/resources/common.js +59 -0
  304. package/dist/factories/traefik/resources/common.js.map +1 -0
  305. package/dist/factories/traefik/resources/gateway.d.ts +85 -0
  306. package/dist/factories/traefik/resources/gateway.d.ts.map +1 -0
  307. package/dist/factories/traefik/resources/gateway.js +109 -0
  308. package/dist/factories/traefik/resources/gateway.js.map +1 -0
  309. package/dist/factories/traefik/resources/helm.d.ts +56 -0
  310. package/dist/factories/traefik/resources/helm.d.ts.map +1 -0
  311. package/dist/factories/traefik/resources/helm.js +131 -0
  312. package/dist/factories/traefik/resources/helm.js.map +1 -0
  313. package/dist/factories/traefik/resources/index.d.ts +10 -0
  314. package/dist/factories/traefik/resources/index.d.ts.map +1 -0
  315. package/dist/factories/traefik/resources/index.js +10 -0
  316. package/dist/factories/traefik/resources/index.js.map +1 -0
  317. package/dist/factories/traefik/resources/middleware.d.ts +169 -0
  318. package/dist/factories/traefik/resources/middleware.d.ts.map +1 -0
  319. package/dist/factories/traefik/resources/middleware.js +225 -0
  320. package/dist/factories/traefik/resources/middleware.js.map +1 -0
  321. package/dist/factories/traefik/resources/routing.d.ts +113 -0
  322. package/dist/factories/traefik/resources/routing.d.ts.map +1 -0
  323. package/dist/factories/traefik/resources/routing.js +119 -0
  324. package/dist/factories/traefik/resources/routing.js.map +1 -0
  325. package/dist/factories/traefik/resources/tls.d.ts +85 -0
  326. package/dist/factories/traefik/resources/tls.d.ts.map +1 -0
  327. package/dist/factories/traefik/resources/tls.js +93 -0
  328. package/dist/factories/traefik/resources/tls.js.map +1 -0
  329. package/dist/factories/traefik/types.d.ts +2690 -0
  330. package/dist/factories/traefik/types.d.ts.map +1 -0
  331. package/dist/factories/traefik/types.js +1084 -0
  332. package/dist/factories/traefik/types.js.map +1 -0
  333. package/dist/factories/traefik/utils/helm-values-mapper.d.ts +198 -0
  334. package/dist/factories/traefik/utils/helm-values-mapper.d.ts.map +1 -0
  335. package/dist/factories/traefik/utils/helm-values-mapper.js +372 -0
  336. package/dist/factories/traefik/utils/helm-values-mapper.js.map +1 -0
  337. package/dist/factories/traefik/utils/index.d.ts +6 -0
  338. package/dist/factories/traefik/utils/index.d.ts.map +1 -0
  339. package/dist/factories/traefik/utils/index.js +6 -0
  340. package/dist/factories/traefik/utils/index.js.map +1 -0
  341. package/dist/factories/traefik/utils/middleware-validation.d.ts +42 -0
  342. package/dist/factories/traefik/utils/middleware-validation.d.ts.map +1 -0
  343. package/dist/factories/traefik/utils/middleware-validation.js +78 -0
  344. package/dist/factories/traefik/utils/middleware-validation.js.map +1 -0
  345. package/dist/factories/webapp/compositions/web-app-with-processing.d.ts.map +1 -1
  346. package/dist/factories/webapp/compositions/web-app-with-processing.js +10 -0
  347. package/dist/factories/webapp/compositions/web-app-with-processing.js.map +1 -1
  348. package/dist/index.d.ts +4 -3
  349. package/dist/index.d.ts.map +1 -1
  350. package/dist/index.js +3 -1
  351. package/dist/index.js.map +1 -1
  352. package/package.json +9 -1
@@ -14,36 +14,242 @@ import { canonicalizeCelResourceAliases } from '../../utils/cel-resource-identif
14
14
  import { isCelExpression, isKubernetesRef } from '../../utils/type-guards.js';
15
15
  import { isValuesMergeExpression } from '../aspects/values-merge.js';
16
16
  import { remapVariableNames } from '../composition/nested-status-cel.js';
17
+ import { celLineCommentEnd, celStringLiteralEnd, collectCelLambdaScopes, maskClosedCelLiteralsAndComments, } from '../references/cel-lexical-scanner.js';
18
+ import { ConversionError } from '../errors.js';
19
+ import { isStrictCelDiagnosticsEnabled } from '../expressions/analysis/strict-cel.js';
17
20
  import { getComponentLogger } from '../logging/index.js';
18
21
  import { copyResourceMetadata } from '../metadata/index.js';
19
22
  const logger = getComponentLogger('cel-references');
20
23
  function escapeRegExpLiteral(value) {
21
24
  return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
22
25
  }
23
- export function normalizeCelArrayIndexPaths(expr) {
26
+ /** CEL `IDENT ::= [_a-zA-Z][_a-zA-Z0-9]*`, widened by `$` as the marker charset spells it. */
27
+ const CEL_INDEX_TARGET_START = /[A-Za-z_$]/;
28
+ const CEL_INDEX_TARGET_CHARACTER = /[A-Za-z0-9_$]/;
29
+ /**
30
+ * What may follow a `.<digits>` run without making it part of a longer token.
31
+ *
32
+ * Read against the CEL lexis punctuation set. The class holds every CEL
33
+ * punctuation character that can legally follow a complete index, plus
34
+ * whitespace and end-of-text:
35
+ *
36
+ * - `[` — an index may be taken of an index. `a.0[1]` and `a.0["k"]` are the
37
+ * dotted spelling of `a[0][1]` and `a[0]["k"]`; leaving `[` out meant those
38
+ * were not converted and stayed invalid CEL.
39
+ * - `%` — the modulo operator, alongside the `+ - * /` already here. `a.0 % 2`
40
+ * only converted because of the whitespace; `a.0%2` did not.
41
+ * - `.` `]` `)` `}` — a following select, a closing index, call or aggregate.
42
+ * - `? :` `,` `< > = ! & |` — the conditional, argument lists, and the
43
+ * relational, equality and logical operators.
44
+ *
45
+ * `(` is deliberately NOT here: `a.0(` is not CEL under any reading, because a
46
+ * call target is an `IDENT` and an index is not callable — converting it would
47
+ * invent a program rather than respell one. `;` `@` `#` are not CEL punctuation
48
+ * at all, so text containing them is not an expression this sweep should touch.
49
+ */
50
+ const CEL_INDEX_RUN_TERMINATOR = /[.[\])}%\s?:,+\-*/<>=!&|]/;
51
+ /**
52
+ * Does the text emitted so far end with something an index can be taken of?
53
+ *
54
+ * That is a whole CEL identifier — `[A-Za-z_$][A-Za-z0-9_$]*` — or a `]` that
55
+ * closed an earlier index. Requiring a whole IDENTIFIER rather than merely an
56
+ * identifier CHARACTER is what separates `v2.0` (the identifier `v2`, indexed)
57
+ * from `1.0` (a NUMBER literal whose fraction must not be touched): both end in
58
+ * a digit, but only the former's digit run is anchored by an identifier start.
59
+ */
60
+ function endsWithIndexableTarget(emitted) {
61
+ let index = emitted.length - 1;
62
+ if (index < 0)
63
+ return false;
64
+ if (emitted[index] === ']')
65
+ return true;
66
+ while (index >= 0 && CEL_INDEX_TARGET_CHARACTER.test(emitted[index]))
67
+ index -= 1;
68
+ const start = emitted[index + 1];
69
+ return start !== undefined && CEL_INDEX_TARGET_START.test(start);
70
+ }
71
+ /** The `STRING_LIT`/`BYTES_LIT` prefix letters, in either case and either order. */
72
+ const CEL_STRING_PREFIX_LETTER = /[rRbB]/;
73
+ /**
74
+ * End of the CEL string literal that starts at `index`, or `index` for none.
75
+ *
76
+ * Both scans below walk this module's text one character at a time, so each
77
+ * needs the same question answered at each step: *does a `STRING_LIT` or
78
+ * `BYTES_LIT` token begin here, and where does it end?* The answer comes from
79
+ * {@link celStringLiteralEnd}, the branch's spec-complete lexer — `r`/`R`/`b`/`B`
80
+ * prefixes in either order, the two triple-quoted forms, escapes in the non-raw
81
+ * forms only — rather than from a hand-rolled `"…"`/`'…'` loop, which mis-lexed
82
+ * every one of those: `r"a\"` is a COMPLETE raw literal (raw forms have no
83
+ * escape alternative), and `"""…"""` closes only at its matching triple.
84
+ *
85
+ * The STRING-ONLY entry point is deliberate. Its whole-text sibling
86
+ * {@link maskClosedCelLiteralsAndComments} masks comments too, and running THAT
87
+ * over a KRO MIXED TEMPLATE is wrong, because a template's literal part
88
+ * routinely contains `http://…` and a whole-text comment walk would read that
89
+ * `//` as a `COMMENT` and blank the rest of the line — the region markers
90
+ * included. Comment awareness is still owed to the parts of the text that
91
+ * really are CEL; it is supplied there, region-locally, by
92
+ * {@link celRegionCommentEnd}, which the two scans below ask ALONGSIDE this one
93
+ * at each index. Only the string half of the lexis is wanted from this helper.
94
+ *
95
+ * `previous` is the input character before `index`. A prefix letter opens a
96
+ * literal only when it does not merely CONTINUE an identifier, so that `myr"x"`
97
+ * lexes as the identifier `myr` followed by a plain string rather than as a raw
98
+ * one — the same boundary rule the scanner's own source-ordered walk gets from
99
+ * consuming an `IDENT` whole. A quote needs no such guard: `a"x"` opens a
100
+ * literal whatever precedes it.
101
+ *
102
+ * An UNTERMINATED literal is not a token: `index` comes back, and the caller
103
+ * steps over the opening quote as ordinary text. That is what keeps a bare
104
+ * apostrophe in marker-laden template text from swallowing everything after it.
105
+ */
106
+ function celStringLiteralEndAt(expr, index, previous) {
107
+ const character = expr[index];
108
+ if (character === undefined)
109
+ return index;
110
+ if (character !== '"' && character !== "'") {
111
+ if (!CEL_STRING_PREFIX_LETTER.test(character))
112
+ return index;
113
+ if (previous !== undefined && CEL_INDEX_TARGET_CHARACTER.test(previous))
114
+ return index;
115
+ }
116
+ return celStringLiteralEnd(expr, index);
117
+ }
118
+ /**
119
+ * End of the CEL `COMMENT ::= '//' ~NEWLINE*` that starts at `index`, or
120
+ * `index` for none — asked ONLY of text that is itself CEL.
121
+ *
122
+ * **Why a comment is a token here.** Inside a `${ … }` region — and in a
123
+ * genuinely bare CEL expression — the text IS CEL, and CEL's lexis has line
124
+ * comments. A `//` there that is not inside a STRING_LIT begins one, and
125
+ * everything up to the newline is commentary: it holds no braces that can close
126
+ * the region and no paths that can be rewritten. Without this, the `{` of
127
+ * `${a.0 // {` + newline + ` + b.1}` was counted as a nested brace, so
128
+ * {@link celTemplateRegionEnd} never found the region's close, returned `-1`,
129
+ * and the caller copied the whole remainder through — leaving the invalid `.0`
130
+ * and `.1` accesses the sweep exists to remove. The rewrite walk had the
131
+ * mirror-image fault, respelling a `.<digits>` that was only ever commentary.
132
+ *
133
+ * **Where this does NOT apply, and why that is not the round-3 `http://`
134
+ * problem.** That evidence was about LITERAL TEMPLATE TEXT — the part of a KRO
135
+ * mixed template outside every `${ … }`, which KRO emits verbatim and which is
136
+ * not CEL at all. In `` `http://${string(a.spec.ports.0.port)}` `` the `//`
137
+ * belongs to a URL scheme, and reading it as a comment would blank the region
138
+ * that follows it. Literal text is still never comment-scanned: this helper is
139
+ * asked only between a region's braces, and in the whole-string branch that is
140
+ * reached only when the text is bare CEL. A URL that appears INSIDE a region
141
+ * lives in a string literal (`${"http://x" + a.0}`), and
142
+ * {@link celStringLiteralEndAt} skips that literal WHOLE before this question is
143
+ * ever reached at the `//`.
144
+ *
145
+ * **Ordering.** Both callers ask this question and the string question at the
146
+ * same index in one source-ordered walk — the rule the scanner's own
147
+ * `celLiteralRegions` encodes: a `//` inside a literal opens no comment, and a
148
+ * quote inside a comment opens no literal, so `${a.0 // "` + newline +
149
+ * ` + b.1}` still rewrites both runs instead of letting the commented-out quote
150
+ * swallow the rest of the region.
151
+ */
152
+ function celRegionCommentEnd(expr, index) {
153
+ return celLineCommentEnd(expr, index);
154
+ }
155
+ /**
156
+ * Find the `}` that closes the KRO `${ … }` region whose body starts at
157
+ * `start`.
158
+ *
159
+ * Brace-balanced, string-literal aware and COMMENT aware. A CEL region may
160
+ * contain a `{` of its own (a map literal), and may contain a `{` or `}` that
161
+ * does not open or end anything — inside a STRING_LIT, as in `${"}" + a.0}`, or
162
+ * inside a `//` comment ({@link celRegionCommentEnd}). Scanning for the first
163
+ * `}` would cut the region short there and hand the rest of the expression to
164
+ * the literal-text path. Returns the index of the closing brace, or `-1` when the
165
+ * region is never closed (in which case the caller copies the remainder
166
+ * through unchanged rather than guessing where it ended).
167
+ *
168
+ * Every literal is skipped WHOLE, via {@link celStringLiteralEndAt}, and brace
169
+ * counting resumes after it. Deciding string-ness delimiter by delimiter got
170
+ * the whole prefixed and triple-quoted family wrong in both directions: in
171
+ * `${r"\" + a.0}` the `\"` read as an escape, so the region ran past its real
172
+ * `}` and the remainder — `a.0` included — was copied through unnormalised,
173
+ * while in `${"""a"}b""" + c.0}` the quotes paired off two at a time and the
174
+ * region ended at a `}` that is inside the triple-quoted literal.
175
+ *
176
+ * A comment is skipped WHOLE the same way, up to but not including its newline,
177
+ * so neither the `{` of `${a.0 // {` nor the `}` of `${a.0 // }` reaches the
178
+ * depth counter.
179
+ */
180
+ function celTemplateRegionEnd(expr, start) {
181
+ let depth = 1;
182
+ for (let i = start; i < expr.length; i++) {
183
+ const char = expr[i];
184
+ if (!char)
185
+ continue;
186
+ const commentEnd = celRegionCommentEnd(expr, i);
187
+ if (commentEnd > i) {
188
+ i = commentEnd - 1;
189
+ continue;
190
+ }
191
+ const literalEnd = celStringLiteralEndAt(expr, i, expr[i - 1]);
192
+ if (literalEnd > i) {
193
+ i = literalEnd - 1;
194
+ continue;
195
+ }
196
+ if (char === '{') {
197
+ depth += 1;
198
+ }
199
+ else if (char === '}') {
200
+ depth -= 1;
201
+ if (depth === 0)
202
+ return i;
203
+ }
204
+ }
205
+ return -1;
206
+ }
207
+ /**
208
+ * Does this text carry a `__KUBERNETES_REF_…__` marker?
209
+ *
210
+ * Built from the shared marker grammar — `KUBERNETES_REF_MARKER_SOURCE` in
211
+ * `shared/brands.ts`, the single source of truth for marker syntax — rather
212
+ * than from a hand-written `__KUBERNETES_REF_` prefix, so that a change to the
213
+ * resource-id or field-path charset reaches this test too. Deliberately NOT
214
+ * global: `test` on a `/g` regex carries `lastIndex` between calls.
215
+ */
216
+ const CARRIES_KUBERNETES_REF_MARKER = new RegExp(KUBERNETES_REF_MARKER_SOURCE);
217
+ /**
218
+ * Apply the dotted-numeric-run rule to ONE CEL region — a `${ … }` body, or a
219
+ * whole bare CEL expression. See {@link normalizeCelArrayIndexPaths} for the
220
+ * rule itself, and {@link celStringLiteralEndAt} for why the string scan is the
221
+ * STRING-ONLY lexer rather than the shared comment-aware mask.
222
+ *
223
+ * A literal is copied through WHOLE and the walk resumes after it, so quoted
224
+ * data is never rewritten and text outside a literal is never protected by one.
225
+ * The delimiter-by-delimiter scan this replaced got both halves wrong on the
226
+ * prefixed forms: `r"a\".b.0"` is the complete raw literal `r"a\"` followed by
227
+ * `.b.0"`, whose `.0` is an ordinary index, yet the `\"` read as an escape and
228
+ * protected it; `rb'x\' + c.0` lost its `c.0` the same way.
229
+ *
230
+ * A `//` COMMENT is copied through whole for the same reason
231
+ * ({@link celRegionCommentEnd}): its text is commentary, not a path, so the
232
+ * `c.1` of `a.0 // c.1` + newline + `+ b.1` stays as written while the two real
233
+ * accesses either side of it become `a[0]` and `b[1]`. The newline is left to
234
+ * the ordinary walk, which also means a comment can never lend its last
235
+ * identifier as the left context of the next line's run.
236
+ */
237
+ function rewriteCelIndexPathsInRegion(expr) {
24
238
  let result = '';
25
- let quote = null;
26
- let escaped = false;
27
239
  for (let i = 0; i < expr.length; i++) {
28
240
  const char = expr[i];
29
241
  if (!char)
30
242
  continue;
31
- if (quote) {
32
- result += char;
33
- if (escaped) {
34
- escaped = false;
35
- }
36
- else if (char === '\\') {
37
- escaped = true;
38
- }
39
- else if (char === quote) {
40
- quote = null;
41
- }
243
+ const commentEnd = celRegionCommentEnd(expr, i);
244
+ if (commentEnd > i) {
245
+ result += expr.slice(i, commentEnd);
246
+ i = commentEnd - 1;
42
247
  continue;
43
248
  }
44
- if (char === '"' || char === "'") {
45
- quote = char;
46
- result += char;
249
+ const literalEnd = celStringLiteralEndAt(expr, i, expr[i - 1]);
250
+ if (literalEnd > i) {
251
+ result += expr.slice(i, literalEnd);
252
+ i = literalEnd - 1;
47
253
  continue;
48
254
  }
49
255
  if (char === '.') {
@@ -52,11 +258,10 @@ export function normalizeCelArrayIndexPaths(expr) {
52
258
  while (digitEnd < expr.length && /\d/.test(expr[digitEnd] ?? '')) {
53
259
  digitEnd++;
54
260
  }
55
- const previous = expr[i - 1] ?? '';
56
261
  const next = expr[digitEnd] ?? '';
57
262
  if (digitEnd > digitStart &&
58
- /[A-Za-z_$\]]/.test(previous) &&
59
- (next === '' || /[.\])}\s?:,+\-*/<>=!&|]/.test(next))) {
263
+ endsWithIndexableTarget(result) &&
264
+ (next === '' || CEL_INDEX_RUN_TERMINATOR.test(next))) {
60
265
  result += `[${expr.slice(digitStart, digitEnd)}]`;
61
266
  i = digitEnd - 1;
62
267
  continue;
@@ -66,6 +271,136 @@ export function normalizeCelArrayIndexPaths(expr) {
66
271
  }
67
272
  return result;
68
273
  }
274
+ /**
275
+ * Rewrite every dotted numeric path segment as a CEL index: `items.0` → `items[0]`.
276
+ *
277
+ * CEL has no `.0` field select — a `SELECT` takes an `IDENT`, and an `IDENT` may
278
+ * not begin with a digit — so a dotted numeric segment is a parse error on both
279
+ * evaluation engines and `[0]` is the only spelling of that access. Paths reach
280
+ * this module in the dotted form from several producers (`extractNestedStatusCel`
281
+ * builds an array element's key as `` `${fieldPath}.${index}` ``, and the marker
282
+ * field-path charset admits that form too), so this is the sweep that makes them
283
+ * valid.
284
+ *
285
+ * **The rule.** A `.` followed by one or more digits becomes `[<digits>]` when
286
+ * both contexts allow it:
287
+ *
288
+ * 1. *Left* — the text EMITTED SO FAR ends with a CEL identifier or with a `]`
289
+ * ({@link endsWithIndexableTarget}). Reading the left context off the OUTPUT
290
+ * rather than the input is what lets a run of numeric segments chain: in
291
+ * `a.0.1.b` the first run emits `a[0]`, so the second run sees the `]` this
292
+ * function has just written and the whole path comes out as `a[0][1].b`. Read
293
+ * off the INPUT the second run would see the digit `0`, stop, and leave the
294
+ * equally invalid `a[0].1.b` behind. This is the same semantics
295
+ * {@link splitFieldPathSegments} applies to a field path — a whole-digit
296
+ * segment is an index on the segment before it — now agreed on by both.
297
+ *
298
+ * NUMBER literals are left alone by that same test: in `1.0` and `2.5e3` the
299
+ * digits before the `.` are not anchored by an identifier start, so the
300
+ * fraction survives, and a leading `.5` has no left context at all. Where the
301
+ * digits ARE the tail of an identifier, the index reading is the only one CEL
302
+ * has, because `a1.5` is not a float literal — a NUMBER may not begin with an
303
+ * identifier character — which is why `v2.0` becomes `v2[0]` while `v2.name`,
304
+ * having no digit run to convert, is untouched.
305
+ *
306
+ * `)` is deliberately NOT a left context: a postfix that lands after a
307
+ * parenthesised nested-status expansion is normalised at its source, by
308
+ * {@link splitFieldPathSegments}, where the segment structure is still known.
309
+ *
310
+ * 2. *Right* — the run ends the text, or is followed by a character that can
311
+ * neither continue a number nor start an identifier
312
+ * ({@link CEL_INDEX_RUN_TERMINATOR}). This is what keeps the `5` of `2.5e3`
313
+ * out independently of the left-context test.
314
+ *
315
+ * Quoted data is skipped, and skipped with the WHOLE `STRING_LIT`/`BYTES_LIT`
316
+ * lexis: `r`/`R`/`b`/`B` prefixes in either order, the two triple-quoted forms,
317
+ * and escapes in the non-raw forms only ({@link celStringLiteralEndAt}).
318
+ * `//` COMMENTS are skipped whole too, but only where the text is CEL — see
319
+ * {@link celRegionCommentEnd}, and the boundary in the next paragraph. What is
320
+ * deliberately NOT used is a WHOLE-TEXT mask,
321
+ * {@link maskClosedCelLiteralsAndComments} or `celStringLiteralSpans`: this
322
+ * function runs over KRO MIXED-TEMPLATE text rather than over CEL alone, its
323
+ * inputs include values like `` `http://${string(service.spec.ports.0.port)}` ``,
324
+ * and a whole-text walk would read that URL scheme's `//` — which sits in
325
+ * LITERAL text, where CEL's lexis does not reach — as a `COMMENT` and blank the
326
+ * template that follows, silently dropping this rewrite.
327
+ *
328
+ * **Where the rewrite applies.** The inputs above are KRO MIXED TEMPLATES: a
329
+ * `${ … }` CEL region embedded in LITERAL text that KRO emits verbatim. Only
330
+ * the CEL regions are rewritten; literal text is copied through untouched. A
331
+ * `.<digits>` run after an identifier is a perfectly ordinary thing for literal
332
+ * text to contain — the `v1.2` of a URL path, an `image:tag.1`, a `file.txt.1`,
333
+ * a version string like `alpha.3` — and none of those is a CEL index. Applying
334
+ * the rule to the whole string corrupted them into `v1[2]` and the like,
335
+ * changing text KRO would have emitted as written.
336
+ *
337
+ * That split is also the boundary of CEL's COMMENT rule. A region's body and a
338
+ * bare CEL expression are CEL, so a `//` outside a string literal there begins a
339
+ * comment and is copied through whole; literal template text is not CEL, is
340
+ * never comment-scanned, and keeps its `http://`. The two never collide: a URL
341
+ * that appears inside a region is inside a STRING_LIT, which is skipped whole
342
+ * before the `//` is ever reached.
343
+ *
344
+ * A region's end is found by brace balancing that is STRING-LITERAL AND COMMENT
345
+ * AWARE ({@link celTemplateRegionEnd}), because a CEL region may legitimately
346
+ * contain a `{` or `}` that closes nothing — inside a map literal, inside a
347
+ * STRING_LIT as in `${"}" + a.0}`, or inside a comment as in `${a.0 // {`.
348
+ * Each region is rewritten independently, from an empty left context, so a run
349
+ * never chains across the literal text between two regions.
350
+ *
351
+ * Text with no `${` is rewritten whole only when it is GENUINELY BARE CEL, and
352
+ * that takes a second condition: no `__KUBERNETES_REF_…__` marker either. Bare
353
+ * CEL is the form `getInnerCelPath` and `markerToCelPath` build, and the form
354
+ * the marker and nested-status resolvers hand over. Being CEL, it is read with
355
+ * the comment rule too.
356
+ *
357
+ * MARKER-LADEN TEXT is the other thing that arrives without a `${`: a string
358
+ * derived from a template literal whose interpolations coerced to markers, as
359
+ * in `http://__KUBERNETES_REF___schema___spec.name__:8080/api/v1.2`. That is
360
+ * literal text with references embedded in it, not an expression — its `v1.2`
361
+ * is a URL path segment — yet the whole-string branch rewrote it to `v1[2]`,
362
+ * the very corruption the region split above exists to prevent, arriving by the
363
+ * other door. So text carrying a marker is copied through untouched
364
+ * ({@link CARRIES_KUBERNETES_REF_MARKER}), and nothing is lost by that: a
365
+ * marker's OWN dotted digits are normalised when the marker itself is converted,
366
+ * by {@link markerToCelPath}, which calls this function on the bare
367
+ * `<resourceId>.<fieldPath>` path — which, carrying no marker, still takes the
368
+ * whole-string branch.
369
+ *
370
+ * A text may hold BOTH markers and `${ … }` regions. The region split already
371
+ * gets that right — regions are rewritten, the literal text between them,
372
+ * markers included, is copied — so the marker test guards only the whole-string
373
+ * branch.
374
+ */
375
+ export function normalizeCelArrayIndexPaths(expr) {
376
+ if (!expr.includes('${')) {
377
+ // Literal template text that merely embeds references — not an expression.
378
+ if (CARRIES_KUBERNETES_REF_MARKER.test(expr))
379
+ return expr;
380
+ // No `${` and no marker — a bare CEL expression, rewritten whole.
381
+ return rewriteCelIndexPathsInRegion(expr);
382
+ }
383
+ let result = '';
384
+ let index = 0;
385
+ while (index < expr.length) {
386
+ const open = expr.indexOf('${', index);
387
+ if (open === -1) {
388
+ result += expr.slice(index);
389
+ break;
390
+ }
391
+ // Literal text before the region: emitted verbatim, never rewritten.
392
+ result += expr.slice(index, open);
393
+ const close = celTemplateRegionEnd(expr, open + 2);
394
+ if (close === -1) {
395
+ // Unterminated region — copy the remainder through untouched.
396
+ result += expr.slice(open);
397
+ break;
398
+ }
399
+ result += `\${${rewriteCelIndexPathsInRegion(expr.slice(open + 2, close))}}`;
400
+ index = close + 1;
401
+ }
402
+ return result;
403
+ }
69
404
  // ---------------------------------------------------------------------------
70
405
  // Primitive helpers
71
406
  // ---------------------------------------------------------------------------
@@ -178,11 +513,16 @@ function generateCelExpression(ref, context) {
178
513
  const isNestedComp = ref.__nestedComposition === true;
179
514
  if (isNestedComp && context?.nestedStatusCel) {
180
515
  const fieldName = ref.fieldPath.replace(/^status\./, '');
181
- const innerExpr = context.resourceIds?.has(ref.resourceId)
182
- ? lookupNestedExpression(ref.resourceId, fieldName, context.nestedStatusCel, false)
183
- : lookupNestedExpression(ref.resourceId, fieldName, context.nestedStatusCel);
184
- if (innerExpr !== undefined) {
185
- return finalizeCelForKro(innerExpr, context.nestedStatusCel, context, true);
516
+ // A ref path can reach past the mapping key (`status.addr.ip` against an
517
+ // `addr` entry); the remainder is re-attached to the finalized expression.
518
+ const resolution = resolveNestedField(ref.resourceId, fieldName, context.nestedStatusCel, context.resourceIds?.has(ref.resourceId) !== true, false);
519
+ if (resolution !== undefined) {
520
+ // Seed the entry we just looked up so a self-referential mapping stops
521
+ // at its concrete resource reference instead of expanding again.
522
+ const finalized = finalizeCelForKro(resolution.entry.expression, context.nestedStatusCel, context, true, resolution.entry.key);
523
+ const withPostfix = applyKroSegmentPostfix(finalized, resolution.postfix);
524
+ if (withPostfix !== undefined)
525
+ return withPostfix;
186
526
  }
187
527
  }
188
528
  const expression = normalizeCelArrayIndexPaths(`${resolveResourceIdAlias(ref.resourceId, context)}.${ref.fieldPath}`);
@@ -357,17 +697,30 @@ export function isStaticExpression(expr, nestedStatusCel, resourceIds, resolveKn
357
697
  // Nested composition reference resolution
358
698
  // ---------------------------------------------------------------------------
359
699
  /**
360
- * Maximum number of substitution passes when resolving nested composition
361
- * references in {@link resolveNestedCompositionRefs}. The fixed-point loop
362
- * normally converges in one or two passes (one per level of nesting), so
363
- * 16 is a comfortable cap that handles pathologically deep compositions
364
- * without giving runaway substitution loops a chance to wedge serialization.
700
+ * Maximum nesting depth when resolving nested composition references in
701
+ * {@link resolveNestedCompositionRefs}. One level is consumed per
702
+ * `(resourceId, field)` entry expanded on the current recursion path, so a
703
+ * three-level composition uses three levels. Cycles are caught by the
704
+ * in-progress set rather than by this limit, which therefore only fires for
705
+ * genuinely deep — acyclic — composition chains.
365
706
  *
366
707
  * Hitting this limit indicates a real bug in the resolution table — most
367
- * likely a cycle introduced by a faulty alias entry — not a legitimate
368
- * composition shape.
708
+ * likely an unexpectedly deep chain introduced by a faulty alias entry — not
709
+ * a legitimate composition shape, so under strict CEL diagnostics it fails
710
+ * the serialization instead of emitting a partially-resolved expression.
369
711
  */
370
712
  const NESTED_REF_RESOLUTION_DEPTH_LIMIT = 16;
713
+ /**
714
+ * Pattern matching a `<id>.status.<fieldPath>` nested-composition token.
715
+ *
716
+ * The fieldPath capture is greedy on dots so paths like `components.app` are
717
+ * captured whole — that's the form `nestedStatusCel` keys use after recursive
718
+ * extraction. Always instantiate a fresh `RegExp` from this source: the
719
+ * substitution walker is re-entrant (its replacer recurses into another
720
+ * `String.replace`), so a shared `/g` regex would have its `lastIndex`
721
+ * clobbered mid-scan.
722
+ */
723
+ const NESTED_STATUS_TOKEN_SOURCE = String.raw `\b([a-zA-Z_$][\w$]*)\.status\.([a-zA-Z_$][\w$.]*)`;
371
724
  /**
372
725
  * Look up a nested composition's analyzed expression by `(resourceId, fieldName)`.
373
726
  *
@@ -375,7 +728,7 @@ const NESTED_REF_RESOLUTION_DEPTH_LIMIT = 16;
375
728
  * every code path that needs to find an inner expression from a
376
729
  * `nestedStatusCel` table — both the structured-ref paths
377
730
  * (`generateCelExpression`, `serializeStatusMappingsToCel`) and the
378
- * string-resolver path (`substituteNestedRefsOnce`,
731
+ * string-resolver path (`substituteNestedRefsInText`,
379
732
  * `resolveNestedRefMarkers`).
380
733
  *
381
734
  * Returns `undefined` when no match is found, leaving the caller to
@@ -402,12 +755,21 @@ const NESTED_REF_RESOLUTION_DEPTH_LIMIT = 16;
402
755
  * Ambiguous matches (multiple candidates from the prefix or field-name
403
756
  * strategies) emit a warning log and return `undefined` so the caller
404
757
  * can fall through to its own error handling.
758
+ *
759
+ * Returns the matched entry together with the key it is stored under, which
760
+ * strategies 2-4 can reach through an alias. See {@link NestedStatusEntry}.
405
761
  */
406
- export function lookupNestedExpression(resourceId, fieldName, nestedStatusCel, allowFieldFallback = true) {
762
+ function lookupNestedEntry(resourceId, fieldName, nestedStatusCel, allowFieldFallback = true) {
763
+ const entryFor = (key) => {
764
+ if (key === undefined)
765
+ return undefined;
766
+ const expression = nestedStatusCel[key];
767
+ return expression === undefined ? undefined : { key, expression };
768
+ };
407
769
  // Strategy 1: exact match.
408
770
  const exactKey = `__nestedStatus:${resourceId}:${fieldName}`;
409
771
  if (Object.hasOwn(nestedStatusCel, exactKey)) {
410
- return nestedStatusCel[exactKey];
772
+ return entryFor(exactKey);
411
773
  }
412
774
  // Gather all entries for the requested field name once — strategies
413
775
  // 2-4 all need this list.
@@ -427,8 +789,7 @@ export function lookupNestedExpression(resourceId, fieldName, nestedStatusCel, a
427
789
  const refBase = resourceId.replace(/\d+$/, '');
428
790
  const baseNameMatches = fieldMatches.filter((m) => m.baseId.replace(/\d+$/, '') === refBase);
429
791
  if (baseNameMatches.length === 1) {
430
- const match = baseNameMatches[0];
431
- return match ? nestedStatusCel[match.key] : undefined;
792
+ return entryFor(baseNameMatches[0]?.key);
432
793
  }
433
794
  // Strategy 3: unambiguous camelCase / case-insensitive prefix.
434
795
  const prefixMatches = fieldMatches.filter((m) => {
@@ -448,8 +809,7 @@ export function lookupNestedExpression(resourceId, fieldName, nestedStatusCel, a
448
809
  baseStem.toLowerCase() === resourceId.toLowerCase());
449
810
  });
450
811
  if (prefixMatches.length === 1) {
451
- const match = prefixMatches[0];
452
- return match ? nestedStatusCel[match.key] : undefined;
812
+ return entryFor(prefixMatches[0]?.key);
453
813
  }
454
814
  if (prefixMatches.length > 1) {
455
815
  logger.warn('Ambiguous nested composition prefix match', {
@@ -463,8 +823,7 @@ export function lookupNestedExpression(resourceId, fieldName, nestedStatusCel, a
463
823
  if (!allowFieldFallback)
464
824
  return undefined;
465
825
  if (fieldMatches.length === 1) {
466
- const match = fieldMatches[0];
467
- return match ? nestedStatusCel[match.key] : undefined;
826
+ return entryFor(fieldMatches[0]?.key);
468
827
  }
469
828
  // Field-only fallback is intentionally best-effort and fully silent on
470
829
  // ambiguity. Prefix/base-name ambiguity still logs above, but this final
@@ -472,6 +831,244 @@ export function lookupNestedExpression(resourceId, fieldName, nestedStatusCel, a
472
831
  // like `ready` that appear across many unrelated nested compositions.
473
832
  return undefined;
474
833
  }
834
+ /**
835
+ * Look up a nested composition's analyzed expression by `(resourceId, fieldName)`.
836
+ *
837
+ * Thin projection of {@link lookupNestedEntry} for callers that only need the
838
+ * expression text. Anything resolving a DOTTED path wants
839
+ * {@link resolveNestedField} instead.
840
+ */
841
+ export function lookupNestedExpression(resourceId, fieldName, nestedStatusCel, allowFieldFallback = true) {
842
+ return lookupNestedEntry(resourceId, fieldName, nestedStatusCel, allowFieldFallback)?.expression;
843
+ }
844
+ /**
845
+ * Resolve the longest dotted prefix of `fieldPath` that names a mapping entry
846
+ * for `id`, returning the unmatched remainder as a postfix.
847
+ *
848
+ * `nestedStatusCel` keys are field PATHS — `ready`, but also `components.app`
849
+ * — so the token capture is greedy on dots and routinely reaches past the key
850
+ * it should match: `svc.status.addr.ip` when the key is `addr`,
851
+ * `svc.status.items.size()` when the key is `items`,
852
+ * `svc.status.phase.startsWith("x")` when the key is `phase`. Looking the whole
853
+ * captured path up either finds nothing (leaving a virtual id in the emitted
854
+ * RGD) or lets the field-name fallback match an unrelated entry. Resolving the
855
+ * longest prefix instead, and re-attaching the remainder to the parenthesized
856
+ * substitution, preserves the postfix operation: `(inner).ip`, `(inner).size()`,
857
+ * `(inner).startsWith("x")`.
858
+ *
859
+ * A path segment can also carry an INDEX chain — `items[0].name`,
860
+ * `ports["http"].port`, `a.b[0][1].c` — so the path is lexed by
861
+ * {@link splitFieldPathSegments} rather than split on `.`. Keys are matched on
862
+ * the bare names; the index chain of the last matched segment plus every
863
+ * remaining segment rides along verbatim as the postfix, giving `(inner)[0].name`
864
+ * and `(inner)["http"].port`.
865
+ *
866
+ * A list index reaches this function in EITHER of two spellings — bracketed
867
+ * (`items[0].name`, what a proxy field path carries) or DOTTED
868
+ * (`items.0.name`, what `extractNestedStatusCel` builds its keys from). The
869
+ * lexer folds the dotted form into the bracketed one, and
870
+ * {@link keyPrefixCandidates} spells a numeric index back out as `.0` when it
871
+ * builds a key path, so the two spellings resolve identically: the emitted CEL
872
+ * always uses `[0]` (`.0` is not a field select and parses on neither engine)
873
+ * and key matching always uses `.0` (the spelling keys are stored under).
874
+ *
875
+ * Exact `__nestedStatus:<id>:<prefix>` keys are checked at every prefix length
876
+ * BEFORE the alias ladder runs at any length, so a real key (`addr`) always
877
+ * beats a fuzzy match on a longer path (`addr.ip`).
878
+ *
879
+ * `trailingSegmentIsMethodName` reports that the captured path is immediately
880
+ * followed by `(` in the source text, which makes its last segment a method
881
+ * name rather than a field — never a candidate key. It is ignored for a
882
+ * single-segment path, which has no shorter prefix to fall back to and so keeps
883
+ * its existing behaviour.
884
+ */
885
+ function resolveNestedField(id, fieldPath, nestedStatusCel, allowFieldFallback, trailingSegmentIsMethodName) {
886
+ const candidates = keyPrefixCandidates(splitFieldPathSegments(fieldPath), trailingSegmentIsMethodName);
887
+ // Pass 1: exact keys only, longest prefix first.
888
+ for (const candidate of candidates) {
889
+ const key = `__nestedStatus:${id}:${candidate.keyPath}`;
890
+ const expression = nestedStatusCel[key];
891
+ if (expression !== undefined) {
892
+ return { entry: { key, expression }, postfix: candidate.postfix };
893
+ }
894
+ }
895
+ // Pass 2: the full alias ladder, longest prefix first.
896
+ for (const candidate of candidates) {
897
+ const entry = lookupNestedEntry(id, candidate.keyPath, nestedStatusCel, allowFieldFallback);
898
+ if (entry !== undefined)
899
+ return { entry, postfix: candidate.postfix };
900
+ }
901
+ return undefined;
902
+ }
903
+ /**
904
+ * Every prefix of `segments` that could name a `__nestedStatus:<id>:<path>`
905
+ * key, longest first.
906
+ *
907
+ * A prefix may end at a name (`items`, postfix `[0].name`) or after any leading
908
+ * run of its NUMERIC indexes (`items.0`, postfix `.name`) — a numeric index is
909
+ * spellable in a key, because {@link extractNestedStatusCel} builds an array
910
+ * element's key as `` `${fieldPath}.${index}` ``. A NON-numeric index
911
+ * (`["http"]`) has no key spelling, so it ends key candidacy: neither it nor
912
+ * anything past it can be part of a key, and matching a longer prefix across it
913
+ * would silently drop the index from the emitted expression.
914
+ */
915
+ function keyPrefixCandidates(segments, trailingSegmentIsMethodName) {
916
+ const nameLimit = trailingSegmentIsMethodName && segments.length > 1 ? segments.length - 1 : segments.length;
917
+ /** The path past `count` segments plus `depth` of the last one's indexes. */
918
+ const postfixFrom = (count, depth) => (segments[count - 1]?.indexes.slice(depth).join('') ?? '') +
919
+ segments
920
+ .slice(count)
921
+ .map((segment) => `.${segment.name}${segment.indexes.join('')}`)
922
+ .join('');
923
+ // Built shortest-first so each key path extends the previous one, then
924
+ // reversed — the caller wants the longest prefix tried first.
925
+ const candidates = [];
926
+ let keyPath = '';
927
+ for (let count = 1; count <= nameLimit; count += 1) {
928
+ const segment = segments[count - 1];
929
+ if (segment === undefined)
930
+ break;
931
+ keyPath = count === 1 ? segment.name : `${keyPath}.${segment.name}`;
932
+ candidates.push({ keyPath, postfix: postfixFrom(count, 0) });
933
+ let depth = 0;
934
+ for (const index of segment.indexes) {
935
+ const digits = NUMERIC_INDEX_PATTERN.exec(index)?.[1];
936
+ if (digits === undefined)
937
+ break;
938
+ depth += 1;
939
+ keyPath = `${keyPath}.${digits}`;
940
+ candidates.push({ keyPath, postfix: postfixFrom(count, depth) });
941
+ }
942
+ // A non-numeric index was reached: no longer prefix can name a key.
943
+ if (depth < segment.indexes.length)
944
+ break;
945
+ }
946
+ return candidates.reverse();
947
+ }
948
+ /** A whole path segment that is nothing but digits — a DOTTED list index. */
949
+ const DOTTED_NUMERIC_SEGMENT_PATTERN = /^\d+$/;
950
+ /** An index operation that is a plain integer, capturing its digits. */
951
+ const NUMERIC_INDEX_PATTERN = /^\[(\d+)\]$/;
952
+ /**
953
+ * Lex a status field path into `name` + index-chain segments.
954
+ *
955
+ * `split('.')` is wrong for any path carrying an index: it yields `items[0]`,
956
+ * which never equals the mapping key `items`, so the longest-prefix search in
957
+ * {@link resolveNestedField} finds nothing and a virtual id survives into the
958
+ * emitted RGD. Both non-regex callers can hand over such a path — a marker
959
+ * field path admits `[\d+]` (`KUBERNETES_REF_MARKER_FIELD_PATH_SOURCE`, e.g.
960
+ * `status.ports[0].port`) and a `KubernetesRef.fieldPath` reaches this function
961
+ * unmodified — so the shape has to be lexed rather than assumed away. The regex
962
+ * caller cannot: its capture stops at `[`, which leaves the index chain outside
963
+ * the match and therefore untouched in the surrounding text.
964
+ *
965
+ * **An all-digit segment is an INDEX on the segment before it**, not a name:
966
+ * `items.0.name` ≡ `items[0].name`. Both spellings are live here — a proxy
967
+ * renders a numeric key as `[0]` (`schema-proxy.ts`) but
968
+ * `extractNestedStatusCel` builds an array element's mapping key as
969
+ * `` `${fieldPath}.${index}` `` — and the marker charset admits the dotted form
970
+ * too. Folding it into the previous segment's index chain is what makes the two
971
+ * spellings one thing: without it `.0` is lexed as a NAME, so the emitted
972
+ * postfix is `.0.name` (`(inner).0.name` is not CEL — `.0` is not a field
973
+ * select — and parses on neither engine), and a key stored under one spelling
974
+ * cannot be reached from the other. Only a WHOLE segment of digits folds:
975
+ * `v2`, `ip4` and `_0` are identifiers and are left alone.
976
+ *
977
+ * A LEADING numeric segment has no predecessor to index, so it stays a name: a
978
+ * field path is rooted at a field, an index there is not a spelling of anything
979
+ * valid, and leaving it verbatim resolves it exactly as before rather than
980
+ * inventing a meaning for it. (The regex token path cannot produce one — its
981
+ * capture must start with `[a-zA-Z_$]`.)
982
+ *
983
+ * Brackets are matched with a depth counter and quotes are honoured, so neither
984
+ * a `.` nor a `]` inside a map key is read as structure. A path this lexer
985
+ * cannot account for — an unbalanced bracket, an empty segment — is handed back
986
+ * as ONE unsplit segment, so such a path resolves exactly as it did before
987
+ * (whole-path key lookup, no prefix search) instead of being guessed at.
988
+ */
989
+ function splitFieldPathSegments(fieldPath) {
990
+ const unsplit = [{ name: fieldPath, indexes: [] }];
991
+ const segments = [];
992
+ let index = 0;
993
+ for (;;) {
994
+ const nameStart = index;
995
+ while (index < fieldPath.length && fieldPath[index] !== '.' && fieldPath[index] !== '[') {
996
+ index += 1;
997
+ }
998
+ const name = fieldPath.slice(nameStart, index);
999
+ if (name === '')
1000
+ return unsplit;
1001
+ const indexes = [];
1002
+ while (fieldPath[index] === '[') {
1003
+ const close = closingIndexBracket(fieldPath, index);
1004
+ if (close === undefined)
1005
+ return unsplit;
1006
+ indexes.push(fieldPath.slice(index, close + 1));
1007
+ index = close + 1;
1008
+ }
1009
+ const previous = segments[segments.length - 1];
1010
+ if (previous !== undefined && DOTTED_NUMERIC_SEGMENT_PATTERN.test(name)) {
1011
+ previous.indexes.push(`[${name}]`, ...indexes);
1012
+ }
1013
+ else {
1014
+ segments.push({ name, indexes });
1015
+ }
1016
+ if (index === fieldPath.length)
1017
+ return segments;
1018
+ if (fieldPath[index] !== '.')
1019
+ return unsplit;
1020
+ index += 1;
1021
+ }
1022
+ }
1023
+ /** Offset of the `]` closing the `[` at `open`, or `undefined` when unbalanced. */
1024
+ function closingIndexBracket(text, open) {
1025
+ let depth = 0;
1026
+ let quote;
1027
+ let escaped = false;
1028
+ for (let index = open; index < text.length; index += 1) {
1029
+ const character = text[index];
1030
+ if (quote !== undefined) {
1031
+ if (escaped)
1032
+ escaped = false;
1033
+ else if (character === '\\')
1034
+ escaped = true;
1035
+ else if (character === quote)
1036
+ quote = undefined;
1037
+ continue;
1038
+ }
1039
+ if (character === '"' || character === "'") {
1040
+ quote = character;
1041
+ continue;
1042
+ }
1043
+ if (character === '[') {
1044
+ depth += 1;
1045
+ continue;
1046
+ }
1047
+ if (character === ']') {
1048
+ depth -= 1;
1049
+ if (depth === 0)
1050
+ return index;
1051
+ }
1052
+ }
1053
+ return undefined;
1054
+ }
1055
+ /**
1056
+ * Re-attach a {@link NestedFieldResolution} postfix to an already-finalized KRO
1057
+ * segment.
1058
+ *
1059
+ * Only a segment that is exactly one `${—}` expression can carry a postfix. A
1060
+ * mixed template (literal text interleaved with several `${—}` segments) has no
1061
+ * single expression to qualify, so the caller keeps the original reference and
1062
+ * lets downstream validation flag it, exactly as for an unresolvable one.
1063
+ */
1064
+ function applyKroSegmentPostfix(segment, postfix) {
1065
+ if (postfix === '')
1066
+ return segment;
1067
+ const body = /^\$\{([\s\S]*)\}$/.exec(segment)?.[1];
1068
+ if (body === undefined || body === '' || body.includes('${'))
1069
+ return undefined;
1070
+ return `\${(${body})${postfix}}`;
1071
+ }
475
1072
  /**
476
1073
  * Transitively substitute nested-composition references inline from a
477
1074
  * lookup table.
@@ -489,67 +1086,261 @@ export function lookupNestedExpression(resourceId, fieldName, nestedStatusCel, a
489
1086
  * {@link containsNoNonSchemaRefs} (or just call
490
1087
  * {@link isStaticExpression} which composes both steps).
491
1088
  *
492
- * Iterates to a fixed point up to {@link NESTED_REF_RESOLUTION_DEPTH_LIMIT}
493
- * substituted expressions may themselves contain nested references that
494
- * become resolvable once the outer reference is inlined (the three-level
495
- * nesting case: L1 L2 L3).
1089
+ * **Resolution is recursive, never re-scanning.** Each `<id>.status.<field>`
1090
+ * token found in the ORIGINAL text is substituted exactly once; the inner
1091
+ * expression it was replaced with is then walked on its own, so substituted
1092
+ * output is never handed back to the scanner. The result is therefore linear
1093
+ * in the total size of the reachable mapping instead of doubling on every
1094
+ * pass (see #200 — repeated whole-string passes produced a 6 MB expression
1095
+ * from a two-level fixture).
1096
+ *
1097
+ * **Cycles are terminal, not fatal.** A token whose mapping is already being
1098
+ * expanded further up the current path — directly self-referential, or via a
1099
+ * cycle — keeps its concrete `<id>.status.<field>` reference. For a flattened
1100
+ * child id that is also a real graph resource (the common case: an inner
1101
+ * composition's `phase` mapping reads `<flattenedId>.status.phase`), that
1102
+ * concrete reference IS the correct answer.
496
1103
  *
497
- * **Lambda variables are skipped.** When the resolved `<id>` is a CEL
498
- * macro lambda variable like the `c` in `.exists(c, c.status == "Ready")`,
499
- * the substitution does NOT fire the variable refers to the macro's
500
- * iteration element, not a nested composition.
1104
+ * **Known resource ids are substituted only inside an expanded nested
1105
+ * boundary.** `resolveKnownNestedResourceRefs` governs the ORIGINAL text
1106
+ * only. Inside an inner expression that has already been proven to come from
1107
+ * a nested composition, exact mappings for concrete resource ids are
1108
+ * authoritative, so substitution there is always allowed (strict lookup —
1109
+ * no field-name fallback).
1110
+ *
1111
+ * Nesting deeper than {@link NESTED_REF_RESOLUTION_DEPTH_LIMIT} levels stops
1112
+ * expanding and is reported by {@link reportNestedRefDepthExceeded}.
1113
+ *
1114
+ * **Lambda variables are skipped, within their scope.** When the resolved
1115
+ * `<id>` is a CEL macro lambda variable like the `c` in
1116
+ * `.exists(c, c.status == "Ready")`, the substitution does NOT fire — the
1117
+ * variable refers to the macro's iteration element, not a nested composition.
1118
+ * The shield is LEXICAL: it covers references inside the macro's body and
1119
+ * nothing else, so the second `svc` in
1120
+ * `list.map(svc, svc.status.x) && svc.status.phase` is still a nested id.
1121
+ * A variable whose scope encloses the point an inner expression was inlined at
1122
+ * stays in scope throughout that inner expression.
1123
+ *
1124
+ * `seedKey` marks a mapping as already being expanded by the caller, given as
1125
+ * the entry's CANONICAL `nestedStatusCel` key. Entry points that look an entry
1126
+ * up themselves and then resolve its text ({@link generateCelExpression},
1127
+ * {@link resolveNestedRefMarkers}, `serializeStatusMappingsToCel`) pass it so a
1128
+ * self-referential mapping is recognized as terminal there too — the resolver
1129
+ * behaves identically no matter which entry point reached it.
501
1130
  */
502
- function resolveNestedCompositionRefs(expr, nestedStatusCel, resourceIds, resolveKnownNestedResourceRefs = true) {
1131
+ function resolveNestedCompositionRefs(expr, nestedStatusCel, resourceIds, resolveKnownNestedResourceRefs = true, seedKey) {
1132
+ return runNestedRefResolution(expr, nestedStatusCel, resourceIds, resolveKnownNestedResourceRefs, seedKey).text;
1133
+ }
1134
+ /** One resolution pass plus the bookkeeping it accumulated. */
1135
+ function runNestedRefResolution(expr, nestedStatusCel, resourceIds, resolveKnownNestedResourceRefs, seedKey) {
503
1136
  if (!nestedStatusCel || Object.keys(nestedStatusCel).length === 0) {
504
- return expr;
505
- }
506
- let current = expr;
507
- let allowKnownResourceSubstitution = resolveKnownNestedResourceRefs;
508
- for (let i = 0; i < NESTED_REF_RESOLUTION_DEPTH_LIMIT; i++) {
509
- const next = substituteNestedRefsOnce(current, nestedStatusCel, resourceIds, allowKnownResourceSubstitution);
510
- if (next === current)
511
- return current;
512
- current = next;
513
- // Once a virtual nested-composition reference has been expanded, its
514
- // analyzed expression may intentionally use a flattened child id that is
515
- // also a concrete graph resource. Exact nested mappings are authoritative
516
- // only inside that already-proven nested boundary.
517
- allowKnownResourceSubstitution = true;
518
- }
519
- logger.warn('Nested composition resolution depth limit exceeded', {
520
- depthLimit: NESTED_REF_RESOLUTION_DEPTH_LIMIT,
521
- expressionPreview: expr.slice(0, 200),
522
- });
523
- return current;
1137
+ return { text: expr, stats: { cycleHits: 0, depthExceeded: false, textPasses: 0, memoHits: 0 } };
1138
+ }
1139
+ const state = {
1140
+ nestedStatusCel,
1141
+ resourceIds,
1142
+ inProgress: new Set(seedKey === undefined ? [] : [seedKey]),
1143
+ memo: new Map(),
1144
+ cycleHits: 0,
1145
+ depthExceeded: false,
1146
+ textPasses: 0,
1147
+ memoHits: 0,
1148
+ };
1149
+ const text = substituteNestedRefsInText(expr, state, resolveKnownNestedResourceRefs, EMPTY_LAMBDA_VARS);
1150
+ if (state.depthExceeded) {
1151
+ reportNestedRefDepthExceeded(expr);
1152
+ }
1153
+ return {
1154
+ text,
1155
+ stats: {
1156
+ cycleHits: state.cycleHits,
1157
+ depthExceeded: state.depthExceeded,
1158
+ textPasses: state.textPasses,
1159
+ memoHits: state.memoHits,
1160
+ },
1161
+ };
524
1162
  }
525
1163
  export function inlineNestedStatusRefs(expr, nestedStatusCel, resourceIds) {
526
1164
  return resolveNestedCompositionRefs(expr, nestedStatusCel, resourceIds);
527
1165
  }
528
1166
  /**
529
- * One pass of nested-reference substitution. See
1167
+ * {@link inlineNestedStatusRefs} plus the resolver's internal counters.
1168
+ *
1169
+ * @internal Test-only. See {@link NestedRefResolutionStats}.
1170
+ */
1171
+ export function inlineNestedStatusRefsWithStats(expr, nestedStatusCel, resourceIds) {
1172
+ return runNestedRefResolution(expr, nestedStatusCel, resourceIds, true, undefined);
1173
+ }
1174
+ /** Ambient lambda-variable scope for a top-level resolution. */
1175
+ const EMPTY_LAMBDA_VARS = new Set();
1176
+ /**
1177
+ * Kro's implicit element variable for a `forEach` collection's `readyWhen`
1178
+ * body. Unlike a CEL macro variable it has no binder in the expression text —
1179
+ * Kro supplies it to the whole body (`yaml.ts` emits `each` as the base id for
1180
+ * a `forEach` resource) — so it has no lexical scope to be inside of and is
1181
+ * shielded everywhere. `cel-validator.ts` reserves it the same way.
1182
+ */
1183
+ const KRO_FOR_EACH_ELEMENT_VAR = 'each';
1184
+ /**
1185
+ * The macro-bound identifiers that shield a reference AT `offset`.
1186
+ *
1187
+ * A lambda variable is bound by ONE macro and means something only inside that
1188
+ * macro's body: in `list.map(svc, svc.status.x) && svc.status.phase` the first
1189
+ * `svc` is the iteration element and the second is a real nested-composition
1190
+ * id. Shielding every occurrence in the text — which is what collecting the
1191
+ * names alone does — leaves that second reference unexpanded and a virtual id
1192
+ * in the emitted RGD. `ambientLambdaVars` are the scopes that enclosed the
1193
+ * point where THIS text was inlined, and enclose all of it.
1194
+ *
1195
+ * The macro list is `all`/`exists`/`exists_one`/`map`/`filter`, each binding
1196
+ * exactly one variable as its first argument — the standard CEL macro set, and
1197
+ * the whole of what cel-js 0.8.2 implements for direct mode. cel-go's
1198
+ * two-variable comprehensions are an opt-in extension neither engine has here,
1199
+ * so there is no second variable to bind.
1200
+ */
1201
+ function lambdaVarsAt(offset, ambientLambdaVars, scopes) {
1202
+ const enclosing = scopes.filter((scope) => offset >= scope.bodyStart && offset < scope.bodyEnd);
1203
+ if (enclosing.length === 0)
1204
+ return ambientLambdaVars;
1205
+ const vars = new Set(ambientLambdaVars);
1206
+ for (const scope of enclosing)
1207
+ vars.add(scope.variable);
1208
+ return vars;
1209
+ }
1210
+ /**
1211
+ * Substitute every nested-composition token in `text` exactly once. See
530
1212
  * {@link resolveNestedCompositionRefs} for the full contract.
1213
+ *
1214
+ * `ambientLambdaVars` carries the macro-bound identifiers that were in scope AT
1215
+ * THE POINT this text was inlined, so a lambda variable stays shielded no
1216
+ * matter how deep the expression it appears in was inlined from. Scopes opened
1217
+ * by this text itself are per-match, not per-text: see {@link lambdaVarsAt}.
531
1218
  */
532
- function substituteNestedRefsOnce(expr, nestedStatusCel, resourceIds, resolveKnownNestedResourceRefs = false) {
533
- const lambdaVars = collectLambdaVars(expr);
534
- // Match `<id>.status.<fieldPath>`. The fieldPath capture is greedy on
535
- // dots so paths like `components.app` are captured whole — that's the
536
- // form `nestedStatusCel` keys use after recursive extraction.
537
- const pattern = /\b([a-zA-Z_$][\w$]*)\.status\.([a-zA-Z_$][\w$.]*)/g;
538
- return expr.replace(pattern, (match, id, field) => {
539
- if (id === 'schema')
540
- return match;
541
- if (lambdaVars.has(id))
542
- return match;
543
- if (resourceIds?.has(id) && !resolveKnownNestedResourceRefs)
544
- return match;
545
- if (resourceIds?.has(id)) {
546
- const strictInnerExpr = lookupNestedExpression(id, field, nestedStatusCel, false);
547
- return strictInnerExpr === undefined ? match : `(${strictInnerExpr})`;
1219
+ function substituteNestedRefsInText(text, state, allowKnownResourceSubstitution, ambientLambdaVars) {
1220
+ state.textPasses += 1;
1221
+ // Scan a copy with every closed CEL string literal AND every `//` line
1222
+ // comment blanked out, so a `<id>.status.<field>`-shaped run of characters
1223
+ // that is really quoted DATA a log message, a URL, an error string — or
1224
+ // commented-out expression text is never rewritten. Both are masked in one
1225
+ // source-ordered pass, so a `//` inside a string stays string and a quote
1226
+ // inside a comment stays comment. The mask preserves length and offsets
1227
+ // exactly (newlines included), so each replacement splices back into the
1228
+ // ORIGINAL text at the offsets the match reported.
1229
+ const scanned = maskClosedCelLiteralsAndComments(text);
1230
+ // Scopes are read off the MASKED copy — a `.map(` inside quoted data or a
1231
+ // comment binds nothing — and their offsets therefore index `text` too.
1232
+ const lambdaScopes = collectCelLambdaScopes(scanned);
1233
+ const pattern = new RegExp(NESTED_STATUS_TOKEN_SOURCE, 'g');
1234
+ let result = '';
1235
+ let copiedUpTo = 0;
1236
+ let match = pattern.exec(scanned);
1237
+ while (match !== null) {
1238
+ const token = match[0];
1239
+ const tokenEnd = match.index + token.length;
1240
+ const replacement = substituteNestedRefToken(token, match[1] ?? '', match[2] ?? '',
1241
+ // A captured path butted straight against `(` ends in a method name.
1242
+ scanned[tokenEnd] === '(', state, allowKnownResourceSubstitution, lambdaVarsAt(match.index, ambientLambdaVars, lambdaScopes));
1243
+ if (replacement !== token) {
1244
+ result += text.slice(copiedUpTo, match.index) + replacement;
1245
+ copiedUpTo = tokenEnd;
548
1246
  }
549
- const innerExpr = lookupNestedExpression(id, field, nestedStatusCel);
550
- if (innerExpr !== undefined)
551
- return `(${innerExpr})`;
552
- return match;
1247
+ match = pattern.exec(scanned);
1248
+ }
1249
+ return copiedUpTo === 0 ? text : result + text.slice(copiedUpTo);
1250
+ }
1251
+ /**
1252
+ * Resolve one `<id>.status.<fieldPath>` token to its replacement text, or back
1253
+ * to `token` itself when it must be left alone.
1254
+ */
1255
+ function substituteNestedRefToken(token, id, fieldPath, trailingSegmentIsMethodName, state, allowKnownResourceSubstitution,
1256
+ /** Macro-bound identifiers in scope at THIS token — see {@link lambdaVarsAt}. */
1257
+ lambdaVars) {
1258
+ if (id === 'schema')
1259
+ return token;
1260
+ if (id === KRO_FOR_EACH_ELEMENT_VAR)
1261
+ return token;
1262
+ if (lambdaVars.has(id))
1263
+ return token;
1264
+ const isKnownResource = state.resourceIds?.has(id) === true;
1265
+ if (isKnownResource && !allowKnownResourceSubstitution)
1266
+ return token;
1267
+ // A concrete graph resource only ever matches an exact nested mapping —
1268
+ // the field-name fallback would let an unrelated composition's field
1269
+ // hijack a real resource reference.
1270
+ const resolution = resolveNestedField(id, fieldPath, state.nestedStatusCel, !isKnownResource, trailingSegmentIsMethodName);
1271
+ if (resolution === undefined)
1272
+ return token;
1273
+ const resolvedInner = expandNestedEntry(resolution.entry, state, lambdaVars);
1274
+ if (resolvedInner === undefined)
1275
+ return token;
1276
+ // Parenthesize to preserve operator precedence in compound expressions, then
1277
+ // re-attach whatever the token reached past the mapping key so a postfix
1278
+ // field access or method call survives the substitution.
1279
+ return `(${resolvedInner})${resolution.postfix}`;
1280
+ }
1281
+ /**
1282
+ * Resolve one `(id, field)` mapping to its fully-substituted text.
1283
+ *
1284
+ * Returns `undefined` when the mapping must NOT be expanded — because it is
1285
+ * already being expanded on the current path (a cycle, so the concrete
1286
+ * reference is the answer) or because the depth guard tripped. Callers keep
1287
+ * the original `<id>.status.<field>` token in that case.
1288
+ */
1289
+ function expandNestedEntry(entry, state, ambientLambdaVars) {
1290
+ // Both sets key off the entry's CANONICAL mapping key. Keying off the token's
1291
+ // own `(id, field)` would let a cycle that turns a corner through an alias
1292
+ // spelling expand the same mapping a second time instead of terminating here,
1293
+ // and would miss the memo for every spelling but the first.
1294
+ const entryKey = entry.key;
1295
+ if (state.inProgress.has(entryKey)) {
1296
+ state.cycleHits++;
1297
+ return undefined;
1298
+ }
1299
+ const memoKey = `${[...ambientLambdaVars].sort().join(',')}\n${entryKey}`;
1300
+ const memoized = state.memo.get(memoKey);
1301
+ if (memoized !== undefined) {
1302
+ state.memoHits++;
1303
+ return memoized;
1304
+ }
1305
+ if (state.inProgress.size >= NESTED_REF_RESOLUTION_DEPTH_LIMIT) {
1306
+ state.depthExceeded = true;
1307
+ return undefined;
1308
+ }
1309
+ const cycleHitsBefore = state.cycleHits;
1310
+ state.inProgress.add(entryKey);
1311
+ let resolved;
1312
+ try {
1313
+ resolved = substituteNestedRefsInText(entry.expression, state, true, ambientLambdaVars);
1314
+ }
1315
+ finally {
1316
+ state.inProgress.delete(entryKey);
1317
+ }
1318
+ // Only cache expansions that did not depend on the in-progress path: a
1319
+ // cycle-truncated result is specific to the path that produced it, so
1320
+ // reusing it elsewhere would under-resolve the expression.
1321
+ if (state.cycleHits === cycleHitsBefore) {
1322
+ state.memo.set(memoKey, resolved);
1323
+ }
1324
+ return resolved;
1325
+ }
1326
+ /**
1327
+ * Report an expansion stopped by {@link NESTED_REF_RESOLUTION_DEPTH_LIMIT}.
1328
+ *
1329
+ * Under strict CEL diagnostics this fails serialization: the emitted
1330
+ * expression still carries unresolved virtual ids that KRO would reject on
1331
+ * the cluster. By default it stays a warning, matching the lenient posture
1332
+ * the rest of the CEL emission layer takes for unprovable expressions.
1333
+ */
1334
+ function reportNestedRefDepthExceeded(expr) {
1335
+ if (isStrictCelDiagnosticsEnabled()) {
1336
+ throw new ConversionError(`Nested composition resolution exceeded ${NESTED_REF_RESOLUTION_DEPTH_LIMIT} levels of nesting (strict CEL diagnostics)`, expr, 'unknown', undefined, undefined, [
1337
+ 'Check the nested composition status mappings for an unexpectedly deep chain or a faulty alias entry',
1338
+ 'Disable strict CEL diagnostics for this factory (strictCelDiagnostics: false) to emit the partially-resolved expression instead',
1339
+ ]);
1340
+ }
1341
+ logger.warn('Nested composition resolution depth limit exceeded', {
1342
+ depthLimit: NESTED_REF_RESOLUTION_DEPTH_LIMIT,
1343
+ expressionPreview: expr.slice(0, 200),
553
1344
  });
554
1345
  }
555
1346
  /**
@@ -575,16 +1366,17 @@ function resolveNestedRefMarkers(str, nestedStatusCel, resourceIds, context) {
575
1366
  return match;
576
1367
  // Strip leading "status." since nestedStatusCel keys use the bare field path.
577
1368
  const fieldPath = path.replace(/^status\./, '');
578
- if (resourceIds?.has(id)) {
579
- const strictInnerExpr = lookupNestedExpression(id, fieldPath, nestedStatusCel, false);
580
- if (strictInnerExpr !== undefined)
581
- return innerExprToYamlSegment(strictInnerExpr, nestedStatusCel, context);
1369
+ // A marker field path is a dot-separated identifier sequence, so it can
1370
+ // reach past the mapping key (`status.addr.ip` against an `addr` entry) but
1371
+ // never carries a method call — `(` is outside the marker charset.
1372
+ const resolution = resolveNestedField(id, fieldPath, nestedStatusCel, resourceIds?.has(id) !== true, false);
1373
+ if (resolution === undefined)
582
1374
  return match;
583
- }
584
- const innerExpr = lookupNestedExpression(id, fieldPath, nestedStatusCel);
585
- if (innerExpr !== undefined)
586
- return innerExprToYamlSegment(innerExpr, nestedStatusCel, context);
587
- return match;
1375
+ // Seed the entry being expanded so the shared resolver treats a
1376
+ // self-referential mapping as terminal here exactly as it does on the
1377
+ // structured-ref path.
1378
+ const segment = innerExprToYamlSegment(resolution.entry.expression, nestedStatusCel, context, resolution.entry.key);
1379
+ return applyKroSegmentPostfix(segment, resolution.postfix) ?? match;
588
1380
  });
589
1381
  }
590
1382
  /**
@@ -614,10 +1406,10 @@ const WRAPPED_BARE_LITERAL_PATTERN = /^\$\{\s*(-?\d+(?:\.\d+)?|true|false|null)\
614
1406
  * `replicas: stack.status.someCount` should preserve the natural numeric
615
1407
  * type because the destination field expects a number.
616
1408
  */
617
- function innerExprToYamlSegment(innerExpr, nestedStatusCel, context) {
1409
+ function innerExprToYamlSegment(innerExpr, nestedStatusCel, context, seedKey) {
618
1410
  // Recursively resolve any further nested refs the inner expression itself
619
1411
  // contains (multi-level nesting).
620
- const resolved = resolveNestedCompositionRefs(innerExpr, nestedStatusCel, context?.resourceIds, true);
1412
+ const resolved = resolveNestedCompositionRefs(innerExpr, nestedStatusCel, context?.resourceIds, true, seedKey);
621
1413
  if (resolved.includes('__KUBERNETES_REF_')) {
622
1414
  // Marker-laden — convert to mixed-template form.
623
1415
  return convertKubernetesRefMarkersTocel(resolved, context);
@@ -657,8 +1449,8 @@ function innerExprToYamlSegment(innerExpr, nestedStatusCel, context) {
657
1449
  * - Plain-CEL input (raw CEL with no markers or `${…}`) → wrapped in
658
1450
  * `${…}` so KRO recognizes it as an expression to evaluate.
659
1451
  */
660
- export function finalizeCelForKro(expr, nestedStatusCel, context, resolveKnownNestedResourceRefs = true) {
661
- const resolved = normalizeCelArrayIndexPaths(resolveNestedCompositionRefs(expr, nestedStatusCel, context?.resourceIds, resolveKnownNestedResourceRefs));
1452
+ export function finalizeCelForKro(expr, nestedStatusCel, context, resolveKnownNestedResourceRefs = true, seedKey) {
1453
+ const resolved = normalizeCelArrayIndexPaths(resolveNestedCompositionRefs(expr, nestedStatusCel, context?.resourceIds, resolveKnownNestedResourceRefs, seedKey));
662
1454
  if (resolved.includes('__KUBERNETES_REF_')) {
663
1455
  return convertKubernetesRefMarkersTocel(resolved, context);
664
1456
  }
@@ -1424,8 +2216,8 @@ export function serializeStatusMappingsToCel(statusMappings, nestedStatusCel, re
1424
2216
  * Centralized here so the two branches don't drift on what "produce
1425
2217
  * KRO status CEL from a resolved expression" means.
1426
2218
  */
1427
- function statusFieldFromExpression(expr, rewriteSchemaRefs = true, resolveKnownNestedResourceRefs = [...nestedCompositionIds].some((id) => new RegExp(`(^|[^\\w$])${escapeRegExpLiteral(id)}\\s*\\.`).test(expr))) {
1428
- const resolved = normalizeCelArrayIndexPaths(resolveNestedCompositionRefs(normalizeLocalResourceExpr(expr), normalizedNestedStatusCel, resourceIds, resolveKnownNestedResourceRefs));
2219
+ function statusFieldFromExpression(expr, rewriteSchemaRefs = true, resolveKnownNestedResourceRefs = [...nestedCompositionIds].some((id) => new RegExp(`(^|[^\\w$])${escapeRegExpLiteral(id)}\\s*\\.`).test(expr)), seedKey) {
2220
+ const resolved = normalizeCelArrayIndexPaths(resolveNestedCompositionRefs(normalizeLocalResourceExpr(expr), normalizedNestedStatusCel, resourceIds, resolveKnownNestedResourceRefs, seedKey));
1429
2221
  if (resolved.includes('__KUBERNETES_REF_')) {
1430
2222
  // Marker-laden — use mixed-template form.
1431
2223
  const converted = convertKubernetesRefMarkersTocel(resolved);
@@ -1447,9 +2239,16 @@ export function serializeStatusMappingsToCel(statusMappings, nestedStatusCel, re
1447
2239
  // it for KRO status emission.
1448
2240
  if (ref.__nestedComposition && normalizedNestedStatusCel) {
1449
2241
  const fieldName = ref.fieldPath.replace(/^status\./, '');
1450
- const innerExpr = lookupNestedExpression(ref.resourceId, fieldName, normalizedNestedStatusCel);
1451
- if (innerExpr !== undefined) {
1452
- return statusFieldFromExpression(innerExpr, true, true);
2242
+ // A ref path can reach past the mapping key (`status.addr.ip` against
2243
+ // an `addr` entry); the remainder rides along as a postfix.
2244
+ const resolution = resolveNestedField(ref.resourceId, fieldName, normalizedNestedStatusCel, true, false);
2245
+ if (resolution !== undefined) {
2246
+ // Seed the entry we just looked up: its mapping may reference its
2247
+ // own flattened resource id, which is terminal, not re-expandable.
2248
+ const statusField = statusFieldFromExpression(resolution.entry.expression, true, true, resolution.entry.key);
2249
+ const withPostfix = applyKroSegmentPostfix(statusField, resolution.postfix);
2250
+ if (withPostfix !== undefined)
2251
+ return withPostfix;
1453
2252
  }
1454
2253
  }
1455
2254
  // Unresolved ref — still run through the status-expression finalizer so