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
@@ -0,0 +1,1997 @@
1
+ /**
2
+ * Dual-dialect compatibility check for emitted status CEL.
3
+ *
4
+ * TypeKro emits one CEL expression per dynamic status field and then runs it
5
+ * through **two different engines**: `cel-js` evaluates it locally in direct
6
+ * mode, and `cel-go` (inside the KRO controller, with KRO's type environment)
7
+ * evaluates it in Kro mode. The two disagree in ways that only surface when the
8
+ * expression meets real data on a live cluster — one engine returns a value and
9
+ * the other rejects the expression or blows up on a missing field.
10
+ *
11
+ * This module runs every emitted status expression through cel-js's own parser
12
+ * **and** a curated denylist of the divergences we have confirmed, so an
13
+ * expression that only one engine accepts fails at serialization time with the
14
+ * expression, the status leaf it came from, and the dialect that rejects it.
15
+ *
16
+ * ## cel-js's parser is not the CEL grammar
17
+ *
18
+ * cel-js 0.8.2 parses a proper subset of CEL, so a `parse()` failure is a fact
19
+ * about **direct mode only**. Its `atomicExpression` rule takes a postfix
20
+ * `.`/`[` after an identifier (and after a map literal, and one index after a
21
+ * list literal) and nowhere else, while the spec's `Member` production takes a
22
+ * postfix on any `Member` and `Primary` includes both `LITERAL` and
23
+ * `"(" Expr ")"` — so `"x".size()`, `[1,2].size()`, `(a).b` and `size(a).b` are
24
+ * all valid CEL that cel-js rejects, and its lexer has neither the exponent
25
+ * `FLOAT_LIT` form nor the `r`/`b`/triple-quoted `STRING_LIT` forms. Inferring
26
+ * "cel-go would reject this too" from a cel-js parse failure is therefore
27
+ * unsound, and the parse half does not do it: it sorts a failure into
28
+ * `not-valid-cel` (a form the *spec's* grammar has no token or production for),
29
+ * `cel-js-rejects-spec-cel` (a positively identified cel-js shortfall — a real
30
+ * divergence) or `cel-js-parse-failure` (no verdict beyond direct mode).
31
+ *
32
+ * Identifying a shortfall *somewhere* in a rejected expression is not by itself
33
+ * a divergence, because the parse may have failed for an unrelated reason: the
34
+ * shortfall in `"x".size() +` is real and the expression is still unfinished,
35
+ * and cel-go refuses it exactly as cel-js does. So the divergence bucket is
36
+ * earned rather than assumed — every identified shortfall is rewritten into a
37
+ * spelling of the same spec production that cel-js does have (`"x".size()` into
38
+ * `__typekro_recv0.size()`, `1e3` into `0.0`), the rest of the text is left
39
+ * untouched, and cel-js is asked again. Only a rewrite that parses proves the
40
+ * shortfall was the whole reason; anything else falls to `cel-js-parse-failure`.
41
+ *
42
+ * ## Why a denylist rather than a real cel-go
43
+ *
44
+ * cel-go is a Go library; there is no in-process cel-go for a TypeScript
45
+ * serializer to consult, and shelling out to one would make `toYaml()` depend
46
+ * on a Go toolchain. The denylist is therefore a deliberately *curated*,
47
+ * documented set of confirmed divergences rather than a general type checker.
48
+ * It is meant to grow: each rule below records what was observed and on which
49
+ * engine, so a new entry can be justified the same way.
50
+ *
51
+ * ## What may fail strict mode
52
+ *
53
+ * A rule may fail strict mode only when the two engines genuinely **diverge** on
54
+ * the form: there is data for which one engine yields a value and the other does
55
+ * not. That bar rules out two tempting kinds of rule, and both are deliberately
56
+ * excluded rather than merely unwritten.
57
+ *
58
+ * - **Anything that needs a CEL *type* to be a divergence.** Syntax does not
59
+ * establish a type. `x[0]` is a list index against one schema and a map lookup
60
+ * against another; `"k" in e` is fine when `e` is a map and rejected when
61
+ * cel-go's type env has typed `e` as a message. The real types live in the
62
+ * referenced resources' Kubernetes schemas, and nothing at this call site has
63
+ * them: the check receives the serialized status map — expression text naming
64
+ * graph resources by id — and TypeKro's `Enhanced<>` types are erased before
65
+ * serialization runs. A rule that guesses a type from bracket shape fails
66
+ * strict mode on valid CEL, so such rules are reported as notes instead.
67
+ * - **Forms both engines treat the same.** Indexing a required list inside a
68
+ * logical chain is the worked example: `size(l) > 0 && l[0].f != ""` returns
69
+ * `false` on an empty `l` in both engines (cel-js short-circuits, cel-go
70
+ * absorbs the error under a deciding `false`), and an out-of-range index
71
+ * errors on both. Nothing diverges, so nothing is reported.
72
+ *
73
+ * What the bar does **not** license is a claim about what KRO ultimately does
74
+ * with the field. Every rule here reasons about the grammar and about published
75
+ * engine behaviour; none of them models cel-go's *type checker*, which runs
76
+ * after the parse with KRO's type environment and function set and rejects
77
+ * plenty of grammatical CEL — `1.string()` parses on any conformant grammar and
78
+ * the checker still refuses it, `string` being a global conversion function
79
+ * rather than a member, and an unknown member function goes the same way. So a
80
+ * finding says "direct mode can never evaluate this field" where that is proven,
81
+ * and leaves KRO's verdict open. A rule stays a `divergence` on the strength of
82
+ * the disagreement it does establish: the engines differ on the *form*, at a
83
+ * stage that needs no type environment, which makes the emitted CEL defective
84
+ * for one of the two targets TypeKro serializes for whatever the other decides.
85
+ *
86
+ * Findings that do not clear the bar are still worth surfacing, so each rule
87
+ * declares a {@link CelDialectFindingKind}: `divergence` findings abort
88
+ * serialization in strict mode, `note` findings never do and are logged in both
89
+ * modes. Strictness otherwise follows the shared CEL diagnostics convention
90
+ * (`strictCelDiagnostics` factory option, `TYPEKRO_STRICT_CEL=1`).
91
+ */
92
+ import { parse } from 'cel-js';
93
+ import { collectCelLambdaScopes, isCelLambdaLocalAt } from '../references/cel-lexical-scanner.js';
94
+ /**
95
+ * Largest expression this module will analyze, in characters.
96
+ *
97
+ * Both halves of the check cost roughly a microsecond per character — cel-js's
98
+ * parser dominates — so the cost is linear in the length of the expression and
99
+ * the only way it can run away is for the expression itself to run away.
100
+ *
101
+ * The budget is set from what TypeKro actually emits. Across the unit suite the
102
+ * median status expression is under 100 characters, the 99th percentile is
103
+ * ~1.6k, and the largest authored one is ~3.3k. 16 KiB leaves roughly five
104
+ * times the headroom over the largest real expression while capping the check
105
+ * at ~16ms for any single leaf.
106
+ *
107
+ * Anything past this is not an authored status field. The known source is a
108
+ * nested composition whose inlined status re-expands into itself, doubling per
109
+ * level until the depth limit stops it — which yields a multi-megabyte
110
+ * expression that no engine can use: cel-js needs seconds to parse it on every
111
+ * direct-mode reconcile, and a ResourceGraphDefinition carrying it is past the
112
+ * Kubernetes object size limit, so the API server refuses it outright.
113
+ */
114
+ export const CEL_DIALECT_MAX_EXPRESSION_LENGTH = 16_384;
115
+ /**
116
+ * How many levels of bracket nesting the span walks will descend.
117
+ *
118
+ * Both the lazy-region blanking and the logical-chain walk recurse once per
119
+ * nested group, and they now run on text cel-js could not parse — where a
120
+ * runaway `((((…` is exactly the shape that shows up. Nesting is bounded by the
121
+ * expression length, so 16 KiB of open parens is 8k levels of recursion and a
122
+ * blown stack; the parse gate used to hide that by returning first.
123
+ *
124
+ * 64 is far past anything an emitted status expression reaches — the deepest in
125
+ * the unit suite is single digits — and stopping the descent only means a
126
+ * finding deeper than that is not reported, which is the safe direction.
127
+ */
128
+ const CEL_DIALECT_MAX_NESTING_DEPTH = 64;
129
+ /**
130
+ * The curated denylist.
131
+ *
132
+ * Every entry names the engine that rejects the form, whether the entry counts
133
+ * as a divergence, and the observation behind it. Keep this table and
134
+ * {@link checkCelDialectCompatibility} in step.
135
+ */
136
+ export const CEL_DIALECT_RULES = [
137
+ {
138
+ id: 'has-index-argument',
139
+ kind: 'divergence',
140
+ dialect: 'cel-js',
141
+ summary: 'has() applied to an index expression',
142
+ observed: 'cel-js raises "has() does not support atomic expressions" whenever the operand of has() is an index — `has(list[0].field)` and `has(map["k"].field)` alike, so this is about the shape of the macro argument and not about the type of what is indexed. cel-go\'s has() macro accepts any select expression, index included',
143
+ },
144
+ {
145
+ id: 'guard-after-use-in-logical-chain',
146
+ kind: 'divergence',
147
+ dialect: 'cel-js',
148
+ summary: 'a has() guard placed to the right of the access it guards, in the operator\'s guarding polarity',
149
+ observed: 'cel-go absorbs an error in one operand of && / || when the other operand decides the result, regardless of order; cel-js evaluates left to right and propagates the error before the guard is ever reached. The divergence needs no type: the guard itself says the author expects the path to be absent sometimes. Which form is the guard follows the operator: `&&` is decided by `false`, so `has(p)` guards there, while `||` is decided by `true`, so `!has(p)` is the guarding form and the late-guard mirror',
150
+ },
151
+ {
152
+ id: 'heterogeneous-map-literal',
153
+ kind: 'divergence',
154
+ dialect: 'cel-js',
155
+ summary: 'a CEL map literal whose values are not all of one type',
156
+ observed: 'cel-js pins a map literal\'s value type to that of its first entry and throws "invalid_argument: <value>" on the first entry that differs (cel-js 0.8.2, `mapExpression` in its visitor), so `{"name": "http", "port": 80}` cannot be evaluated at all; cel-go types the literal as `map(string, dyn)` when the entry types differ and evaluates it. The divergence needs no schema: the differing types are written out in the literal itself. Lists are unaffected — cel-js evaluates `[1, "a"]` — and a single-entry or empty map is always fine',
157
+ },
158
+ {
159
+ id: 'in-on-list-entry',
160
+ kind: 'note',
161
+ dialect: 'cel-go',
162
+ summary: '`in` applied to something that may be a typed list entry',
163
+ observed: "KRO's cel-go type env types a *message* list entry as a message rather than a map and reports \"no matching overload for '@in'\", where cel-js accepts it. Whether the entry here is a message or a map is a fact about the referenced resource's schema, which this check cannot see, so the form is reported for information rather than failed",
164
+ },
165
+ {
166
+ id: 'cel-js-rejects-spec-cel',
167
+ kind: 'divergence',
168
+ dialect: 'cel-js',
169
+ summary: 'a form the CEL grammar permits and cel-js is known not to parse',
170
+ observed: "cel-js 0.8.2 is not a conformant CEL parser. Its `atomicExpression` rule (dist/parser.js) allows a postfix `.`/`[` only after an Identifier — plus one index after a list literal, and any postfix after a map literal — while the spec's `Member = Primary | Member \".\" SELECTOR [\"(\" [ExprList] \")\"] | Member \"[\" Expr \"]\"` allows a postfix on *any* Member, and `Primary` includes `LITERAL` and `\"(\" Expr \")\"` (cel-spec doc/langdef.md, \"Syntax\"). Its lexer is short of the spec's `FLOAT_LIT` (no EXPONENT form) and `STRING_LIT`/`BYTES_LIT` (no `r`/`R`/`b`/`B` prefix, no triple-quoted form). Each form is confirmed to fail `parse()` and is grammatical CEL. The entry fires only once the *whole* expression is shown to be grammatical: the identified forms are replaced by same-production spellings cel-js does have and cel-js is asked again, and only a rewrite cel-js accepts whole — every character lexed, the token stream parsed — establishes that the shortfall is the sole obstruction rather than one of several problems in the text. What that proves is that the grammar permits the form and cel-js's refusal is its own shortfall, so direct mode can never evaluate the field; it is a divergence in the form rather than a defect in the expression. It does not establish what KRO then does with the field: cel-go parses the form, but its type checker runs afterwards with KRO's type environment and function set, which this module does not model — `1.string()` is grammatical and cel-go's checker still rejects it, `string` being a global conversion function rather than a member",
171
+ },
172
+ {
173
+ id: 'not-valid-cel',
174
+ kind: 'note',
175
+ dialect: 'both',
176
+ summary: 'the text contains something no CEL grammar accepts',
177
+ observed: "matched against the spec's own lexical and syntactic grammar rather than against an engine: `=` outside `==`/`!=`/`<=`/`>=` is not a CEL token at all (the punctuation list in cel-spec doc/langdef.md is `() [] {} . , ? : || && ! < <= >= > == != in + - * / %`), `$` is in neither the punctuation list nor `IDENT`, and a `?` with no matching `:` cannot close `Expr = ConditionalOr [\"?\" ConditionalOr \":\" Expr]`. These are the converter-leakage forms — `===`, `!==`, `=>`, `${`, `?.`, `?[`. Neither engine can evaluate them, so this is a defect rather than a divergence. Note cel-js's *lexer* silently drops a character it has no token for, so `a === b` reaches its parser as `a == b` and \"parses\" — this check therefore runs whether or not cel-js parsed",
178
+ },
179
+ {
180
+ id: 'cel-js-parse-failure',
181
+ kind: 'note',
182
+ dialect: 'cel-js',
183
+ summary: 'cel-js cannot parse the expression and no positive verdict was reached',
184
+ observed: "cel-js's parser rejected the text and nothing above accounts for it: either no confirmed cel-js shortfall was identified, or one was and rewriting it away still left text cel-js refuses — which means the expression has a further problem of its own, and an expression that is ungrammatical elsewhere is rejected by cel-go too. A cel-js parse failure on its own says only that *direct mode* cannot evaluate the field: cel-js is not a conformant CEL grammar, so it establishes nothing about cel-go or KRO. Reported so the field is visible, never failed",
185
+ },
186
+ {
187
+ id: 'expression-too-large',
188
+ kind: 'note',
189
+ dialect: 'unchecked',
190
+ summary: 'the expression is past the analysis budget, so neither half of the check ran',
191
+ observed: 'a nested composition whose inlined status re-expands into itself doubles the expression per level, reaching megabytes; parsing one costs seconds per call and the ResourceGraphDefinition carrying it is past the Kubernetes object size limit. No dialect verdict was reached, so there is no divergence to report',
192
+ },
193
+ ];
194
+ /** True when any finding in the set may fail strict mode. */
195
+ export function hasCelDialectDivergence(findings) {
196
+ return findings.some((found) => found.kind === 'divergence');
197
+ }
198
+ const CLOSERS = { ')': '(', ']': '[', '}': '{' };
199
+ /** Counters behind {@link celDialectWorkStats}; see it for why they exist. */
200
+ let workIndexBuilds = 0;
201
+ let workIndexedCharacters = 0;
202
+ let workLookups = 0;
203
+ /** Read the work counters. */
204
+ export function celDialectWorkStats() {
205
+ return {
206
+ indexBuilds: workIndexBuilds,
207
+ indexedCharacters: workIndexedCharacters,
208
+ lookups: workLookups,
209
+ };
210
+ }
211
+ /** Zero the work counters, so one check can be measured on its own. */
212
+ export function resetCelDialectWorkStats() {
213
+ workIndexBuilds = 0;
214
+ workIndexedCharacters = 0;
215
+ workLookups = 0;
216
+ }
217
+ /**
218
+ * Resolve every bracket pair in `text` in one left-to-right stack pass.
219
+ *
220
+ * A closer that does not match the opener on top of the stack is text no scan
221
+ * could have got past either: a walk forward from any opener still on the stack
222
+ * would reach this character with the same pending brackets and give up, so all
223
+ * of them are recorded unmatched and the stack is cleared. An opener that starts
224
+ * *after* the offending closer is unaffected, which is what a forward scan from
225
+ * it would also have found.
226
+ */
227
+ function buildBracketIndex(text) {
228
+ workIndexBuilds += 1;
229
+ workIndexedCharacters += text.length;
230
+ const close = new Int32Array(text.length).fill(-1);
231
+ const stack = [];
232
+ for (let index = 0; index < text.length; index += 1) {
233
+ const character = text[index];
234
+ if (character === '(' || character === '[' || character === '{') {
235
+ stack.push(index);
236
+ continue;
237
+ }
238
+ const opener = CLOSERS[character];
239
+ if (opener === undefined)
240
+ continue;
241
+ const top = stack.length === 0 ? -1 : stack[stack.length - 1];
242
+ if (top >= 0 && text[top] === opener) {
243
+ stack.pop();
244
+ close[top] = index;
245
+ continue;
246
+ }
247
+ stack.length = 0;
248
+ }
249
+ return { close };
250
+ }
251
+ /** Index of the bracket matching the one at `open`, or -1. */
252
+ function matchingParen(brackets, open) {
253
+ workLookups += 1;
254
+ return open >= 0 && open < brackets.close.length ? brackets.close[open] : -1;
255
+ }
256
+ /**
257
+ * Split `[start, end)` on the given separators, ignoring anything nested inside
258
+ * brackets. Separators are matched as whole tokens.
259
+ *
260
+ * A matched bracket group at depth zero is stepped over in one index lookup
261
+ * rather than character by character, which is what keeps the recursive descent
262
+ * in {@link checkChain} from re-reading the same nested text once per level. The
263
+ * depth counter stays for the unbalanced case — a group whose closer is missing
264
+ * or falls outside the span has no entry to jump to, and the old scan is then
265
+ * exactly the right behaviour.
266
+ */
267
+ function splitTopLevel(masked, span, separators, brackets) {
268
+ const parts = [];
269
+ let depth = 0;
270
+ let partStart = span.start;
271
+ let index = span.start;
272
+ while (index < span.end) {
273
+ const character = masked[index];
274
+ if (character === '(' || character === '[' || character === '{') {
275
+ if (depth === 0) {
276
+ const close = matchingParen(brackets, index);
277
+ if (close >= 0 && close < span.end) {
278
+ index = close + 1;
279
+ continue;
280
+ }
281
+ }
282
+ depth += 1;
283
+ index += 1;
284
+ continue;
285
+ }
286
+ if (character === ')' || character === ']' || character === '}') {
287
+ depth -= 1;
288
+ index += 1;
289
+ continue;
290
+ }
291
+ if (depth === 0) {
292
+ const separator = separators.find((candidate) => masked.startsWith(candidate, index));
293
+ if (separator !== undefined) {
294
+ parts.push({ start: partStart, end: index });
295
+ index += separator.length;
296
+ partStart = index;
297
+ continue;
298
+ }
299
+ }
300
+ index += 1;
301
+ }
302
+ parts.push({ start: partStart, end: span.end });
303
+ return parts;
304
+ }
305
+ /** Top-level parenthesized groups inside a span, as interior spans. */
306
+ function parenGroups(masked, span, brackets) {
307
+ const groups = [];
308
+ let index = span.start;
309
+ while (index < span.end) {
310
+ if (masked[index] === '(') {
311
+ const close = matchingParen(brackets, index);
312
+ if (close < 0 || close > span.end)
313
+ break;
314
+ groups.push({ start: index + 1, end: close });
315
+ index = close + 1;
316
+ continue;
317
+ }
318
+ index += 1;
319
+ }
320
+ return groups;
321
+ }
322
+ const DOTTED_PATH = /[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)+/g;
323
+ function blankRange(text, start, end) {
324
+ return text.slice(0, start) + ' '.repeat(Math.max(0, end - start)) + text.slice(end);
325
+ }
326
+ /**
327
+ * Blank out the regions of an expression that are *not* part of the enclosing
328
+ * logical chain's eager evaluation:
329
+ *
330
+ * - **Collection-macro bodies.** `list.exists(c, ...)` binds `c` per entry, and
331
+ * a `has(c.x)` in one operand's lambda says nothing about another operand's
332
+ * lambda variable of the same name.
333
+ * - **Nested ternaries.** A parenthesized group containing a top-level `?` is
334
+ * lazy in both engines, so its branches cannot make the chain diverge. The
335
+ * group is analyzed on its own by the recursion in `checkLogicalChain`.
336
+ *
337
+ * Offsets are preserved so spans stay valid against the original expression.
338
+ *
339
+ * The bracket index is the one built for `masked`, and stays correct here
340
+ * because every region blanked is a bracket *interior*: a lambda body runs from
341
+ * after the macro's `(` to before its `)`, and a lazy ternary group is blanked
342
+ * between its own parens. Blanking therefore only ever removes whole matched
343
+ * pairs, so a bracket that survives into `blanked` still closes where the index
344
+ * says it does, and a bracket that does not survive is a space nothing asks
345
+ * about.
346
+ */
347
+ function blankLazyRegions(masked, brackets) {
348
+ let blanked = masked;
349
+ for (const scope of collectCelLambdaScopes(masked)) {
350
+ blanked = blankRange(blanked, scope.bodyStart, scope.bodyEnd);
351
+ }
352
+ const blankTernaryGroups = (span, depth) => {
353
+ if (depth > CEL_DIALECT_MAX_NESTING_DEPTH)
354
+ return;
355
+ let index = span.start;
356
+ while (index < span.end) {
357
+ if (blanked[index] === '(') {
358
+ const close = matchingParen(brackets, index);
359
+ if (close < 0 || close > span.end)
360
+ return;
361
+ const interior = { start: index + 1, end: close };
362
+ if (splitTopLevel(blanked, interior, ['?'], brackets).length > 1) {
363
+ blanked = blankRange(blanked, interior.start, interior.end);
364
+ }
365
+ else {
366
+ blankTernaryGroups(interior, depth + 1);
367
+ }
368
+ index = close + 1;
369
+ continue;
370
+ }
371
+ index += 1;
372
+ }
373
+ };
374
+ blankTernaryGroups({ start: 0, end: blanked.length }, 0);
375
+ return blanked;
376
+ }
377
+ const GUARD_OPERAND = /^has\s*\(\s*([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)\s*\)\s*$/;
378
+ /**
379
+ * Read one operand as a possibly-negated `has()` guard.
380
+ *
381
+ * Deliberately whole-operand: the operand has to reduce, after stripping
382
+ * whitespace, `!` operators and balanced enclosing parentheses, to exactly
383
+ * `has(<dotted path>)`. That is what makes the *polarity* readable. A `has()`
384
+ * buried in a larger boolean operand — `has(p) == true`, `x || has(p)` inside a
385
+ * conjunct — has no single polarity with respect to the enclosing chain, so it
386
+ * is not read as a guard in either direction: it neither establishes a path nor
387
+ * is reported as a late guard.
388
+ *
389
+ * Group negation is handled to the extent the masked-span machinery allows:
390
+ * `!has(p)`, `!(has(p))` and `!!has(p)` all parse, because stripping is purely
391
+ * lexical and each step keeps the operand a single `has()`. `!(has(p) && q)`
392
+ * does not, and is treated as "not a guard" rather than guessed at — negating a
393
+ * compound is not a statement about `p` on its own.
394
+ */
395
+ function chainGuard(blanked, span, brackets) {
396
+ // Stripping narrows a span rather than re-slicing a string, so the enclosing
397
+ // parens can be matched out of the shared index instead of rescanned.
398
+ let start = span.start;
399
+ let end = span.end;
400
+ let negated = false;
401
+ const trim = () => {
402
+ while (start < end && /\s/.test(blanked[start]))
403
+ start += 1;
404
+ while (end > start && /\s/.test(blanked[end - 1]))
405
+ end -= 1;
406
+ };
407
+ trim();
408
+ while (start < end) {
409
+ if (blanked[start] === '!') {
410
+ negated = !negated;
411
+ start += 1;
412
+ trim();
413
+ continue;
414
+ }
415
+ if (blanked[start] === '(' && matchingParen(brackets, start) === end - 1) {
416
+ start += 1;
417
+ end -= 1;
418
+ trim();
419
+ continue;
420
+ }
421
+ break;
422
+ }
423
+ const path = GUARD_OPERAND.exec(blanked.slice(start, end))?.[1];
424
+ return path === undefined ? undefined : { path, negated };
425
+ }
426
+ /**
427
+ * The path an operand guards for the operands to its right, if any.
428
+ *
429
+ * A guard only guards when its truth value is the chain's absorbing value
430
+ * exactly where the path is missing: `has(p)` is `false` when `p` is absent, so
431
+ * it guards in an `&&` chain; `!has(p)` is `true` when `p` is absent, so it
432
+ * guards in an `||` chain. A guard of the wrong polarity — `!has(p)` in `&&`,
433
+ * `has(p)` in `||` — establishes nothing, because the chain carries on into the
434
+ * access precisely when the path is absent.
435
+ *
436
+ * The same predicate decides the divergent *late* guard, and for the same
437
+ * reason: a late guard diverges only when cel-go's absorption of the earlier
438
+ * error is what decides the chain. In `p.f == 1 && has(p)` cel-go yields
439
+ * `false` where cel-js errors — a divergence — while in `p.f == 1 && !has(p)`
440
+ * cel-go's absorbed error meets a `true` and stays an error, which is what
441
+ * cel-js does too.
442
+ */
443
+ function guardsEstablishedBy(blanked, span, mode, brackets) {
444
+ const guard = chainGuard(blanked, span, brackets);
445
+ if (guard === undefined)
446
+ return [];
447
+ return guard.negated === (mode === 'or') ? [guard.path] : [];
448
+ }
449
+ /** True when `guard` covers `path` — the same path, or an ancestor of it. */
450
+ function guardCovers(guard, path) {
451
+ return path === guard || path.startsWith(`${guard}.`);
452
+ }
453
+ /**
454
+ * True when a guard already written to the left makes a later one redundant.
455
+ *
456
+ * The direction matters and is the opposite of {@link guardCovers}. Covering a
457
+ * path is about *reaching* it: `has(a.status)` covers the dereference
458
+ * `a.status.list[0]` because it guards an ancestor of it. Establishing a guard
459
+ * is about *presence*: only a guard at least as specific as the later one says
460
+ * the later one's path exists.
461
+ *
462
+ * So `has(a.status)` does not establish `has(a.status.list)` — the status object
463
+ * being present says nothing about the list — while `has(a.status.list.deeper)`
464
+ * does, because CEL evaluates the receiver of a `has()` before testing the last
465
+ * field: for `has(a.status.list.deeper)` to have returned true rather than
466
+ * errored, `a.status.list` had to be present.
467
+ *
468
+ * @param earlier A guard written to the left of the operand in question.
469
+ * @param later The guard written to its right.
470
+ */
471
+ function guardEstablishes(earlier, later) {
472
+ return guardCovers(later, earlier);
473
+ }
474
+ /** Dotted paths dereferenced in a span, excluding those that are `has()` arguments. */
475
+ function dereferencedPaths(masked, span, brackets) {
476
+ let slice = masked.slice(span.start, span.end);
477
+ // Blank out has() arguments: naming a path inside has() is not a dereference.
478
+ // The scan runs over the whole text from `span.start` rather than over the
479
+ // slice, so the shared bracket index answers where each `has(` closes; a
480
+ // `has(` whose closer falls outside the span is left alone, which is what
481
+ // matching inside the slice alone used to arrive at.
482
+ const pattern = /\bhas\s*\(/g;
483
+ pattern.lastIndex = span.start;
484
+ const blanks = [];
485
+ let match = pattern.exec(masked);
486
+ while (match !== null && match.index < span.end) {
487
+ const open = match.index + match[0].length - 1;
488
+ const close = matchingParen(brackets, open);
489
+ if (close > open && close < span.end) {
490
+ blanks.push([match.index - span.start, close + 1 - span.start]);
491
+ }
492
+ match = pattern.exec(masked);
493
+ }
494
+ for (const [start, end] of blanks.reverse()) {
495
+ slice = slice.slice(0, start) + ' '.repeat(end - start) + slice.slice(end);
496
+ }
497
+ return [...slice.matchAll(DOTTED_PATH)].map((found) => found[0]);
498
+ }
499
+ /** Mask one expression into the three alignments the rules read. */
500
+ function celDialectText(reported) {
501
+ const { source, masked } = maskCelCommentsAndStrings(reported);
502
+ // One comparison, and the single thing every offset in this module rests on.
503
+ // The failure it guards against is silent rather than loud: a mask that
504
+ // walked by code point would blank an astral character to *one* space, and
505
+ // every span after it would index the wrong characters — a fragment cut one
506
+ // unit short, a bracket index pointing between two tokens, a literal re-lexed
507
+ // from the wrong place — with no symptom but a wrong finding, and only on
508
+ // expressions that happen to carry an emoji.
509
+ if (source.length !== reported.length || masked.length !== reported.length) {
510
+ throw new Error(`cel-dialect: the lexical mask changed the length of the expression (${reported.length} in, ${source.length}/${masked.length} out). Every offset in this module is a UTF-16 code-unit offset shared across all three texts`);
511
+ }
512
+ return { reported, source, masked };
513
+ }
514
+ function finding(rule, field, expression, fragment, message, suggestion) {
515
+ const entry = CEL_DIALECT_RULES.find((candidate) => candidate.id === rule);
516
+ return {
517
+ rule,
518
+ kind: entry?.kind ?? 'note',
519
+ dialect: entry?.dialect ?? 'cel-js',
520
+ field,
521
+ expression,
522
+ ...(fragment === undefined ? {} : { fragment }),
523
+ message,
524
+ suggestion,
525
+ };
526
+ }
527
+ /**
528
+ * Rule: `has()` whose argument is an index expression.
529
+ *
530
+ * Any index counts, numeric or string-keyed. That is not an inference about what
531
+ * is being indexed: cel-js rejects the *shape* of the macro argument, throwing
532
+ * "has() does not support atomic expressions" for `has(list[0].f)` and
533
+ * `has(map["k"].f)` alike, while cel-go's has() accepts any select expression.
534
+ * The divergence is established without knowing a single type.
535
+ *
536
+ * What that establishes is that direct mode can never evaluate the field. It
537
+ * does not establish that KRO does: cel-go's macro expansion is not its type
538
+ * checker, and the checker runs afterwards with KRO's type environment. The
539
+ * message says so rather than promising the field resolves under KRO.
540
+ */
541
+ function checkHasIndexArgument(text, field, findings, brackets) {
542
+ const masked = text.masked;
543
+ const pattern = /\bhas\s*\(/g;
544
+ let match = pattern.exec(masked);
545
+ while (match !== null) {
546
+ const open = match.index + match[0].length - 1;
547
+ const close = matchingParen(brackets, open);
548
+ if (close > open && masked.slice(open + 1, close).includes('[')) {
549
+ const fragment = text.source.slice(match.index, close + 1);
550
+ findings.push(finding('has-index-argument', field, text.reported, fragment, 'cel-js rejects has() whose operand is an index expression ("has() does not support atomic expressions") while cel-go\'s has() macro accepts any select expression, index included. Direct mode can therefore never evaluate this field. Whether KRO evaluates it depends on cel-go\'s type checker and function environment, which this check does not model', 'Select entries with `list.filter(entry, has(entry.field))` inside a lazy ternary — Cel.firstWhereHas() emits exactly that'));
551
+ }
552
+ match = pattern.exec(masked);
553
+ }
554
+ }
555
+ /**
556
+ * A value that is exactly one whole CEL string literal and nothing else.
557
+ *
558
+ * This is cel-js's own `StringLiteral` token pattern anchored to the entire
559
+ * value: either quote style, `\`-escapes, no newline inside. It is deliberately
560
+ * *not* derived from the masked text. `maskCelStringLiterals` overwrites the
561
+ * open quote, the interior and the close quote alike with spaces, and ordinary
562
+ * inter-token whitespace is spaces already, so a masked string is
563
+ * indistinguishable from a run of blanks and the closing quote's offset is not
564
+ * recoverable from the mask — `"a" "b"` and `"a b"` mask identically. Re-lexing
565
+ * the one token off the original text is what actually settles where the
566
+ * literal ends.
567
+ *
568
+ * cel-js's grammar has no raw (`r"..."`), bytes (`b"..."`) or triple-quoted
569
+ * string form, and nothing in the tree emits one, so a value opening with `r` or
570
+ * `b` simply fails to match here and is left unclassified — the safe direction.
571
+ *
572
+ * That is why this pattern deliberately does **not** follow
573
+ * {@link celStringLiteralEnd} in suppressing escapes inside a raw literal: the
574
+ * two answer different questions. `celStringLiteralEnd` asks where a *spec*
575
+ * token ends, so it has to get the raw rule right; this asks whether cel-js
576
+ * would read the value as one of *its* string literals, and cel-js has no raw
577
+ * form to read. Widening this to admit `r"..."` would be a claim about cel-js's
578
+ * typing of a value cel-js cannot even lex, so a raw literal stays `undefined`
579
+ * and takes its map entry out of the comparison, which is where it belongs.
580
+ */
581
+ const WHOLE_STRING_LITERAL = /^(?:"(?:[^"\n\\]|\\[\s\S])*"|'(?:[^'\n\\]|\\[\s\S])*')$/;
582
+ /**
583
+ * The CEL type a value expression visibly *is*, when the syntax settles it.
584
+ *
585
+ * Only *whole* literals are classified, and that is a statement about the entire
586
+ * value rather than about its first character. A value that merely *opens* with
587
+ * a literal is a different type as often as not: `"x".size()` and `[1,2].size()`
588
+ * are ints, `"s".startsWith("t")` is a bool, and `"x" + y` is a string only by
589
+ * luck. So a string value has to match {@link WHOLE_STRING_LITERAL} end to end,
590
+ * and a list or map value is whole only when `matchingParen` of its opening
591
+ * bracket lands on the value's last character. Numbers, bools and `null` are
592
+ * anchored patterns already and stay as they are. `1 + 2` starts with a digit
593
+ * and is still not classified, because the point is to be certain rather than
594
+ * clever: an identifier, a call, a ternary or any arithmetic yields `undefined`
595
+ * and takes its entry out of the comparison entirely.
596
+ *
597
+ * Brackets are matched out of `brackets`, which was built over the *masked*
598
+ * text, so a `]` or `}` sitting inside a string cannot pose as the closer; the
599
+ * classification itself reads `expression`, since masking is what erases the
600
+ * quotes that make a value a string. Masking preserves offsets, so `span` and
601
+ * the index address the same characters.
602
+ *
603
+ * `int` and `double` are separate classes because cel-js separates them —
604
+ * `{"a": 1, "b": 2.5}` is as rejected as `{"a": 1, "b": "x"}`.
605
+ */
606
+ function literalTypeClass(expression, span, brackets) {
607
+ let start = span.start;
608
+ let end = span.end;
609
+ while (start < end && /\s/.test(expression[start]))
610
+ start += 1;
611
+ while (end > start && /\s/.test(expression[end - 1]))
612
+ end -= 1;
613
+ if (start === end)
614
+ return undefined;
615
+ const value = expression.slice(start, end);
616
+ if (value === 'true' || value === 'false')
617
+ return 'bool';
618
+ if (value === 'null')
619
+ return 'null';
620
+ if (/^-?\d+u$/.test(value))
621
+ return 'uint';
622
+ if (/^-?\d+$/.test(value))
623
+ return 'int';
624
+ if (/^-?(?:\d+\.\d*|\.\d+)(?:[eE][+-]?\d+)?$/.test(value))
625
+ return 'double';
626
+ if (WHOLE_STRING_LITERAL.test(value))
627
+ return 'string';
628
+ if (value.startsWith('[') && matchingParen(brackets, start) === end - 1)
629
+ return 'list';
630
+ if (value.startsWith('{') && matchingParen(brackets, start) === end - 1)
631
+ return 'map';
632
+ return undefined;
633
+ }
634
+ /**
635
+ * Rule: a map literal whose entry values are not all of the same CEL type.
636
+ *
637
+ * Every `{...}` in the expression is checked, nested ones included, and the
638
+ * classification is purely syntactic: two entries have to carry *visibly*
639
+ * different literal types before anything is reported, so a map whose values
640
+ * are identifiers or calls is left alone even though cel-js may still refuse it
641
+ * at runtime. Under-reporting is the deliberate direction — the alternative is
642
+ * failing strict mode on a map the checker merely cannot read.
643
+ *
644
+ * Masking matters here: `masked` settles the structure, since a `,` or `:`
645
+ * inside a string is not a separator, while the classification reads the
646
+ * original text, since masking is what erases the quotes that make a value a
647
+ * string. Offsets are shared, so the same spans index both, and
648
+ * {@link literalTypeClass} is handed the span rather than the sliced text so it
649
+ * can read the original while matching brackets out of the masked index.
650
+ */
651
+ function checkHeterogeneousMapLiteral(text, field, findings, brackets) {
652
+ const masked = text.masked;
653
+ for (let index = 0; index < masked.length; index += 1) {
654
+ if (masked[index] !== '{')
655
+ continue;
656
+ const close = matchingParen(brackets, index);
657
+ if (close < 0)
658
+ continue;
659
+ const classes = new Map();
660
+ for (const entry of splitTopLevel(masked, { start: index + 1, end: close }, [','], brackets)) {
661
+ const [, afterKey] = splitTopLevel(masked, entry, [':'], brackets);
662
+ if (afterKey === undefined)
663
+ continue;
664
+ const span = { start: afterKey.start, end: entry.end };
665
+ const found = literalTypeClass(text.source, span, brackets);
666
+ if (found !== undefined && !classes.has(found)) {
667
+ classes.set(found, text.source.slice(span.start, span.end).trim());
668
+ }
669
+ }
670
+ if (classes.size > 1) {
671
+ const [first, second] = [...classes.entries()];
672
+ findings.push(finding('heterogeneous-map-literal', field, text.reported, text.source.slice(index, close + 1), `this map literal mixes ${first?.[0]} (${first?.[1]}) and ${second?.[0]} (${second?.[1]}) values. cel-js takes the map's value type from its first entry and throws "invalid_argument" on the first entry that differs, so it cannot evaluate this map at all; cel-go types the literal as map(string, dyn) and evaluates it. Direct mode can therefore never evaluate this field. Whether KRO evaluates it depends on the rest of cel-go's type checking under KRO's environment, which this check does not model`, 'Give the entries one value type — `string(...)` around the odd ones out is usually enough — or reference an object of the right shape instead of writing a literal, which is what a KubernetesRef or CEL expression of that type does'));
673
+ }
674
+ }
675
+ }
676
+ /** Rule: `in` whose right-hand operand may be a typed list entry. */
677
+ function checkInOnListEntry(text, field, findings) {
678
+ const masked = text.masked;
679
+ const lambdaScopes = collectCelLambdaScopes(text.source);
680
+ const pattern = /\bin\b/g;
681
+ let match = pattern.exec(masked);
682
+ while (match !== null) {
683
+ let cursor = match.index + 2;
684
+ while (cursor < masked.length && masked[cursor] === ' ')
685
+ cursor += 1;
686
+ const operandStart = cursor;
687
+ let depth = 0;
688
+ while (cursor < masked.length) {
689
+ const character = masked[cursor];
690
+ if (character === '[' || character === '(' || character === '{')
691
+ depth += 1;
692
+ else if (character === ']' || character === ')' || character === '}') {
693
+ if (depth === 0)
694
+ break;
695
+ depth -= 1;
696
+ }
697
+ else if (depth === 0 && !/[\w$.]/.test(character))
698
+ break;
699
+ cursor += 1;
700
+ }
701
+ const operand = masked.slice(operandStart, cursor);
702
+ const root = /^[A-Za-z_$][\w$]*/.exec(operand)?.[0];
703
+ // Two shapes that *may* denote a single entry of a list: an index
704
+ // expression, and a variable bound by a collection macro. Neither says what
705
+ // the entry's CEL type is — `in` is correct on a map entry and rejected on a
706
+ // message one, and that distinction lives in the resource's schema, which is
707
+ // not reachable here. Hence a note rather than a strict-mode failure.
708
+ const mayBeListEntry = operand.includes('[') ||
709
+ (root !== undefined && isCelLambdaLocalAt(root, operandStart, lambdaScopes));
710
+ if (operand.length > 0 && mayBeListEntry) {
711
+ findings.push(finding('in-on-list-entry', field, text.reported, text.source.slice(match.index, cursor), `\`in\` is applied to '${operand}', which may be a single list entry. If KRO's cel-go type env types that entry as a message rather than a map it rejects \`in\` on it ("no matching overload for '@in'") where cel-js accepts it. Whether it does is a fact about the resource's schema, which this check cannot see — so this is reported, not failed`, 'If the entry is a message, test the field with has() instead: `list.filter(entry, has(entry.field))`'));
712
+ }
713
+ match = pattern.exec(masked);
714
+ }
715
+ }
716
+ /**
717
+ * The one rule that only makes sense across the operands of an `&&` / `||`
718
+ * chain: a `has()` guard that sits to the right of the access it guards.
719
+ *
720
+ * A companion rule used to live here — `unguarded-index-in-logical-chain`,
721
+ * flagging `list[0].f` inside a chain with no guard before it — and it has been
722
+ * removed because it was not a divergence. On `size(l) > 0 && l[0].f != ""` with
723
+ * an empty `l`, cel-js short-circuits on the `false` and never indexes, while
724
+ * cel-go absorbs the index error under the deciding `false`: both return
725
+ * `false`. On an out-of-range index with no deciding operand, both error. There
726
+ * is no data that separates the engines, so indexing a required list is simply
727
+ * valid on both and reporting it failed strict mode on correct CEL.
728
+ *
729
+ * ## The chain model
730
+ *
731
+ * `&&` binds tighter than `||`, so a span is read as a disjunction of
732
+ * conjunctions: split on `||` first, and split each disjunct on `&&`.
733
+ * Flattening the two into one operand list would let a guard in one `||`
734
+ * disjunct reach an access in another, which it never does — `A || B && C` is
735
+ * `A || (B && C)`, and nothing in `A` runs before `B`.
736
+ *
737
+ * Within one chain, guards carry left to right with the polarity
738
+ * {@link guardsEstablishedBy} describes, and what an operand has established
739
+ * carries *into* that operand's own nested chains and parenthesized groups: in
740
+ * `has(p) && (p.f > 0 || x)` the group is only reached once `has(p)` held.
741
+ *
742
+ * ## Stated limits
743
+ *
744
+ * - Only a whole-operand `has()` is read as a guard, so a `has()` folded into a
745
+ * larger boolean operand neither establishes a path nor is reported as a late
746
+ * guard.
747
+ * - Negation is lexical: `!has(p)`, `!(has(p))` and `!!has(p)` are understood;
748
+ * `!(has(p) && q)` is not read as a guard at all.
749
+ * - Guards inside a collection-macro body or a lazy ternary group are blanked
750
+ * before this runs (see {@link blankLazyRegions}) and so never carry out of
751
+ * the region that binds them.
752
+ *
753
+ * Every limit is in the direction of reporting less, so what escapes the model
754
+ * is a missed finding rather than a strict-mode failure on valid CEL.
755
+ */
756
+ function checkLogicalChain(text, blanked, field, span, established, findings, depth, brackets) {
757
+ if (depth > CEL_DIALECT_MAX_NESTING_DEPTH)
758
+ return;
759
+ // Ternary branches are lazy in both engines, so each `?`/`:` part is its own
760
+ // chain rather than an operand of the surrounding one.
761
+ for (const part of splitTopLevel(text.masked, span, ['?', ':'], brackets)) {
762
+ checkChain(text, blanked, field, part, established, findings, depth, brackets);
763
+ }
764
+ }
765
+ /** One `||` or `&&` chain — or a single operand — with what already holds at it. */
766
+ function checkChain(text, blanked, field, span, established, findings, depth, brackets) {
767
+ if (depth > CEL_DIALECT_MAX_NESTING_DEPTH)
768
+ return;
769
+ const masked = text.masked;
770
+ // Precedence: `||` is the loosest operator, so it splits first and each
771
+ // disjunct is then read as its own `&&` chain.
772
+ const disjuncts = splitTopLevel(masked, span, ['||'], brackets);
773
+ const conjuncts = disjuncts.length > 1 ? [] : splitTopLevel(masked, span, ['&&'], brackets);
774
+ const mode = disjuncts.length > 1 ? 'or' : conjuncts.length > 1 ? 'and' : undefined;
775
+ if (mode === undefined) {
776
+ // Not a chain, but a parenthesized group inside it may hold one — and that
777
+ // group inherits whatever this position already established.
778
+ for (const group of parenGroups(masked, span, brackets)) {
779
+ checkLogicalChain(text, blanked, field, group, established, findings, depth + 1, brackets);
780
+ }
781
+ return;
782
+ }
783
+ const operands = mode === 'or' ? disjuncts : conjuncts;
784
+ const guards = operands.map((operand) => guardsEstablishedBy(blanked, operand, mode, brackets));
785
+ let known = [...established];
786
+ for (let index = 0; index < operands.length; index += 1) {
787
+ const operand = operands[index];
788
+ const after = guards.slice(index + 1).flat();
789
+ const derefs = dereferencedPaths(blanked, operand, brackets);
790
+ // A guard to the right of an access it covers is only harmless if something
791
+ // to the left already established the same path. "Established" is not
792
+ // "covered": a shallower earlier guard reaches the late guard's path without
793
+ // saying it is present, so it cannot stand in for it.
794
+ const lateGuard = after.find((guard) => derefs.some((path) => guardCovers(guard, path)) &&
795
+ !known.some((earlier) => guardEstablishes(earlier, guard)));
796
+ if (lateGuard !== undefined) {
797
+ const guardText = mode === 'or' ? `!has(${lateGuard})` : `has(${lateGuard})`;
798
+ findings.push(finding('guard-after-use-in-logical-chain', field, text.reported, text.source.slice(operand.start, operand.end).trim(), `${guardText} guards this operand but is written after it. cel-go absorbs the error either way; cel-js evaluates left to right and fails before reaching the guard`, `Move ${guardText} to the left of the access, or use a lazy ternary: has(${lateGuard}) ? (...) : <fallback>`));
799
+ }
800
+ // Descend with what holds at this position: the `&&` chain nested inside an
801
+ // `||` disjunct, and any parenthesized group.
802
+ checkChain(text, blanked, field, operand, known, findings, depth + 1, brackets);
803
+ known = [...known, ...guards[index]];
804
+ }
805
+ }
806
+ /* ------------------------------------------------------------------------- *
807
+ * The parse half.
808
+ *
809
+ * cel-js's `parse()` is the only CEL parser this module can call, and it is
810
+ * **not** a conformant implementation of the CEL grammar. So a `parse()` failure
811
+ * is never read as a verdict about cel-go: it is sorted into one of three
812
+ * buckets, each of which stands on evidence of its own.
813
+ *
814
+ * The spec quoted throughout is cel-spec `doc/langdef.md`, section "Syntax"
815
+ * (the EBNF) and its "Lexical Elements" subsection (the token definitions).
816
+ * ------------------------------------------------------------------------- */
817
+ /**
818
+ * `[start, end)` of every **plain-quoted** string literal body in `expression`.
819
+ *
820
+ * Plain-quoted on purpose, and not the same walk as {@link celStringLiteralEnd}.
821
+ * This feeds {@link findSpecCelCelJsRejects}, which looks *backwards* from a
822
+ * span's start for an `r`/`b` prefix, so the span has to stop at the quote the
823
+ * way cel-js's own `StringLiteral` token does rather than swallow the prefix.
824
+ * The `\`-escape rule here is cel-js's, which has no raw form to exempt.
825
+ *
826
+ * The consequence is that a raw literal whose body ends in `\` — `r"a\"`, one
827
+ * whole token to the spec tokenizer — is mis-read or missed here. That costs a
828
+ * rewrite, never a false proof: a span this walk gets wrong yields a rewrite
829
+ * whose result {@link rewriteKeepsTokenBoundaries} re-tokenizes with
830
+ * {@link celSpecTokens}, and a splice that does not land on exactly one token
831
+ * there throws the whole round away. Identification may under-report; only the
832
+ * boundary proof may license a divergence.
833
+ */
834
+ function stringLiteralSpans(expression) {
835
+ const spans = [];
836
+ let quote;
837
+ let start = 0;
838
+ let escaped = false;
839
+ for (let index = 0; index < expression.length; index += 1) {
840
+ const character = expression[index];
841
+ if (quote === undefined) {
842
+ if (character === '"' || character === "'") {
843
+ quote = character;
844
+ start = index;
845
+ }
846
+ continue;
847
+ }
848
+ if (escaped)
849
+ escaped = false;
850
+ else if (character === '\\')
851
+ escaped = true;
852
+ else if (character === quote) {
853
+ spans.push({ start, end: index + 1 });
854
+ quote = undefined;
855
+ }
856
+ }
857
+ return spans;
858
+ }
859
+ /** Index of the next non-whitespace character at or after `from`, or -1. */
860
+ function nextNonSpace(text, from) {
861
+ for (let index = from; index < text.length; index += 1) {
862
+ if (!/\s/.test(text[index]))
863
+ return index;
864
+ }
865
+ return -1;
866
+ }
867
+ /** Index of the previous non-whitespace character before `from`, or -1. */
868
+ function previousNonSpace(text, from) {
869
+ for (let index = from - 1; index >= 0; index -= 1) {
870
+ if (!/\s/.test(text[index]))
871
+ return index;
872
+ }
873
+ return -1;
874
+ }
875
+ const IDENT_CHARACTER = /[A-Za-z0-9_]/;
876
+ /** True when a `.` or `[` sits at `index`, i.e. a postfix member operator follows. */
877
+ function postfixFollows(masked, after) {
878
+ const index = nextNonSpace(masked, after);
879
+ if (index < 0)
880
+ return undefined;
881
+ const character = masked[index];
882
+ return character === '.' || character === '[' ? character : undefined;
883
+ }
884
+ /**
885
+ * Forms the CEL grammar permits that cel-js 0.8.2 provably cannot parse.
886
+ *
887
+ * Every entry here was confirmed two ways before it was written down: the form
888
+ * is derivable from the spec's own productions (quoted per entry), and
889
+ * `parse()` was actually called on it and failed. The unit suite re-asserts the
890
+ * `parse()` half, so the day cel-js catches up the test fails rather than the
891
+ * rule quietly over-reporting.
892
+ *
893
+ * Detection is deliberately conservative in one direction only: an unrecognized
894
+ * shape yields nothing, because a false positive here fails strict mode on
895
+ * valid CEL while a false negative merely leaves a `cel-js-parse-failure` note.
896
+ */
897
+ function findSpecCelCelJsRejects(text, brackets) {
898
+ // Comments are already blanked out of both, so nothing below can identify a
899
+ // "shortfall" written in a comment, and no rewrite can splice into one.
900
+ const expression = text.source;
901
+ const masked = text.masked;
902
+ const found = [];
903
+ // `end` bounds the quoted *excerpt* and is deliberately generous — it runs past
904
+ // the fragment so the reader sees what it was the receiver of. `rewrite` is the
905
+ // exact span, which is a different thing and never guessed from `end`.
906
+ const add = (at, end, reason, rewrite) => {
907
+ found.push({
908
+ at,
909
+ fragment: expression.slice(at, Math.min(end, at + 80)).trim(),
910
+ reason,
911
+ ...(rewrite === undefined ? {} : { rewrite }),
912
+ });
913
+ };
914
+ // Spec: `Primary = ... | LITERAL`, and `Member = Member "." SELECTOR [...]`
915
+ // | `Member "[" Expr "]"`, so a postfix applies to a literal primary. cel-js
916
+ // consumes a StringLiteral as a bare `atomicExpression` alternative with no
917
+ // postfix at all: `"x".size()`, `"a" .size()` and `"a"["b"]` all fail.
918
+ for (const span of stringLiteralSpans(expression)) {
919
+ const postfix = postfixFollows(masked, span.end);
920
+ if (postfix !== undefined) {
921
+ add(span.start, span.end + 24, 'a string literal used as the receiver of a member access', {
922
+ start: span.start,
923
+ end: span.end,
924
+ as: 'receiver',
925
+ });
926
+ }
927
+ // Spec: `STRING_LIT ::= [rR]? (...)` and `BYTES_LIT ::= [bB] STRING_LIT`.
928
+ // cel-js has no raw-string and no bytes token, so the prefix lexes as a
929
+ // one-character identifier and the parse fails.
930
+ const before = previousNonSpace(expression, span.start);
931
+ if (before === span.start - 1 &&
932
+ /[rRbB]/.test(expression[before]) &&
933
+ !IDENT_CHARACTER.test(expression[before - 1] ?? '')) {
934
+ add(before, span.end, 'a raw-string or bytes literal prefix, which cel-js has no token for', {
935
+ start: before,
936
+ end: span.end,
937
+ as: 'string',
938
+ });
939
+ }
940
+ }
941
+ // Spec `STRING_LIT` also admits the triple-quoted forms; cel-js has neither.
942
+ for (const quote of ['"""', "'''"]) {
943
+ const at = expression.indexOf(quote);
944
+ if (at < 0)
945
+ continue;
946
+ // The closing run is what makes the literal rewritable. Without one there is
947
+ // no span to substitute, so the shortfall is recorded with no rewrite and
948
+ // can never carry a divergence on its own.
949
+ const closed = expression.indexOf(quote, at + quote.length);
950
+ add(at, at + 24, 'a triple-quoted string literal, which cel-js has no token for', closed < 0 ? undefined : { start: at, end: closed + quote.length, as: 'string' });
951
+ }
952
+ // Spec: `Primary = "(" Expr ")"`, again a Member and so a legal receiver.
953
+ // cel-js's `parenthesisExpression` takes no postfix: `(a).b` and `(a+b)[0]`
954
+ // fail. A `(` that opens a *call* is a different production and is fine, so
955
+ // only a grouping paren — one not preceded by an identifier character —
956
+ // counts here.
957
+ for (let index = 0; index < masked.length; index += 1) {
958
+ if (masked[index] !== '(')
959
+ continue;
960
+ const before = previousNonSpace(masked, index);
961
+ const identifierCall = before >= 0 && IDENT_CHARACTER.test(masked[before]);
962
+ const close = matchingParen(brackets, index);
963
+ if (close < 0 || postfixFollows(masked, close + 1) === undefined)
964
+ continue;
965
+ if (!identifierCall) {
966
+ add(index, close + 24, 'a parenthesized expression used as the receiver of a member access', {
967
+ start: index,
968
+ end: close + 1,
969
+ as: 'receiver',
970
+ });
971
+ continue;
972
+ }
973
+ // A global function call — `size(a)`, `has(a.b)`, `int(a)` — is cel-js's
974
+ // `macrosExpression`, which also takes no postfix, so `size(a).b` fails.
975
+ // A *member* call (`a.map(x, x).size()`) is part of `identifierExpression`
976
+ // and cel-js handles it, so a callee written after a `.` is left alone.
977
+ let start = before;
978
+ while (start >= 0 && IDENT_CHARACTER.test(masked[start]))
979
+ start -= 1;
980
+ if (previousNonSpace(masked, start + 1) < 0 || masked[start] !== '.') {
981
+ add(start + 1, close + 24, 'a global function call used as the receiver of a member access', { start: start + 1, end: close + 1, as: 'receiver' });
982
+ }
983
+ }
984
+ // Spec: `Primary = "[" [ExprList] [","] "]"`, and a postfix applies to it.
985
+ // cel-js's `listExpression` allows exactly one trailing index and no `.` at
986
+ // all, so `[a,b].size()` and `[1,2][0].f` fail while `[1,2][0]` parses.
987
+ for (let index = 0; index < masked.length; index += 1) {
988
+ if (masked[index] !== '[')
989
+ continue;
990
+ const before = previousNonSpace(masked, index);
991
+ // An index expression, not a list literal, when something precedes it that
992
+ // a postfix can attach to.
993
+ if (before >= 0 && /[A-Za-z0-9_)\]}]/.test(masked[before]))
994
+ continue;
995
+ const close = matchingParen(brackets, index);
996
+ if (close < 0)
997
+ continue;
998
+ if (postfixFollows(masked, close + 1) === '.') {
999
+ add(index, close + 24, 'a list literal used as the receiver of a member access', {
1000
+ start: index,
1001
+ end: close + 1,
1002
+ as: 'receiver',
1003
+ });
1004
+ continue;
1005
+ }
1006
+ // The one permitted index, then anything further is past what cel-js takes.
1007
+ if (postfixFollows(masked, close + 1) !== '[')
1008
+ continue;
1009
+ const second = matchingParen(brackets, nextNonSpace(masked, close + 1));
1010
+ if (second > 0 && postfixFollows(masked, second + 1) !== undefined) {
1011
+ // The list *and* its one permitted index collapse into the placeholder:
1012
+ // `[1,2][0].f` is grammatical as `__typekro_recv0.f`, where rewriting the
1013
+ // list alone would leave `__typekro_recv0[0].f` — still past cel-js.
1014
+ add(index, second + 24, 'a list literal with more than the one postfix cel-js allows', {
1015
+ start: index,
1016
+ end: second + 1,
1017
+ as: 'receiver',
1018
+ });
1019
+ }
1020
+ }
1021
+ // Spec: `Primary = ... | LITERAL` covers the number, bool and null literals
1022
+ // too, so `1.string()`, `1.0.x`, `2u.x`, `true.x` and `null.x` are all
1023
+ // grammatical. cel-js consumes each as a bare token with no postfix.
1024
+ const literalReceiver = /(?<![A-Za-z0-9_.])(0[xX][0-9a-fA-F]+[uU]?|\d+(?:\.\d+)?[uU]?|true|false|null)\s*\.\s*[A-Za-z_]/g;
1025
+ for (const match of masked.matchAll(literalReceiver)) {
1026
+ // Only the literal itself is the receiver; the `.` and the selector after it
1027
+ // are the member access that stays.
1028
+ const literal = match[1];
1029
+ add(match.index, match.index + match[0].length, 'a number, bool or null literal used as the receiver of a member access', { start: match.index, end: match.index + literal.length, as: 'receiver' });
1030
+ }
1031
+ // Spec: `FLOAT_LIT ::= -? DIGIT* . DIGIT+ EXPONENT? | -? DIGIT+ EXPONENT`.
1032
+ // cel-js's `Float` token is `-?\d+\.\d+` with no exponent form, so `1e3`,
1033
+ // `1.5e-3` and `0.5e3` are all rejected.
1034
+ for (const match of masked.matchAll(/(?<![A-Za-z0-9_.])\d+(?:\.\d+)?[eE][+-]?\d+/g)) {
1035
+ add(match.index, match.index + match[0].length, 'a float literal written with an exponent, which cel-js has no token for', { start: match.index, end: match.index + match[0].length, as: 'float' });
1036
+ }
1037
+ return found.sort((left, right) => left.at - right.at);
1038
+ }
1039
+ /**
1040
+ * Largest number of rewrite rounds {@link rewriteAwayCelJsLimitations} will run.
1041
+ *
1042
+ * One round removes every shortfall that is not nested inside another, so a
1043
+ * round is needed per level of nesting: `("x".size()).b` takes one (the outer
1044
+ * parenthesized receiver swallows the inner string one), `r"x".size()` takes two
1045
+ * (the raw prefix goes first, and only then is the plain literal visible as a
1046
+ * receiver). Real emitted status CEL reaches one. The cap exists so a pathological
1047
+ * expression cannot loop — each round either rewrites something or the loop
1048
+ * stops, and hitting the cap simply means no divergence is proven, which is the
1049
+ * safe direction.
1050
+ */
1051
+ const CEL_DIALECT_MAX_REWRITE_ROUNDS = 8;
1052
+ /** Receiver placeholders are bound nowhere; only their grammar matters. */
1053
+ const CEL_JS_RECEIVER_PLACEHOLDER = '__typekro_recv';
1054
+ /* ---------------------------------------------------------------------------
1055
+ * Lexical coverage: "cel-js parsed it" has to mean "cel-js read all of it".
1056
+ *
1057
+ * cel-js's `parse()` (dist/lib.js) calls `CELLexer.tokenize(expression)` and
1058
+ * then looks only at `parserInstance.errors` — `lexResult.errors` is discarded
1059
+ * unread. A chevrotain lexer skips a character it has no token for and carries
1060
+ * on, so every character cel-js has no token for is dropped silently before the
1061
+ * parser ever sees the text: `a === b` reaches the parser as `a == b` and
1062
+ * "parses", and so do `x.size() $` and `x.size() ☃`. Taking `isSuccess` as
1063
+ * proof that cel-js accepted the *text* is therefore wrong in exactly the
1064
+ * direction that matters here — a divergence proof is a claim about the whole
1065
+ * expression, and a parse that dropped part of it proves nothing about the part
1066
+ * it dropped.
1067
+ *
1068
+ * **Which route this takes, and why.** The first choice would be to ask cel-js
1069
+ * itself: `dist/tokens.js` exports both `CELLexer` and the `allTokens`
1070
+ * vocabulary, and `CELLexer.tokenize()` returns the `errors` that `parse()`
1071
+ * throws away. It is not reachable. cel-js 0.8.2's package.json declares
1072
+ * `"exports": { ".": "./dist/index.js" }` and nothing else, so
1073
+ * `cel-js/dist/tokens.js` does not resolve, and `index.js` re-exports only
1074
+ * `parse`, `evaluate` and the three error classes — no lexer, no token
1075
+ * vocabulary. The second choice, running chevrotain's own `Lexer` over that
1076
+ * vocabulary, needs the same unreachable export (and `chevrotain` is cel-js's
1077
+ * dependency, not TypeKro's). So this takes the third route: a coverage scanner
1078
+ * written from the CEL specification's own lexical grammar (cel-spec
1079
+ * doc/langdef.md, "Lexical Elements"), which walks the text and reports the
1080
+ * first character no CEL token can carry.
1081
+ *
1082
+ * That scanner is a superset of cel-js's lexer — it knows the raw, bytes and
1083
+ * triple-quoted `STRING_LIT` forms and the exponent `FLOAT_LIT` that cel-js has
1084
+ * no token for — and the superset direction is the safe one for both jobs it
1085
+ * does. As a *proof* gate it can only withhold a divergence, never invent one:
1086
+ * anything cel-js's lexer drops is a character cel-js has no token for, and the
1087
+ * only such characters the spec does have a token for are the four forms just
1088
+ * named, each of which the rewriter has already replaced by the time a proof is
1089
+ * checked. As a `not-valid-cel` input it reports only characters the **spec**
1090
+ * has no token for, which is a fact about the language rather than about either
1091
+ * engine, so it belongs in that bucket by the same standard as the rest of it.
1092
+ * ------------------------------------------------------------------------- */
1093
+ /** `WHITESPACE ::= [\t\n\f\r ]+`. */
1094
+ const CEL_WHITESPACE = /[\t\n\f\r ]/;
1095
+ /** `DIGIT ::= [0-9]`. */
1096
+ const CEL_DIGIT = /[0-9]/;
1097
+ /** `HEXDIGIT ::= [0-9abcdefABCDEF]`. */
1098
+ const CEL_HEXDIGIT = /[0-9a-fA-F]/;
1099
+ /** The first character of `IDENT ::= [_a-zA-Z][_a-zA-Z0-9]*`. */
1100
+ const CEL_IDENT_START = /[_A-Za-z]/;
1101
+ /** The two-character operators in the spec's punctuation list. */
1102
+ const CEL_PUNCTUATION_PAIRS = new Set(['||', '&&', '==', '!=', '<=', '>=']);
1103
+ /** The one-character punctuation, once the pairs above are taken out. */
1104
+ const CEL_PUNCTUATION_SINGLES = '()[]{}.,?:!<>+-*/%';
1105
+ /**
1106
+ * End of the `STRING_LIT`/`BYTES_LIT` token starting at `at`, or `at` for none.
1107
+ *
1108
+ * `STRING_LIT ::= [rR]? ( '"' … '"' | "'" … "'" | '"""' … '"""' | "'''" … "'''" )`
1109
+ * and `BYTES_LIT ::= [bB] STRING_LIT`; cel-go accepts the two prefixes in
1110
+ * either order, so at most one of each is taken. A prefix with no quote behind
1111
+ * it is not a string at all and is handed back for `IDENT` to consume, which is
1112
+ * what makes `bar"x"` lex as an identifier and a string rather than as nothing.
1113
+ *
1114
+ * A `\` consumes the character after it in every form **except** a raw one.
1115
+ * The langdef lexis and cel-go's lexer spell the raw alternatives with no
1116
+ * `ESC_SEQ` in them at all — `RAW '"' ~["\n\r]* '"'`, `RAW '"""' .*? '"""'`,
1117
+ * and the `'`-delimited pair of each — so a raw literal has no escapes to lex
1118
+ * and closes at the first delimiter after its opening one. `r"a\"` is therefore
1119
+ * one complete literal whose value is `a\`, and `r"a\" == x.status.y` is that
1120
+ * literal followed by `== x.status.y`. Reading the `\"` there as an escape runs
1121
+ * the literal on to the wrong closing quote — or off the end of the text, where
1122
+ * it is no token at all — and takes everything after it down with it, which
1123
+ * turns a perfectly ordinary comparison into an unlexable expression.
1124
+ *
1125
+ * The `b` prefix does **not** suppress escapes: `BYTES_LIT ::= [bB] STRING_LIT`
1126
+ * keeps whichever `STRING_LIT` form follows it, so `b"a\""` escapes and only
1127
+ * `r`, `rb` and `br` set the flag.
1128
+ *
1129
+ * An unterminated literal, or a newline inside a single-delimiter one — raw
1130
+ * included, since `~["\n\r]` excludes it there too — is not a token; the
1131
+ * opening quote is then the character nothing can carry, which is the honest
1132
+ * place to point at. The triple-quoted forms admit newlines in both variants.
1133
+ */
1134
+ function celStringLiteralEnd(text, at) {
1135
+ let index = at;
1136
+ let raw = false;
1137
+ let bytes = false;
1138
+ for (let take = 0; take < 2; take += 1) {
1139
+ const character = text[index];
1140
+ if (!raw && (character === 'r' || character === 'R')) {
1141
+ raw = true;
1142
+ index += 1;
1143
+ }
1144
+ else if (!bytes && (character === 'b' || character === 'B')) {
1145
+ bytes = true;
1146
+ index += 1;
1147
+ }
1148
+ else
1149
+ break;
1150
+ }
1151
+ const delimiter = ['"""', "'''", '"', "'"].find((candidate) => text.startsWith(candidate, index));
1152
+ if (delimiter === undefined)
1153
+ return at;
1154
+ for (let scan = index + delimiter.length; scan < text.length; scan += 1) {
1155
+ const character = text[scan];
1156
+ if (!raw && character === '\\') {
1157
+ if (scan + 1 >= text.length)
1158
+ return at;
1159
+ scan += 1;
1160
+ continue;
1161
+ }
1162
+ if (delimiter.length === 1 && (character === '\n' || character === '\r'))
1163
+ return at;
1164
+ if (text.startsWith(delimiter, scan))
1165
+ return scan + delimiter.length;
1166
+ }
1167
+ return at;
1168
+ }
1169
+ /**
1170
+ * End of the `INT_LIT`/`UINT_LIT`/`FLOAT_LIT` token starting at `at`, or `at`.
1171
+ *
1172
+ * `INT_LIT ::= DIGIT+ | '0x' HEXDIGIT+`, `UINT_LIT ::= INT_LIT [uU]`,
1173
+ * `FLOAT_LIT ::= DIGIT* '.' DIGIT+ EXPONENT? | DIGIT+ EXPONENT` and
1174
+ * `EXPONENT ::= [eE] [+-]? DIGIT+`. The leading `-` the spec writes into each
1175
+ * form is consumed as punctuation instead, which changes no coverage verdict.
1176
+ * `0x` with no hex digit behind it falls through to the decimal form, because
1177
+ * cel-go's longest-match lexer reads that as `0` and the identifier `x`.
1178
+ */
1179
+ function celNumberLiteralEnd(text, at) {
1180
+ if (!CEL_DIGIT.test(text[at] ?? ''))
1181
+ return at;
1182
+ if (text[at] === '0' && (text[at + 1] === 'x' || text[at + 1] === 'X')) {
1183
+ let hex = at + 2;
1184
+ while (hex < text.length && CEL_HEXDIGIT.test(text[hex]))
1185
+ hex += 1;
1186
+ if (hex > at + 2)
1187
+ return text[hex] === 'u' || text[hex] === 'U' ? hex + 1 : hex;
1188
+ }
1189
+ let end = at;
1190
+ while (end < text.length && CEL_DIGIT.test(text[end]))
1191
+ end += 1;
1192
+ if (text[end] === '.' && CEL_DIGIT.test(text[end + 1] ?? '')) {
1193
+ end += 1;
1194
+ while (end < text.length && CEL_DIGIT.test(text[end]))
1195
+ end += 1;
1196
+ }
1197
+ const exponent = /^[eE][+-]?[0-9]+/.exec(text.slice(end));
1198
+ if (exponent !== null)
1199
+ return end + exponent[0].length;
1200
+ return text[end] === 'u' || text[end] === 'U' ? end + 1 : end;
1201
+ }
1202
+ /** End of the one CEL token starting at `at`, or `at` when there is none. */
1203
+ function celSpecTokenEnd(text, at) {
1204
+ const character = text[at];
1205
+ if (CEL_WHITESPACE.test(character)) {
1206
+ let end = at + 1;
1207
+ while (end < text.length && CEL_WHITESPACE.test(text[end]))
1208
+ end += 1;
1209
+ return end;
1210
+ }
1211
+ // `COMMENT ::= '//' ~NEWLINE*`.
1212
+ if (character === '/' && text[at + 1] === '/') {
1213
+ const newline = text.slice(at).search(/[\r\n]/);
1214
+ return newline < 0 ? text.length : at + newline;
1215
+ }
1216
+ const string = celStringLiteralEnd(text, at);
1217
+ if (string > at)
1218
+ return string;
1219
+ const number = celNumberLiteralEnd(text, at);
1220
+ if (number > at)
1221
+ return number;
1222
+ if (CEL_IDENT_START.test(character)) {
1223
+ let end = at + 1;
1224
+ while (end < text.length && IDENT_CHARACTER.test(text[end]))
1225
+ end += 1;
1226
+ return end;
1227
+ }
1228
+ if (CEL_PUNCTUATION_PAIRS.has(text.slice(at, at + 2)))
1229
+ return at + 2;
1230
+ return CEL_PUNCTUATION_SINGLES.includes(character) ? at + 1 : at;
1231
+ }
1232
+ /**
1233
+ * Offset of the first character of `text` no CEL token can carry, or `-1`.
1234
+ *
1235
+ * A `-1` means the spec's lexical grammar tiles the text end to end with no
1236
+ * gaps: every character belongs to a token, a comment or whitespace. That is
1237
+ * the property `parse()` does not check, and it is now required before anything
1238
+ * is read into a cel-js verdict.
1239
+ */
1240
+ function firstUnlexableOffset(text) {
1241
+ let index = 0;
1242
+ while (index < text.length) {
1243
+ const end = celSpecTokenEnd(text, index);
1244
+ if (end <= index)
1245
+ return index;
1246
+ index = end;
1247
+ }
1248
+ return -1;
1249
+ }
1250
+ /**
1251
+ * Every significant CEL token in `text`, or `undefined` when a character is one
1252
+ * no CEL token can carry.
1253
+ *
1254
+ * "Significant" drops whitespace and comments, which is what the token stream a
1255
+ * parser sees consists of. This is the same walk {@link firstUnlexableOffset}
1256
+ * makes, keeping the spans instead of discarding them, so a token boundary here
1257
+ * is a token boundary there by construction. Its one caller is
1258
+ * {@link rewriteKeepsTokenBoundaries}, which needs to compare two token streams
1259
+ * rather than merely establish that both are lexable.
1260
+ */
1261
+ function celSpecTokens(text) {
1262
+ const tokens = [];
1263
+ let index = 0;
1264
+ while (index < text.length) {
1265
+ const end = celSpecTokenEnd(text, index);
1266
+ if (end <= index)
1267
+ return undefined;
1268
+ const first = text[index];
1269
+ const comment = first === '/' && text[index + 1] === '/';
1270
+ if (!comment && !CEL_WHITESPACE.test(first))
1271
+ tokens.push({ start: index, end });
1272
+ index = end;
1273
+ }
1274
+ return tokens;
1275
+ }
1276
+ /**
1277
+ * Blank the string literals and the comments out of an expression, in one pass
1278
+ * driven by the same tokenizer {@link firstUnlexableOffset} walks with.
1279
+ *
1280
+ * Masking and lexical coverage have to agree about where a token starts and
1281
+ * ends, and the way to get that is to ask the same function rather than to
1282
+ * write the rule twice. Two orderings are what a string-only mask gets wrong,
1283
+ * and both fall out of walking tokens in source order: a `//` *inside* a string
1284
+ * literal is part of the literal and opens no comment (`"a // b"` is a string),
1285
+ * and a quote *inside* a comment is part of the comment and opens no literal
1286
+ * (`// it's "quoted"` is a comment — under a string-only mask its apostrophe
1287
+ * flips the masking of everything after it).
1288
+ *
1289
+ * `COMMENT ::= '//' ~NEWLINE*` (cel-spec doc/langdef.md, "Syntax") stops before
1290
+ * its newline, so the newline is never blanked and a multi-line expression
1291
+ * keeps its line structure.
1292
+ *
1293
+ * Two results, because the rules need two different things:
1294
+ *
1295
+ * - `masked` blanks comments *and* string literals. Every structural scan reads
1296
+ * it, so a bracket, `&&`, `?`, `:` or `has(` that is really text inside a
1297
+ * literal or a comment is not mistaken for structure.
1298
+ * - `source` blanks only the comments. A rule that needs the *content* of a
1299
+ * literal — {@link literalTypeClass}, {@link stringLiteralSpans} — reads it,
1300
+ * and so does every fragment a finding quotes, so no identified form can be
1301
+ * made of comment text.
1302
+ *
1303
+ * A character no CEL token can carry stops neither: it is copied through and
1304
+ * the walk steps over it one **code unit** at a time, which keeps this a mask
1305
+ * rather than a second lexical gate — text that is not CEL at all (`a === b`,
1306
+ * `${x}`) still has to reach {@link findNonCelTokens} with its `=` and `$`
1307
+ * visible. Every write is by code unit and every blank is one space per unit,
1308
+ * so both results are exactly as long as the input and an offset means the same
1309
+ * thing in all three texts.
1310
+ */
1311
+ function maskCelCommentsAndStrings(text) {
1312
+ // `split('')` is by UTF-16 code unit. `[...text]` is by code point, which
1313
+ // would blank an astral character to a *single* space and shrink the mask by
1314
+ // one unit against the original — see {@link CelDialectText}.
1315
+ const source = text.split('');
1316
+ const masked = text.split('');
1317
+ let index = 0;
1318
+ while (index < text.length) {
1319
+ if (text[index] === '/' && text[index + 1] === '/') {
1320
+ const end = celSpecTokenEnd(text, index);
1321
+ for (let at = index; at < end; at += 1) {
1322
+ source[at] = ' ';
1323
+ masked[at] = ' ';
1324
+ }
1325
+ index = end;
1326
+ continue;
1327
+ }
1328
+ const stringEnd = celStringLiteralEnd(text, index);
1329
+ if (stringEnd > index) {
1330
+ for (let at = index; at < stringEnd; at += 1)
1331
+ masked[at] = ' ';
1332
+ index = stringEnd;
1333
+ continue;
1334
+ }
1335
+ const end = celSpecTokenEnd(text, index);
1336
+ index = end > index ? end : index + 1;
1337
+ }
1338
+ return { source: source.join(''), masked: masked.join('') };
1339
+ }
1340
+ /** Run cel-js's parser and the lexical coverage scan over the same text. */
1341
+ function celJsReads(expression) {
1342
+ let parsed = false;
1343
+ try {
1344
+ parsed = parse(expression).isSuccess === true;
1345
+ }
1346
+ catch {
1347
+ parsed = false;
1348
+ }
1349
+ return { parsed, unlexableAt: firstUnlexableOffset(expression) };
1350
+ }
1351
+ /**
1352
+ * cel-js accepted the **whole** text: every character is carried by a CEL token
1353
+ * and the parser accepted the token stream.
1354
+ *
1355
+ * This — never `parse().isSuccess` on its own — is what "cel-js parses it" means
1356
+ * anywhere a verdict is drawn from it, whether that is a divergence proof or the
1357
+ * gate that decides an expression is established grammatical.
1358
+ */
1359
+ function celJsAcceptsWhole(expression) {
1360
+ const reading = celJsReads(expression);
1361
+ return reading.parsed && reading.unlexableAt < 0;
1362
+ }
1363
+ /**
1364
+ * How deep the swallowed-span check will recurse before giving up.
1365
+ *
1366
+ * A bracketed receiver span nested inside another bracketed receiver span costs
1367
+ * one level — `(("x".size()).b).c` is two — and each level asks cel-js about a
1368
+ * strictly shorter piece of text, so the recursion terminates on its own. The
1369
+ * cap bounds the *cost* rather than the termination: without it an adversarial
1370
+ * chain of nested receivers would re-parse a prefix of the expression once per
1371
+ * level, which is the quadratic shape this module has already had to fix once.
1372
+ * Real emitted status CEL reaches one level; giving up past eight only means no
1373
+ * divergence is proven, which is the safe direction.
1374
+ */
1375
+ const CEL_DIALECT_MAX_SPAN_DEPTH = 8;
1376
+ /**
1377
+ * Whether cel-js accepts the span a `receiver` rewrite is about to swallow.
1378
+ *
1379
+ * The rewrite replaces a whole bracketed span with one identifier, so the text
1380
+ * cel-js is asked about no longer contains it. Without this check the proof
1381
+ * inherits exactly the defect it was built to rule out: `(a &&).b`, `size(a +).b`
1382
+ * and `[1, ,2].size()` each rewrite to something cel-js parses — `__typekro_recv0.b`
1383
+ * — while the part that vanished is ungrammatical on any engine, so cel-go
1384
+ * refuses the original as readily as cel-js does and the "divergence" is a false
1385
+ * positive against strict mode.
1386
+ *
1387
+ * The span is held to the same bar as the whole expression, recursively: cel-js
1388
+ * accepts it outright, or a proof of its own establishes it. The recursion is
1389
+ * needed because a span can carry its own shortfall — `("x".size()).b` swallows
1390
+ * `("x".size())`, which cel-js cannot parse for a reason that *is* a divergence.
1391
+ *
1392
+ * Spans that carry no bracket — a string-literal receiver, an `r`/`b` prefix, a
1393
+ * triple-quoted literal, an exponent float, a number/bool/null receiver — are a
1394
+ * single literal token with no subexpression inside it to be ungrammatical, and
1395
+ * the lexical coverage scan over the original has already established that each
1396
+ * is a well-formed token. Everything a bracket could hide is checked: the
1397
+ * parenthesized group, the global call's argument list, and the list literal.
1398
+ */
1399
+ function celJsAcceptsSwallowedSpan(text, rewrite, depth) {
1400
+ if (rewrite.as !== 'receiver')
1401
+ return true;
1402
+ const span = celDialectText(text.source.slice(rewrite.start, rewrite.end));
1403
+ if (!/[([{]/.test(span.masked))
1404
+ return true;
1405
+ if (depth >= CEL_DIALECT_MAX_SPAN_DEPTH)
1406
+ return false;
1407
+ if (celJsAcceptsWhole(span.reported))
1408
+ return true;
1409
+ // Every receiver rewrite requires a postfix operator after its span, so `span`
1410
+ // is strictly shorter than `text` and the recursion cannot revisit it.
1411
+ const rewritten = rewriteAwayCelJsLimitations(span, buildBracketIndex(span.masked), depth + 1);
1412
+ return rewritten.applied > 0 && celJsAcceptsWhole(rewritten.text);
1413
+ }
1414
+ /**
1415
+ * Rewrite every identified cel-js shortfall out of an expression, leaving the
1416
+ * rest of it byte for byte as it was.
1417
+ *
1418
+ * This is the evidence half of the `cel-js-rejects-spec-cel` bucket. A cel-js
1419
+ * parse failure plus a shortfall *somewhere* in the text proves nothing on its
1420
+ * own: the failure may be caused by a genuine syntax error elsewhere — `"x".size() +`
1421
+ * has a string-literal receiver in it and is also simply unfinished, and cel-go
1422
+ * rejects it as readily as cel-js does. Divergence needs the stronger claim that
1423
+ * the identified shortfalls are the *only* reason cel-js refuses the text, and
1424
+ * the way to establish that is to take them away and ask cel-js again.
1425
+ *
1426
+ * Each round re-runs the detector over the current text and applies the
1427
+ * outermost non-overlapping rewrites, right to left so the spans of the
1428
+ * not-yet-applied rewrites stay valid; the text is re-masked between rounds
1429
+ * because a rewrite can change where the string literals are. Overlapping
1430
+ * shortfalls — a string receiver inside a parenthesized receiver — resolve
1431
+ * outermost-first, and whatever is left over is picked up by the next round.
1432
+ *
1433
+ * A `receiver` rewrite over a bracketed span *swallows* that span: `(a && b).c`
1434
+ * becomes `__typekro_recv0.c`, and whatever was between the brackets is gone
1435
+ * from the text cel-js is asked about. That makes the same mistake the whole
1436
+ * proof exists to avoid, one level down — the parse says nothing about the part
1437
+ * it never saw — so a bracketed span is swallowed only once cel-js has been
1438
+ * shown to accept the span itself; see {@link celJsAcceptsSwallowedSpan}.
1439
+ */
1440
+ function rewriteAwayCelJsLimitations(expression, brackets, depth = 0) {
1441
+ // The rewrite runs on `source` — comments already blanked — so no splice can
1442
+ // land inside a comment and no comment can survive into the proof text.
1443
+ let current = expression;
1444
+ let text = current.source;
1445
+ let currentBrackets = brackets;
1446
+ let placeholders = 0;
1447
+ let applied = 0;
1448
+ for (let round = 0; round < CEL_DIALECT_MAX_REWRITE_ROUNDS; round += 1) {
1449
+ const candidates = findSpecCelCelJsRejects(current, currentBrackets)
1450
+ .map((limitation) => limitation.rewrite)
1451
+ .filter((rewrite) => rewrite !== undefined && rewrite.end > rewrite.start)
1452
+ .filter((rewrite) => celJsAcceptsSwallowedSpan(current, rewrite, depth))
1453
+ // Outermost first at a shared start, so a containing span wins and the
1454
+ // contained one is skipped rather than splitting the container in two.
1455
+ .sort((left, right) => left.start - right.start || right.end - left.end);
1456
+ const chosen = [];
1457
+ for (const candidate of candidates) {
1458
+ const overlaps = chosen.some((taken) => candidate.start < taken.end && taken.start < candidate.end);
1459
+ if (!overlaps)
1460
+ chosen.push(candidate);
1461
+ }
1462
+ if (chosen.length === 0)
1463
+ break;
1464
+ // Numbered left to right, so a reader of the proof meets `__typekro_recv0`
1465
+ // before `__typekro_recv1`; `chosen` is already in source order.
1466
+ const replacements = chosen.map((rewrite) => {
1467
+ const width = rewrite.end - rewrite.start;
1468
+ if (rewrite.as === 'receiver')
1469
+ return `${CEL_JS_RECEIVER_PLACEHOLDER}${placeholders++}`;
1470
+ const filler = Math.max(1, width - 2);
1471
+ return rewrite.as === 'string'
1472
+ ? `"${'x'.repeat(filler)}"`
1473
+ : `0.${'0'.repeat(filler)}`;
1474
+ });
1475
+ // A splice has no boundary of its own: it sits between two characters the
1476
+ // tokenizer has already paired with their own tokens, and a replacement
1477
+ // that abuts an `IDENT` or digit character *joins* it. `x"y".size()` is the
1478
+ // worked example — splicing the placeholder in for the string literal
1479
+ // yields `x__typekro_recv0.size()`, one identifier where the original had
1480
+ // an identifier next to a string literal. One space keeps the boundary and
1481
+ // changes no CEL production, whitespace being allowed between any two
1482
+ // tokens. A neighbour that is itself being replaced is consulted at its
1483
+ // replacement, since that is what will be adjacent.
1484
+ const padding = chosen.map((rewrite, index) => {
1485
+ const replacement = replacements[index];
1486
+ const previousSpan = chosen[index - 1];
1487
+ const nextSpan = chosen[index + 1];
1488
+ const before = previousSpan !== undefined && previousSpan.end === rewrite.start
1489
+ ? replacements[index - 1].slice(-1)
1490
+ : (text[rewrite.start - 1] ?? '');
1491
+ const after = nextSpan !== undefined && nextSpan.start === rewrite.end
1492
+ ? replacements[index + 1].slice(0, 1)
1493
+ : (text[rewrite.end] ?? '');
1494
+ return {
1495
+ left: IDENT_CHARACTER.test(before) ? ' ' : '',
1496
+ // A closing `"` ends its token whatever follows it, so a string filler
1497
+ // cannot fuse to the right. A number filler can: `0.000` followed by
1498
+ // `e5`, or by another digit, is one `FLOAT_LIT`.
1499
+ right: !replacement.endsWith('"') && IDENT_CHARACTER.test(after) ? ' ' : '',
1500
+ };
1501
+ });
1502
+ // Where each span lands in the rewritten text, accumulated left to right.
1503
+ let shift = 0;
1504
+ const rewritten = chosen.map((rewrite, index) => {
1505
+ const replacement = replacements[index];
1506
+ const { left, right } = padding[index];
1507
+ const start = rewrite.start + shift + left.length;
1508
+ shift += left.length + replacement.length + right.length - (rewrite.end - rewrite.start);
1509
+ return {
1510
+ before: { start: rewrite.start, end: rewrite.end },
1511
+ after: { start, end: start + replacement.length },
1512
+ };
1513
+ });
1514
+ // Applied right to left: a `receiver` rewrite changes the width of the text,
1515
+ // and splicing from the end keeps every earlier span indexing what it named.
1516
+ const original = text;
1517
+ for (let index = chosen.length - 1; index >= 0; index -= 1) {
1518
+ const rewrite = chosen[index];
1519
+ const { left, right } = padding[index];
1520
+ text =
1521
+ text.slice(0, rewrite.start) +
1522
+ left +
1523
+ replacements[index] +
1524
+ right +
1525
+ text.slice(rewrite.end);
1526
+ }
1527
+ // Padding is a guess about where two tokens could fuse; this is the proof
1528
+ // that none did. A round that moved a token boundary anywhere outside its
1529
+ // own spans is refused outright rather than carried forward, so the caller
1530
+ // sees `applied: 0` and the expression falls to `cel-js-parse-failure`.
1531
+ if (!rewriteKeepsTokenBoundaries(original, text, rewritten)) {
1532
+ return { text: expression.source, applied: 0 };
1533
+ }
1534
+ applied += chosen.length;
1535
+ current = celDialectText(text);
1536
+ currentBrackets = buildBracketIndex(current.masked);
1537
+ }
1538
+ return { text, applied };
1539
+ }
1540
+ /**
1541
+ * Whether a rewrite round left the token stream alone outside its own spans.
1542
+ *
1543
+ * The proof a `cel-js-rejects-spec-cel` divergence stands on is "cel-js parses
1544
+ * this text, which differs from the original only where a confirmed shortfall
1545
+ * was replaced by a same-production spelling of it". String concatenation does
1546
+ * not respect that claim on its own: a replacement that abuts an `IDENT` or
1547
+ * digit character merges with it, and the rewrite then lexes a *different token
1548
+ * stream* from the original outside the spans it was licensed to change.
1549
+ * `x"y".size()` is an identifier next to a string literal, which no CEL
1550
+ * production joins; splicing the literal out gives `x__typekro_recv0.size()`,
1551
+ * a single identifier with a member call on it that cel-js parses happily. Left
1552
+ * unchecked, that reported a divergence against text cel-go rejects outright —
1553
+ * a false positive against strict mode, which is the one direction this module
1554
+ * may not fail in.
1555
+ *
1556
+ * So the boundary is verified rather than assumed. Both texts are tokenized
1557
+ * with the spec tokenizer {@link firstUnlexableOffset} walks with, and the round
1558
+ * holds only when all three hold:
1559
+ *
1560
+ * - no token straddles the edge of a rewritten span, on either side. A token
1561
+ * that touches a span lies wholly inside it, and no token reaches across two.
1562
+ * - the tokens *outside* the spans are the same sequence, character for
1563
+ * character, in the same order.
1564
+ * - each rewritten span is **exactly one token** in the result — the single
1565
+ * `IDENT` placeholder, `STRING_LIT` or `FLOAT_LIT` the substitution promised.
1566
+ *
1567
+ * Failing any of them means the splice said something about the text the
1568
+ * rewrite rule did not license, so the round is thrown away and no divergence
1569
+ * is proven. That is the safe direction: the expression is reported as
1570
+ * `cel-js-parse-failure` instead, which claims nothing beyond direct mode.
1571
+ */
1572
+ function rewriteKeepsTokenBoundaries(before, after, spans) {
1573
+ const beforeTokens = celSpecTokens(before);
1574
+ const afterTokens = celSpecTokens(after);
1575
+ if (beforeTokens === undefined || afterTokens === undefined)
1576
+ return false;
1577
+ const outside = (tokens, side) => {
1578
+ const kept = [];
1579
+ for (const token of tokens) {
1580
+ const touched = spans.filter((span) => {
1581
+ const at = side(span);
1582
+ return token.start < at.end && at.start < token.end;
1583
+ });
1584
+ if (touched.length === 0) {
1585
+ kept.push(token);
1586
+ continue;
1587
+ }
1588
+ // A token reaching across two spans, or past the edge of the one it
1589
+ // touches, is a token the splice created or destroyed.
1590
+ if (touched.length > 1)
1591
+ return undefined;
1592
+ const at = side(touched[0]);
1593
+ if (token.start < at.start || token.end > at.end)
1594
+ return undefined;
1595
+ }
1596
+ return kept;
1597
+ };
1598
+ const keptBefore = outside(beforeTokens, (span) => span.before);
1599
+ const keptAfter = outside(afterTokens, (span) => span.after);
1600
+ if (keptBefore === undefined || keptAfter === undefined)
1601
+ return false;
1602
+ if (keptBefore.length !== keptAfter.length)
1603
+ return false;
1604
+ for (let index = 0; index < keptBefore.length; index += 1) {
1605
+ const left = keptBefore[index];
1606
+ const right = keptAfter[index];
1607
+ if (before.slice(left.start, left.end) !== after.slice(right.start, right.end))
1608
+ return false;
1609
+ }
1610
+ return spans.every((span) => afterTokens.some((token) => token.start === span.after.start && token.end === span.after.end));
1611
+ }
1612
+ /**
1613
+ * The rewrite, when cel-js accepts it whole — i.e. when it proves the divergence.
1614
+ *
1615
+ * "Accepts it whole" is {@link celJsAcceptsWhole} rather than `parse()`: a
1616
+ * rewrite that only "parses" because cel-js's lexer dropped a character it has
1617
+ * no token for proves nothing about the character it dropped, and the text the
1618
+ * proof is about is the text including that character. The original is held to
1619
+ * the same bar, so a proof is never built on top of an expression that is not
1620
+ * itself lexically CEL.
1621
+ */
1622
+ function celJsParseProof(text, brackets) {
1623
+ if (firstUnlexableOffset(text.reported) >= 0)
1624
+ return undefined;
1625
+ const rewritten = rewriteAwayCelJsLimitations(text, brackets);
1626
+ return rewritten.applied > 0 && celJsAcceptsWhole(rewritten.text) ? rewritten.text : undefined;
1627
+ }
1628
+ /**
1629
+ * Offset of the first character of `expression` no CEL token can carry, or `-1`
1630
+ * when the spec's lexical grammar covers the text end to end.
1631
+ *
1632
+ * Exported for the same reason {@link celDialectParseProof} is: the coverage
1633
+ * scan is half of what "cel-js parsed it" means here, so it has to be checkable
1634
+ * from outside rather than taken on trust. `-1` is the precondition for every
1635
+ * verdict this module draws from a cel-js parse — see
1636
+ * {@link firstUnlexableOffset} for why `parse()` alone is not enough.
1637
+ */
1638
+ export function celDialectLexicalGap(expression) {
1639
+ return firstUnlexableOffset(expression.trim());
1640
+ }
1641
+ /**
1642
+ * The rewritten expression a `cel-js-rejects-spec-cel` divergence stands on, or
1643
+ * `undefined` when there is none.
1644
+ *
1645
+ * Exported so the proof can be reproduced rather than taken on trust: the
1646
+ * returned text differs from `expression` only where a confirmed cel-js
1647
+ * shortfall was replaced by a same-production spelling cel-js has, cel-js's
1648
+ * lexer carries every character of it and cel-js parses it. An expression with
1649
+ * no identified shortfall, one whose own text is not lexically CEL, or one where
1650
+ * taking the shortfalls away still leaves text cel-js refuses, yields
1651
+ * `undefined` — and that is exactly the case the check refuses to call a
1652
+ * divergence.
1653
+ */
1654
+ export function celDialectParseProof(expression) {
1655
+ const trimmed = expression.trim();
1656
+ if (trimmed.length === 0 || trimmed.length > CEL_DIALECT_MAX_EXPRESSION_LENGTH)
1657
+ return undefined;
1658
+ const text = celDialectText(trimmed);
1659
+ const brackets = buildBracketIndex(text.masked);
1660
+ if (findSpecCelCelJsRejects(text, brackets).length === 0)
1661
+ return undefined;
1662
+ return celJsParseProof(text, brackets);
1663
+ }
1664
+ /**
1665
+ * Text that is not CEL under the spec's own grammar, on any engine.
1666
+ *
1667
+ * The list is built from the spec rather than from an engine, and only from
1668
+ * *absences*: a character or sequence the lexical grammar has no token for, or
1669
+ * a production that cannot be completed. Nothing here is inferred from cel-js
1670
+ * having failed — indeed this runs whether cel-js parsed or not, because its
1671
+ * lexer silently drops a character it has no token for and hands `a === b` to
1672
+ * its parser as `a == b`.
1673
+ *
1674
+ * A trailing `.length` is deliberately *not* in this set. It is a plain
1675
+ * `Member "." SELECTOR`, grammatical on both engines; whether the field exists
1676
+ * is a question about a type this module cannot see.
1677
+ *
1678
+ * @param parsed Whether cel-js's *parser* accepted the token stream. The
1679
+ * unpaired-`?` check is gated on a failure only because a successful parse
1680
+ * already proves every ternary is closed — `?` and `:` are both cel-js tokens,
1681
+ * so neither can be among the characters its lexer dropped — which makes
1682
+ * running the check wasted work rather than unsound.
1683
+ * @param unlexableAt Offset of the first character no CEL token can carry, from
1684
+ * {@link firstUnlexableOffset}, or `-1`. Reported here because "the lexical
1685
+ * grammar has no token for this character" is a fact about the spec, not
1686
+ * about an engine, which is the standard the rest of this bucket is held to.
1687
+ */
1688
+ function findNonCelTokens(text, parsed, unlexableAt) {
1689
+ // Comments are blanked out of both, so a `$` or an `=` written in one is not
1690
+ // a leak: `// a === b` is a comment on any conformant CEL lexer.
1691
+ const expression = text.source;
1692
+ const masked = text.masked;
1693
+ const found = [];
1694
+ const add = (at, length, reason) => {
1695
+ found.push({ at, fragment: expression.slice(at, at + Math.max(length, 8)).trim(), reason });
1696
+ };
1697
+ if (unlexableAt >= 0) {
1698
+ const character = expression[unlexableAt];
1699
+ add(unlexableAt, 8, character === '"' || character === "'"
1700
+ ? 'an unterminated string literal — `STRING_LIT` has to close with the delimiter it opened with, and a single-quoted form cannot span a newline'
1701
+ : `\`${character}\` — a character the CEL lexical grammar has no token for: it is in neither \`IDENT\`, nor any literal form, nor the punctuation list`);
1702
+ }
1703
+ // The spec's punctuation list is `() [] {} . , ? : || && ! < <= >= > == != in
1704
+ // + - * / %`. There is no bare `=` in it and CEL has no assignment, so once
1705
+ // the four comparison operators that contain one are taken out, a remaining
1706
+ // `=` is a character no CEL token can carry: `===`, `!==`, `=>`, `a = b`.
1707
+ const withoutComparisons = masked.replace(/[=!<>]=/g, ' ');
1708
+ const stray = withoutComparisons.indexOf('=');
1709
+ if (stray >= 0) {
1710
+ add(Math.max(0, stray - 2), 6, '`=` outside `==`, `!=`, `<=` or `>=` — CEL has no assignment operator and no `=` token, so `===`, `!==` and `=>` are all JavaScript');
1711
+ }
1712
+ // `IDENT` is `[_a-zA-Z][_a-zA-Z0-9]*` and `$` is in neither it nor the
1713
+ // punctuation list, so a `${` is an un-substituted template placeholder.
1714
+ const template = masked.indexOf('${');
1715
+ if (template >= 0) {
1716
+ add(template, 8, '`${` — a template placeholder that was never substituted; `$` is not a CEL character');
1717
+ }
1718
+ // `Expr = ConditionalOr ["?" ConditionalOr ":" Expr]` is the only production
1719
+ // that consumes a `?`, and it always consumes a `:` with it. A top-level `?`
1720
+ // with no top-level `:` to close it cannot be parsed by any CEL grammar —
1721
+ // which is what `a?.b`, `a?[0]` and `a.?b` each reduce to, the `?` of
1722
+ // JavaScript optional chaining having no ternary behind it.
1723
+ if (!parsed) {
1724
+ let depth = 0;
1725
+ let questions = 0;
1726
+ let colons = 0;
1727
+ let firstQuestion = -1;
1728
+ for (let index = 0; index < masked.length; index += 1) {
1729
+ const character = masked[index];
1730
+ if (character === '(' || character === '[' || character === '{')
1731
+ depth += 1;
1732
+ else if (character === ')' || character === ']' || character === '}')
1733
+ depth -= 1;
1734
+ else if (depth === 0 && character === '?') {
1735
+ questions += 1;
1736
+ if (firstQuestion < 0)
1737
+ firstQuestion = index;
1738
+ }
1739
+ else if (depth === 0 && character === ':')
1740
+ colons += 1;
1741
+ }
1742
+ if (questions > colons && firstQuestion >= 0) {
1743
+ add(firstQuestion, 8, '`?` with no `:` to close the conditional — CEL spells optional access `a.b` behind a `has()` guard or a full `? :`, never as JavaScript optional chaining');
1744
+ }
1745
+ }
1746
+ return found.sort((left, right) => left.at - right.at);
1747
+ }
1748
+ /**
1749
+ * Check one emitted status CEL expression against both dialects.
1750
+ *
1751
+ * @param expression The emitted CEL, without any `${...}` wrapper.
1752
+ * @param field The status leaf path the expression belongs to.
1753
+ */
1754
+ export function checkCelDialectCompatibility(expression, field) {
1755
+ const findings = [];
1756
+ const trimmed = expression.trim();
1757
+ if (trimmed.length === 0)
1758
+ return findings;
1759
+ // Budget gate, before either half. Both halves are linear in the length of
1760
+ // the expression, so an expression that has run away makes the check run away
1761
+ // with it — a 6MB one costs ~6.5s and, having no denylisted form in it,
1762
+ // reports nothing for the trouble. Refuse to spend serialization time on it
1763
+ // and report the size itself, which is the real defect.
1764
+ if (trimmed.length > CEL_DIALECT_MAX_EXPRESSION_LENGTH) {
1765
+ findings.push(finding('expression-too-large', field,
1766
+ // Carry a bounded prefix rather than megabytes of text into the report.
1767
+ `${trimmed.slice(0, 200)}…`, undefined, `the expression is ${trimmed.length} characters, past the ${CEL_DIALECT_MAX_EXPRESSION_LENGTH} character dual-dialect analysis budget, so neither half of the check was run. An expression this size cannot be served by either engine: cel-js spends seconds parsing it on every direct-mode reconcile, and a ResourceGraphDefinition carrying it is past the Kubernetes object size limit`, 'Shrink the status field. An expression this large is a runaway expansion rather than authored status — most often a nested composition whose inlined status re-expands into itself; give the inner composition an explicit status field and reference that instead'));
1768
+ return findings;
1769
+ }
1770
+ // One lexical pass, three aligned texts: what was written, the same with
1771
+ // comments blanked, and that with the string literals blanked too. Every scan
1772
+ // below reads the one it is entitled to; see {@link CelDialectText}.
1773
+ const text = celDialectText(trimmed);
1774
+ // Every bracket pair, resolved once. Both halves of the check walk the same
1775
+ // text repeatedly — the receiver detectors touch every opener in it — and
1776
+ // rescanning forward from each one is what used to make a deeply nested
1777
+ // expression quadratic.
1778
+ const brackets = buildBracketIndex(text.masked);
1779
+ // Half one: what cel-js's parser can and cannot be made to say.
1780
+ //
1781
+ // It can say that *direct mode* cannot evaluate the field, and nothing more.
1782
+ // cel-js 0.8.2 is not a conformant CEL grammar — it rejects `"x".size()`,
1783
+ // `[1,2].size()`, `(a).b` and `1e3`, all of which the spec permits — so a
1784
+ // parse failure on its own is no evidence at all about cel-go or KRO. The
1785
+ // three buckets below each carry their own evidence instead, and only the one
1786
+ // backed by a positive, spec-cited identification of valid CEL is a
1787
+ // divergence.
1788
+ //
1789
+ // "cel-js parsed it" means its lexer carried every character *and* its parser
1790
+ // accepted the token stream. `parse()` checks only the second half — it reads
1791
+ // `parserInstance.errors` and discards `lexResult.errors` — so a text whose
1792
+ // unknown characters its lexer dropped "parses" while cel-js never saw all of
1793
+ // it. `unlexableAt` is the missing half; see {@link firstUnlexableOffset}.
1794
+ const reading = celJsReads(trimmed);
1795
+ const parsed = reading.parsed && reading.unlexableAt < 0;
1796
+ // Bucket one: text no CEL grammar accepts. Built from the spec's own lexical
1797
+ // and syntactic grammar, never from cel-js's verdict, and so run whether or
1798
+ // not cel-js parsed — its lexer drops an unknown character silently, and
1799
+ // `a === b` reaches its parser as `a == b`.
1800
+ const nonCel = findNonCelTokens(text, reading.parsed, reading.unlexableAt);
1801
+ const leak = nonCel[0];
1802
+ // Hoisted out of bucket two: half two is gated on it. See the comment there.
1803
+ let proof;
1804
+ if (leak !== undefined) {
1805
+ findings.push(finding('not-valid-cel', field, trimmed, leak.fragment, `this is not CEL under the language grammar, whichever engine reads it: ${leak.reason}. Both engines reject it at the parse, before any type environment is consulted, so direct mode can never evaluate this status field and cel-go cannot parse it inside the KRO controller either`, 'Usually JavaScript that survived conversion. Write the CEL form instead: a has() guard rather than `?.`, an index rather than `?[`, `==` rather than `===`, and a resolved reference rather than an un-substituted template placeholder'));
1806
+ }
1807
+ else if (!parsed) {
1808
+ // Bucket two: a cel-js parse failure that a positive check identifies as
1809
+ // grammatical CEL. This may fail strict mode.
1810
+ //
1811
+ // What it claims, exactly: the CEL grammar permits the form, cel-js's
1812
+ // refusal is cel-js's own shortfall, and direct mode can therefore never
1813
+ // evaluate this field. It does *not* claim the field resolves under KRO.
1814
+ // Parsing is not evaluating: cel-go's type checker runs after the parse with
1815
+ // KRO's type environment and function set, and it rejects plenty of
1816
+ // grammatical CEL — `1.string()` parses, but `string` is a global conversion
1817
+ // function rather than a member, so the checker refuses the call; an unknown
1818
+ // member function goes the same way. Nothing here models that checker, so
1819
+ // nothing here may speak for it.
1820
+ //
1821
+ // `divergence` is still the right kind under that weaker claim. The two
1822
+ // engines disagree on the *form*, at the parse stage both of them have and
1823
+ // which needs no type environment at all: the spec's grammar admits it,
1824
+ // cel-go implements that grammar, and cel-js does not. Direct mode is
1825
+ // therefore broken on an expression the language permits, whatever cel-go's
1826
+ // checker later decides — so the emitted CEL is defective for one of the two
1827
+ // targets TypeKro serializes for, which is what strict mode exists to catch.
1828
+ //
1829
+ // Finding a known cel-js shortfall *somewhere* in the text is not enough to
1830
+ // get here. A parse failure has exactly one cause the whole expression can
1831
+ // be blamed on, and an expression that carries a shortfall may also simply
1832
+ // be ungrammatical elsewhere — `"x".size() +`, `(a).b ==`, `"x".size())`.
1833
+ // cel-go rejects those too, so calling them divergences would fail strict
1834
+ // mode on genuinely invalid CEL. The stronger claim is the one that has to
1835
+ // hold: rewrite the identified shortfalls into spellings cel-js does have,
1836
+ // leaving everything else untouched, and ask cel-js again. Only if the
1837
+ // rewrite parses is the shortfall the *only* obstruction, which is what
1838
+ // makes the rest of the expression grammatical CEL and cel-go's acceptance
1839
+ // of it a fact rather than an inference.
1840
+ const limitation = findSpecCelCelJsRejects(text, brackets)[0];
1841
+ proof = limitation === undefined ? undefined : celJsParseProof(text, brackets);
1842
+ if (limitation !== undefined && proof !== undefined) {
1843
+ findings.push(finding('cel-js-rejects-spec-cel', field, trimmed, limitation.fragment,
1844
+ // The proof keeps the original's offsets, so a blanked comment leaves a
1845
+ // run of spaces in it. The quoted form is trimmed: that changes no
1846
+ // token, and keeps it something a reader can paste into `parse()`.
1847
+ `cel-js cannot parse this, but the CEL grammar permits it: ${limitation.reason} (cel-spec doc/langdef.md, "Syntax"). Replacing only that form with a spelling cel-js does have — \`${excerpt(proof.trim())}\` — makes cel-js parse the whole expression, so nothing else in it is ungrammatical and the refusal is cel-js's shortfall alone. Direct mode can therefore never evaluate this field. Whether KRO evaluates it is a further question this check does not model: cel-go parses the form, but its type checker then runs with KRO's type environment and function set and may still reject it`, 'Rewrite the receiver as an identifier chain — bind the literal or parenthesized value to a resource field, or use the global form of the call (`size(x)` rather than `x.size()`) — until cel-js supports the spec form'));
1848
+ }
1849
+ else {
1850
+ // Bucket three: cel-js cannot parse it and nothing above explains why —
1851
+ // either no known shortfall was identified, or one was and taking it away
1852
+ // still left text cel-js refuses, which means something else in the
1853
+ // expression is at fault and no divergence is established. The only sound
1854
+ // claim either way is about direct mode.
1855
+ const unexplained = limitation === undefined
1856
+ ? 'Neither the non-CEL token scan nor the confirmed cel-js shortfalls account for the refusal'
1857
+ : `A confirmed cel-js shortfall was identified in it (${limitation.reason}), but rewriting that form into one cel-js does have still leaves the expression unparseable, so the shortfall is not the whole reason and nothing here is established beyond direct mode`;
1858
+ findings.push(finding('cel-js-parse-failure', field, trimmed, undefined, `cel-js cannot parse this expression, so direct mode cannot evaluate this field; the CEL specification may still permit it, and the controller may still serve it. ${unexplained}. Verify against the spec grammar; if it is valid CEL, this is a cel-js limitation worth reporting upstream`, 'Check the expression against cel-spec doc/langdef.md. If the grammar permits it, only direct mode is certainly affected — whether Kro mode serves the field also depends on cel-go\'s type checker under KRO\'s environment, which this check does not model. Otherwise fix the emitted CEL'));
1859
+ }
1860
+ }
1861
+ // Half two: the curated cel-go/cel-js divergence denylist, gated on the
1862
+ // expression being established grammatical.
1863
+ //
1864
+ // Every rule here is regex- and bracket-mask-based rather than tree-based, so
1865
+ // none of them *needs* a parse to run — and that is exactly why the gate has
1866
+ // to be explicit. What each rule needs is not a parse tree but the structure
1867
+ // it reads off the text: `has-index-argument` wants the argument list of a
1868
+ // `has(` to be a real bracket pair, `heterogeneous-map-literal` wants `{`…`}`
1869
+ // to be a map literal, and `guard-after-use-in-logical-chain` wants the
1870
+ // top-level `&&`/`||` split to be the expression's actual operator chain. On
1871
+ // ungrammatical text none of that is established: `a?.b && has(list[0].f)` and
1872
+ // `foo(((( && has(list[0].f)` are not CEL at all, yet both used to report a
1873
+ // `has-index-argument` divergence and so could fail strict mode on text cel-go
1874
+ // rejects outright — the exact class of false positive this check exists to
1875
+ // avoid. cel-js merely failing to parse something is *not* that case, which is
1876
+ // the point the gate has to be careful about: cel-js is not a conformant
1877
+ // grammar, so its refusal alone establishes nothing either way.
1878
+ //
1879
+ // So the gate is grammaticality, established one of two ways:
1880
+ //
1881
+ // - cel-js accepted the whole expression — its lexer carried every character
1882
+ // and its parser took the token stream. cel-js's grammar is a subset of the
1883
+ // spec's, so anything it accepts is CEL.
1884
+ // - the rewrite proof succeeded. The proof text is grammatical CEL, and the
1885
+ // original differs from it only inside the rewritten spans — each of which
1886
+ // is itself either a single well-formed literal token or a bracketed span
1887
+ // cel-js has separately been shown to accept (see
1888
+ // {@link celJsAcceptsSwallowedSpan}). So the original is grammatical too.
1889
+ //
1890
+ // Under a proof the rules run over the **original** text, not the proof text.
1891
+ // That is both sound and necessary. Sound, because a rewrite substitutes a
1892
+ // same-production spelling for a balanced span: a `receiver` rewrite replaces
1893
+ // a whole bracket pair (or a quoted token) with an identifier and the `string`
1894
+ // and `float` rewrites replace one literal token with another, so no bracket
1895
+ // pair outside a rewritten span is opened, closed or re-paired, and no
1896
+ // top-level `&&`/`||`/`?`/`:` is added or removed — every operator a rewrite
1897
+ // takes away was inside a bracket pair and so was never part of the top-level
1898
+ // chain. Necessary, because the rules must be able to read *inside* the
1899
+ // rewritten spans: `has(list[0].f).x` collapses to `__typekro_recv0.x`, and
1900
+ // the `has()` the rule is looking for survives only in the original.
1901
+ //
1902
+ // On text that is neither parsed nor proven, nothing here runs — note-kind
1903
+ // rules included. A note about a map literal in text that is not CEL describes
1904
+ // a structure that is not there, and the expression is already reported by
1905
+ // half one.
1906
+ if (!(parsed || proof !== undefined))
1907
+ return findings;
1908
+ checkHasIndexArgument(text, field, findings, brackets);
1909
+ checkHeterogeneousMapLiteral(text, field, findings, brackets);
1910
+ checkInOnListEntry(text, field, findings);
1911
+ const blanked = blankLazyRegions(text.masked, brackets);
1912
+ checkLogicalChain(text, blanked, field, { start: 0, end: text.masked.length }, [], findings, 0, brackets);
1913
+ return findings;
1914
+ }
1915
+ /** Strip the KRO `${...}` wrapper from an emitted status value. */
1916
+ function unwrapKroExpression(value) {
1917
+ const match = /^\$\{([\s\S]*)\}$/.exec(value.trim());
1918
+ return match?.[1];
1919
+ }
1920
+ /**
1921
+ * Check every emitted status CEL expression in a serialized status map.
1922
+ *
1923
+ * Accepts the nested shape `serializeStatusMappingsToCel` produces: leaves are
1924
+ * `${...}`-wrapped CEL strings, and objects/arrays nest arbitrarily. Values
1925
+ * that carry no CEL are skipped, so plain literal status fields cost nothing.
1926
+ */
1927
+ export function collectStatusCelDialectFindings(statusCel) {
1928
+ const findings = [];
1929
+ const walk = (value, path) => {
1930
+ if (typeof value === 'string') {
1931
+ const expression = unwrapKroExpression(value);
1932
+ if (expression !== undefined)
1933
+ findings.push(...checkCelDialectCompatibility(expression, path));
1934
+ return;
1935
+ }
1936
+ if (Array.isArray(value)) {
1937
+ value.forEach((item, index) => walk(item, `${path}[${index}]`));
1938
+ return;
1939
+ }
1940
+ if (value !== null && typeof value === 'object') {
1941
+ for (const [key, nested] of Object.entries(value)) {
1942
+ walk(nested, path === '' ? key : `${path}.${key}`);
1943
+ }
1944
+ }
1945
+ };
1946
+ for (const [key, value] of Object.entries(statusCel))
1947
+ walk(value, key);
1948
+ return findings;
1949
+ }
1950
+ /**
1951
+ * Longest expression or fragment a report will quote, in characters.
1952
+ *
1953
+ * The analysis budget already keeps `checkCelDialectCompatibility` from
1954
+ * returning an oversized `expression`, but the formatter is exported and takes
1955
+ * findings from wherever the caller got them — so it bounds what it quotes on
1956
+ * its own rather than trusting its input. A report is read by a human; past a
1957
+ * couple of lines per finding the excerpt stops helping and starts being the
1958
+ * thing that fills the log.
1959
+ */
1960
+ export const CEL_DIALECT_MAX_EXCERPT_LENGTH = 400;
1961
+ /** Quote at most {@link CEL_DIALECT_MAX_EXCERPT_LENGTH} characters of a snippet. */
1962
+ function excerpt(text) {
1963
+ if (text.length <= CEL_DIALECT_MAX_EXCERPT_LENGTH)
1964
+ return text;
1965
+ // `slice` counts UTF-16 code units like everything else here, so the cut can
1966
+ // land between the two halves of a surrogate pair and render the last
1967
+ // character as a replacement glyph. Step back one unit when it does.
1968
+ const lead = text.charCodeAt(CEL_DIALECT_MAX_EXCERPT_LENGTH - 1);
1969
+ const cut = lead >= 0xd800 && lead <= 0xdbff
1970
+ ? CEL_DIALECT_MAX_EXCERPT_LENGTH - 1
1971
+ : CEL_DIALECT_MAX_EXCERPT_LENGTH;
1972
+ return `${text.slice(0, cut)}… (${text.length} characters)`;
1973
+ }
1974
+ /** How a finding's verdict reads at the head of its report entry. */
1975
+ function verdict(found) {
1976
+ if (found.dialect === 'unchecked')
1977
+ return 'not checked';
1978
+ if (found.dialect === 'both')
1979
+ return 'rejected by both dialects';
1980
+ // A cel-js parse failure is a certainty about cel-js and says nothing at all
1981
+ // about the other engine, so it reads as neither "rejected" (which would
1982
+ // imply a verdict was reached on the form) nor "may be rejected".
1983
+ if (found.rule === 'cel-js-parse-failure')
1984
+ return 'not parseable by cel-js, cel-go unknown';
1985
+ return `${found.kind === 'divergence' ? 'rejected by' : 'may be rejected by'} ${found.dialect}`;
1986
+ }
1987
+ /** Render findings as a multi-line report naming the leaf, dialect and expression. */
1988
+ export function formatCelDialectFindings(findings) {
1989
+ return findings
1990
+ .map((found) => ` status.${found.field}: ${verdict(found)} [${found.rule}, ${found.kind}]\n` +
1991
+ ` ${found.message}\n` +
1992
+ (found.fragment ? ` at: ${excerpt(found.fragment)}\n` : '') +
1993
+ ` expression: ${excerpt(found.expression)}\n` +
1994
+ ` fix: ${found.suggestion}`)
1995
+ .join('\n');
1996
+ }
1997
+ //# sourceMappingURL=cel-dialect.js.map