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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/compositions/typekro-runtime/typekro-runtime.d.ts +0 -33
- package/dist/compositions/typekro-runtime/typekro-runtime.d.ts.map +1 -1
- package/dist/compositions/typekro-runtime/typekro-runtime.js +63 -1
- package/dist/compositions/typekro-runtime/typekro-runtime.js.map +1 -1
- package/dist/compositions/typekro-runtime/types.d.ts +8 -0
- package/dist/compositions/typekro-runtime/types.d.ts.map +1 -1
- package/dist/compositions/typekro-runtime/types.js +1 -0
- package/dist/compositions/typekro-runtime/types.js.map +1 -1
- package/dist/core/aspects/values-merge.d.ts +17 -0
- package/dist/core/aspects/values-merge.d.ts.map +1 -1
- package/dist/core/aspects/values-merge.js +67 -8
- package/dist/core/aspects/values-merge.js.map +1 -1
- package/dist/core/deployment/direct-factory.d.ts +9 -0
- package/dist/core/deployment/direct-factory.d.ts.map +1 -1
- package/dist/core/deployment/direct-factory.js +37 -3
- package/dist/core/deployment/direct-factory.js.map +1 -1
- package/dist/core/deployment/engine.d.ts +34 -0
- package/dist/core/deployment/engine.d.ts.map +1 -1
- package/dist/core/deployment/engine.js +173 -29
- package/dist/core/deployment/engine.js.map +1 -1
- package/dist/core/deployment/index.d.ts +2 -0
- package/dist/core/deployment/index.d.ts.map +1 -1
- package/dist/core/deployment/index.js +1 -0
- package/dist/core/deployment/index.js.map +1 -1
- package/dist/core/deployment/k8s-helpers.d.ts +73 -0
- package/dist/core/deployment/k8s-helpers.d.ts.map +1 -1
- package/dist/core/deployment/k8s-helpers.js +198 -4
- package/dist/core/deployment/k8s-helpers.js.map +1 -1
- package/dist/core/deployment/kro-factory.d.ts.map +1 -1
- package/dist/core/deployment/kro-factory.js +21 -1
- package/dist/core/deployment/kro-factory.js.map +1 -1
- package/dist/core/deployment/status-leaf-resolution.d.ts +101 -0
- package/dist/core/deployment/status-leaf-resolution.d.ts.map +1 -0
- package/dist/core/deployment/status-leaf-resolution.js +232 -0
- package/dist/core/deployment/status-leaf-resolution.js.map +1 -0
- package/dist/core/deployment/strategies/base-strategy.d.ts.map +1 -1
- package/dist/core/deployment/strategies/base-strategy.js +31 -3
- package/dist/core/deployment/strategies/base-strategy.js.map +1 -1
- package/dist/core/deployment/strategies/kro-strategy.d.ts.map +1 -1
- package/dist/core/deployment/strategies/kro-strategy.js +8 -1
- package/dist/core/deployment/strategies/kro-strategy.js.map +1 -1
- package/dist/core/expressions/composition/composition-analyzer-helpers.d.ts.map +1 -1
- package/dist/core/expressions/composition/composition-analyzer-helpers.js +20 -2
- package/dist/core/expressions/composition/composition-analyzer-helpers.js.map +1 -1
- package/dist/core/kro/index.d.ts +6 -0
- package/dist/core/kro/index.d.ts.map +1 -0
- package/dist/core/kro/index.js +6 -0
- package/dist/core/kro/index.js.map +1 -0
- package/dist/core/kro/label-guard-capability.d.ts +141 -0
- package/dist/core/kro/label-guard-capability.d.ts.map +1 -0
- package/dist/core/kro/label-guard-capability.js +257 -0
- package/dist/core/kro/label-guard-capability.js.map +1 -0
- package/dist/core/kro/labels.d.ts +56 -0
- package/dist/core/kro/labels.d.ts.map +1 -0
- package/dist/core/kro/labels.js +62 -0
- package/dist/core/kro/labels.js.map +1 -0
- package/dist/core/kubernetes/api-capability.d.ts +196 -0
- package/dist/core/kubernetes/api-capability.d.ts.map +1 -0
- package/dist/core/kubernetes/api-capability.js +301 -0
- package/dist/core/kubernetes/api-capability.js.map +1 -0
- package/dist/core/kubernetes/index.d.ts +2 -0
- package/dist/core/kubernetes/index.d.ts.map +1 -1
- package/dist/core/kubernetes/index.js +2 -0
- package/dist/core/kubernetes/index.js.map +1 -1
- package/dist/core/kubernetes/naming.d.ts +100 -0
- package/dist/core/kubernetes/naming.d.ts.map +1 -0
- package/dist/core/kubernetes/naming.js +93 -0
- package/dist/core/kubernetes/naming.js.map +1 -0
- package/dist/core/planning/direct-runtime-adapter.d.ts.map +1 -1
- package/dist/core/planning/direct-runtime-adapter.js +54 -17
- package/dist/core/planning/direct-runtime-adapter.js.map +1 -1
- package/dist/core/planning/planner.d.ts.map +1 -1
- package/dist/core/planning/planner.js +17 -0
- package/dist/core/planning/planner.js.map +1 -1
- package/dist/core/planning/schema.d.ts.map +1 -1
- package/dist/core/planning/schema.js +7 -3
- package/dist/core/planning/schema.js.map +1 -1
- package/dist/core/references/cel-lexical-scanner.d.ts +100 -1
- package/dist/core/references/cel-lexical-scanner.d.ts.map +1 -1
- package/dist/core/references/cel-lexical-scanner.js +196 -2
- package/dist/core/references/cel-lexical-scanner.js.map +1 -1
- package/dist/core/references/cel.d.ts +249 -1
- package/dist/core/references/cel.d.ts.map +1 -1
- package/dist/core/references/cel.js +289 -8
- package/dist/core/references/cel.js.map +1 -1
- package/dist/core/serialization/arktype-ast.d.ts +21 -0
- package/dist/core/serialization/arktype-ast.d.ts.map +1 -0
- package/dist/core/serialization/arktype-ast.js +29 -0
- package/dist/core/serialization/arktype-ast.js.map +1 -0
- package/dist/core/serialization/cel-references.d.ts +133 -32
- package/dist/core/serialization/cel-references.d.ts.map +1 -1
- package/dist/core/serialization/cel-references.js +908 -109
- package/dist/core/serialization/cel-references.js.map +1 -1
- package/dist/core/serialization/core.d.ts.map +1 -1
- package/dist/core/serialization/core.js +13 -2
- package/dist/core/serialization/core.js.map +1 -1
- package/dist/core/serialization/schema.d.ts +16 -2
- package/dist/core/serialization/schema.d.ts.map +1 -1
- package/dist/core/serialization/schema.js +113 -21
- package/dist/core/serialization/schema.js.map +1 -1
- package/dist/core/types/composable.d.ts +7 -1
- package/dist/core/types/composable.d.ts.map +1 -1
- package/dist/core/types/deployment.d.ts +32 -2
- package/dist/core/types/deployment.d.ts.map +1 -1
- package/dist/core/types/serialization.d.ts +37 -0
- package/dist/core/types/serialization.d.ts.map +1 -1
- package/dist/core/validation/cel-dialect.d.ts +246 -0
- package/dist/core/validation/cel-dialect.d.ts.map +1 -0
- package/dist/core/validation/cel-dialect.js +1997 -0
- package/dist/core/validation/cel-dialect.js.map +1 -0
- package/dist/core/validation/cel-validator.d.ts.map +1 -1
- package/dist/core/validation/cel-validator.js +8 -1
- package/dist/core/validation/cel-validator.js.map +1 -1
- package/dist/core/validation/literal-status.d.ts +107 -0
- package/dist/core/validation/literal-status.d.ts.map +1 -0
- package/dist/core/validation/literal-status.js +346 -0
- package/dist/core/validation/literal-status.js.map +1 -0
- package/dist/core/validation/structural-spec-dependence.d.ts +179 -0
- package/dist/core/validation/structural-spec-dependence.d.ts.map +1 -0
- package/dist/core/validation/structural-spec-dependence.js +911 -0
- package/dist/core/validation/structural-spec-dependence.js.map +1 -0
- package/dist/factories/apisix/compositions/apisix-bootstrap.d.ts.map +1 -1
- package/dist/factories/apisix/compositions/apisix-bootstrap.js +9 -0
- package/dist/factories/apisix/compositions/apisix-bootstrap.js.map +1 -1
- package/dist/factories/clickhouse/compositions/clickhouse-cluster.d.ts +6 -0
- package/dist/factories/clickhouse/compositions/clickhouse-cluster.d.ts.map +1 -1
- package/dist/factories/clickhouse/compositions/clickhouse-cluster.js +273 -23
- package/dist/factories/clickhouse/compositions/clickhouse-cluster.js.map +1 -1
- package/dist/factories/clickhouse/resources/index.d.ts +1 -0
- package/dist/factories/clickhouse/resources/index.d.ts.map +1 -1
- package/dist/factories/clickhouse/resources/index.js +1 -0
- package/dist/factories/clickhouse/resources/index.js.map +1 -1
- package/dist/factories/clickhouse/resources/installation.d.ts.map +1 -1
- package/dist/factories/clickhouse/resources/installation.js +31 -4
- package/dist/factories/clickhouse/resources/installation.js.map +1 -1
- package/dist/factories/clickhouse/resources/s3-backup.d.ts +215 -0
- package/dist/factories/clickhouse/resources/s3-backup.d.ts.map +1 -0
- package/dist/factories/clickhouse/resources/s3-backup.js +410 -0
- package/dist/factories/clickhouse/resources/s3-backup.js.map +1 -0
- package/dist/factories/clickhouse/types.d.ts +416 -20
- package/dist/factories/clickhouse/types.d.ts.map +1 -1
- package/dist/factories/clickhouse/types.js +235 -11
- package/dist/factories/clickhouse/types.js.map +1 -1
- package/dist/factories/clickhouse/utils/helm-values-mapper.d.ts +42 -0
- package/dist/factories/clickhouse/utils/helm-values-mapper.d.ts.map +1 -1
- package/dist/factories/clickhouse/utils/helm-values-mapper.js +48 -0
- package/dist/factories/clickhouse/utils/helm-values-mapper.js.map +1 -1
- package/dist/factories/clickhouse/utils/index.d.ts +2 -0
- package/dist/factories/clickhouse/utils/index.d.ts.map +1 -1
- package/dist/factories/clickhouse/utils/index.js +2 -0
- package/dist/factories/clickhouse/utils/index.js.map +1 -1
- package/dist/factories/clickhouse/utils/s3-storage.d.ts +503 -0
- package/dist/factories/clickhouse/utils/s3-storage.d.ts.map +1 -0
- package/dist/factories/clickhouse/utils/s3-storage.js +1139 -0
- package/dist/factories/clickhouse/utils/s3-storage.js.map +1 -0
- package/dist/factories/clickhouse/utils/validation.d.ts +103 -0
- package/dist/factories/clickhouse/utils/validation.d.ts.map +1 -1
- package/dist/factories/clickhouse/utils/validation.js +129 -0
- package/dist/factories/clickhouse/utils/validation.js.map +1 -1
- package/dist/factories/clickhouse/utils/xml.d.ts +87 -0
- package/dist/factories/clickhouse/utils/xml.d.ts.map +1 -0
- package/dist/factories/clickhouse/utils/xml.js +151 -0
- package/dist/factories/clickhouse/utils/xml.js.map +1 -0
- package/dist/factories/clickstack/compositions/clickstack-bootstrap.d.ts +57 -0
- package/dist/factories/clickstack/compositions/clickstack-bootstrap.d.ts.map +1 -1
- package/dist/factories/clickstack/compositions/clickstack-bootstrap.js +266 -7
- package/dist/factories/clickstack/compositions/clickstack-bootstrap.js.map +1 -1
- package/dist/factories/clickstack/types.d.ts +205 -26
- package/dist/factories/clickstack/types.d.ts.map +1 -1
- package/dist/factories/clickstack/types.js +106 -8
- package/dist/factories/clickstack/types.js.map +1 -1
- package/dist/factories/clickstack/utils/collector-config.d.ts +104 -0
- package/dist/factories/clickstack/utils/collector-config.d.ts.map +1 -0
- package/dist/factories/clickstack/utils/collector-config.js +266 -0
- package/dist/factories/clickstack/utils/collector-config.js.map +1 -0
- package/dist/factories/clickstack/utils/helm-values-mapper.d.ts +26 -0
- package/dist/factories/clickstack/utils/helm-values-mapper.d.ts.map +1 -1
- package/dist/factories/clickstack/utils/helm-values-mapper.js +53 -12
- package/dist/factories/clickstack/utils/helm-values-mapper.js.map +1 -1
- package/dist/factories/clickstack/utils/index.d.ts +2 -0
- package/dist/factories/clickstack/utils/index.d.ts.map +1 -1
- package/dist/factories/clickstack/utils/index.js +2 -0
- package/dist/factories/clickstack/utils/index.js.map +1 -1
- package/dist/factories/clickstack/utils/storage.d.ts +449 -0
- package/dist/factories/clickstack/utils/storage.d.ts.map +1 -0
- package/dist/factories/clickstack/utils/storage.js +698 -0
- package/dist/factories/clickstack/utils/storage.js.map +1 -0
- package/dist/factories/cnpg/resources/helm.d.ts.map +1 -1
- package/dist/factories/cnpg/resources/helm.js +4 -4
- package/dist/factories/cnpg/resources/helm.js.map +1 -1
- package/dist/factories/cnpg/types.d.ts +10 -1
- package/dist/factories/cnpg/types.d.ts.map +1 -1
- package/dist/factories/cnpg/types.js.map +1 -1
- package/dist/factories/cnpg/utils/helm-values-mapper.d.ts +19 -2
- package/dist/factories/cnpg/utils/helm-values-mapper.d.ts.map +1 -1
- package/dist/factories/cnpg/utils/helm-values-mapper.js +22 -4
- package/dist/factories/cnpg/utils/helm-values-mapper.js.map +1 -1
- package/dist/factories/envoy-ai-gateway/compositions/gateway.d.ts.map +1 -1
- package/dist/factories/envoy-ai-gateway/compositions/gateway.js +16 -3
- package/dist/factories/envoy-ai-gateway/compositions/gateway.js.map +1 -1
- package/dist/factories/envoy-ai-gateway/constants.d.ts +5 -2
- package/dist/factories/envoy-ai-gateway/constants.d.ts.map +1 -1
- package/dist/factories/envoy-ai-gateway/constants.js +5 -2
- package/dist/factories/envoy-ai-gateway/constants.js.map +1 -1
- package/dist/factories/envoy-ai-gateway/resources/gateway.d.ts +12 -0
- package/dist/factories/envoy-ai-gateway/resources/gateway.d.ts.map +1 -1
- package/dist/factories/envoy-ai-gateway/resources/gateway.js +27 -104
- package/dist/factories/envoy-ai-gateway/resources/gateway.js.map +1 -1
- package/dist/factories/envoy-ai-gateway/types.d.ts +15 -47
- package/dist/factories/envoy-ai-gateway/types.d.ts.map +1 -1
- package/dist/factories/envoy-ai-gateway/types.js.map +1 -1
- package/dist/factories/gateway-api/constants.d.ts +27 -0
- package/dist/factories/gateway-api/constants.d.ts.map +1 -0
- package/dist/factories/gateway-api/constants.js +27 -0
- package/dist/factories/gateway-api/constants.js.map +1 -0
- package/dist/factories/gateway-api/index.d.ts +14 -0
- package/dist/factories/gateway-api/index.d.ts.map +1 -0
- package/dist/factories/gateway-api/index.js +14 -0
- package/dist/factories/gateway-api/index.js.map +1 -0
- package/dist/factories/gateway-api/readiness.d.ts +67 -0
- package/dist/factories/gateway-api/readiness.d.ts.map +1 -0
- package/dist/factories/gateway-api/readiness.js +213 -0
- package/dist/factories/gateway-api/readiness.js.map +1 -0
- package/dist/factories/gateway-api/resources/gateway.d.ts +91 -0
- package/dist/factories/gateway-api/resources/gateway.d.ts.map +1 -0
- package/dist/factories/gateway-api/resources/gateway.js +99 -0
- package/dist/factories/gateway-api/resources/gateway.js.map +1 -0
- package/dist/factories/gateway-api/resources/index.d.ts +2 -0
- package/dist/factories/gateway-api/resources/index.d.ts.map +1 -0
- package/dist/factories/gateway-api/resources/index.js +2 -0
- package/dist/factories/gateway-api/resources/index.js.map +1 -0
- package/dist/factories/gateway-api/types.d.ts +1269 -0
- package/dist/factories/gateway-api/types.d.ts.map +1 -0
- package/dist/factories/gateway-api/types.js +417 -0
- package/dist/factories/gateway-api/types.js.map +1 -0
- package/dist/factories/helm/readiness-evaluators.d.ts +21 -3
- package/dist/factories/helm/readiness-evaluators.d.ts.map +1 -1
- package/dist/factories/helm/readiness-evaluators.js +117 -11
- package/dist/factories/helm/readiness-evaluators.js.map +1 -1
- package/dist/factories/helm/types.d.ts +28 -0
- package/dist/factories/helm/types.d.ts.map +1 -1
- package/dist/factories/index.d.ts +2 -0
- package/dist/factories/index.d.ts.map +1 -1
- package/dist/factories/index.js +8 -0
- package/dist/factories/index.js.map +1 -1
- package/dist/factories/inngest/compositions/inngest-bootstrap.d.ts.map +1 -1
- package/dist/factories/inngest/compositions/inngest-bootstrap.js +10 -0
- package/dist/factories/inngest/compositions/inngest-bootstrap.js.map +1 -1
- package/dist/factories/kubernetes/admission/index.d.ts +2 -0
- package/dist/factories/kubernetes/admission/index.d.ts.map +1 -1
- package/dist/factories/kubernetes/admission/index.js +2 -0
- package/dist/factories/kubernetes/admission/index.js.map +1 -1
- package/dist/factories/kubernetes/admission/label-propagation-guard.d.ts +108 -0
- package/dist/factories/kubernetes/admission/label-propagation-guard.d.ts.map +1 -0
- package/dist/factories/kubernetes/admission/label-propagation-guard.js +247 -0
- package/dist/factories/kubernetes/admission/label-propagation-guard.js.map +1 -0
- package/dist/factories/kubernetes/admission/mutating-admission-policy.d.ts +286 -0
- package/dist/factories/kubernetes/admission/mutating-admission-policy.d.ts.map +1 -0
- package/dist/factories/kubernetes/admission/mutating-admission-policy.js +157 -0
- package/dist/factories/kubernetes/admission/mutating-admission-policy.js.map +1 -0
- package/dist/factories/opensearch/compositions/cluster.d.ts.map +1 -1
- package/dist/factories/opensearch/compositions/cluster.js +6 -1
- package/dist/factories/opensearch/compositions/cluster.js.map +1 -1
- package/dist/factories/ory/compositions/ory-identity-stack.d.ts +8 -8
- package/dist/factories/ory/types.d.ts +16 -16
- package/dist/factories/rook/compositions/object-storage-claim.d.ts +1 -1
- package/dist/factories/rook/compositions/rook-ceph-platform.js +8 -1
- package/dist/factories/rook/compositions/rook-ceph-platform.js.map +1 -1
- package/dist/factories/rook/types.d.ts +1 -1
- package/dist/factories/searxng/compositions/searxng-bootstrap.d.ts +1 -1
- package/dist/factories/searxng/compositions/searxng-bootstrap.d.ts.map +1 -1
- package/dist/factories/searxng/compositions/searxng-bootstrap.js +18 -13
- package/dist/factories/searxng/compositions/searxng-bootstrap.js.map +1 -1
- package/dist/factories/searxng/index.d.ts +3 -0
- package/dist/factories/searxng/index.d.ts.map +1 -1
- package/dist/factories/searxng/index.js +3 -0
- package/dist/factories/searxng/index.js.map +1 -1
- package/dist/factories/searxng/types.js +1 -1
- package/dist/factories/searxng/types.js.map +1 -1
- package/dist/factories/traefik/compositions/index.d.ts +6 -0
- package/dist/factories/traefik/compositions/index.d.ts.map +1 -0
- package/dist/factories/traefik/compositions/index.js +6 -0
- package/dist/factories/traefik/compositions/index.js.map +1 -0
- package/dist/factories/traefik/compositions/traefik-bootstrap.d.ts +185 -0
- package/dist/factories/traefik/compositions/traefik-bootstrap.d.ts.map +1 -0
- package/dist/factories/traefik/compositions/traefik-bootstrap.js +339 -0
- package/dist/factories/traefik/compositions/traefik-bootstrap.js.map +1 -0
- package/dist/factories/traefik/compositions/traefik-helm-repository.d.ts +16 -0
- package/dist/factories/traefik/compositions/traefik-helm-repository.d.ts.map +1 -0
- package/dist/factories/traefik/compositions/traefik-helm-repository.js +31 -0
- package/dist/factories/traefik/compositions/traefik-helm-repository.js.map +1 -0
- package/dist/factories/traefik/constants.d.ts +78 -0
- package/dist/factories/traefik/constants.d.ts.map +1 -0
- package/dist/factories/traefik/constants.js +104 -0
- package/dist/factories/traefik/constants.js.map +1 -0
- package/dist/factories/traefik/index.d.ts +25 -0
- package/dist/factories/traefik/index.d.ts.map +1 -0
- package/dist/factories/traefik/index.js +25 -0
- package/dist/factories/traefik/index.js.map +1 -0
- package/dist/factories/traefik/resources/common.d.ts +68 -0
- package/dist/factories/traefik/resources/common.d.ts.map +1 -0
- package/dist/factories/traefik/resources/common.js +59 -0
- package/dist/factories/traefik/resources/common.js.map +1 -0
- package/dist/factories/traefik/resources/gateway.d.ts +85 -0
- package/dist/factories/traefik/resources/gateway.d.ts.map +1 -0
- package/dist/factories/traefik/resources/gateway.js +109 -0
- package/dist/factories/traefik/resources/gateway.js.map +1 -0
- package/dist/factories/traefik/resources/helm.d.ts +56 -0
- package/dist/factories/traefik/resources/helm.d.ts.map +1 -0
- package/dist/factories/traefik/resources/helm.js +131 -0
- package/dist/factories/traefik/resources/helm.js.map +1 -0
- package/dist/factories/traefik/resources/index.d.ts +10 -0
- package/dist/factories/traefik/resources/index.d.ts.map +1 -0
- package/dist/factories/traefik/resources/index.js +10 -0
- package/dist/factories/traefik/resources/index.js.map +1 -0
- package/dist/factories/traefik/resources/middleware.d.ts +169 -0
- package/dist/factories/traefik/resources/middleware.d.ts.map +1 -0
- package/dist/factories/traefik/resources/middleware.js +225 -0
- package/dist/factories/traefik/resources/middleware.js.map +1 -0
- package/dist/factories/traefik/resources/routing.d.ts +113 -0
- package/dist/factories/traefik/resources/routing.d.ts.map +1 -0
- package/dist/factories/traefik/resources/routing.js +119 -0
- package/dist/factories/traefik/resources/routing.js.map +1 -0
- package/dist/factories/traefik/resources/tls.d.ts +85 -0
- package/dist/factories/traefik/resources/tls.d.ts.map +1 -0
- package/dist/factories/traefik/resources/tls.js +93 -0
- package/dist/factories/traefik/resources/tls.js.map +1 -0
- package/dist/factories/traefik/types.d.ts +2690 -0
- package/dist/factories/traefik/types.d.ts.map +1 -0
- package/dist/factories/traefik/types.js +1084 -0
- package/dist/factories/traefik/types.js.map +1 -0
- package/dist/factories/traefik/utils/helm-values-mapper.d.ts +198 -0
- package/dist/factories/traefik/utils/helm-values-mapper.d.ts.map +1 -0
- package/dist/factories/traefik/utils/helm-values-mapper.js +372 -0
- package/dist/factories/traefik/utils/helm-values-mapper.js.map +1 -0
- package/dist/factories/traefik/utils/index.d.ts +6 -0
- package/dist/factories/traefik/utils/index.d.ts.map +1 -0
- package/dist/factories/traefik/utils/index.js +6 -0
- package/dist/factories/traefik/utils/index.js.map +1 -0
- package/dist/factories/traefik/utils/middleware-validation.d.ts +42 -0
- package/dist/factories/traefik/utils/middleware-validation.d.ts.map +1 -0
- package/dist/factories/traefik/utils/middleware-validation.js +78 -0
- package/dist/factories/traefik/utils/middleware-validation.js.map +1 -0
- package/dist/factories/webapp/compositions/web-app-with-processing.d.ts.map +1 -1
- package/dist/factories/webapp/compositions/web-app-with-processing.js +10 -0
- package/dist/factories/webapp/compositions/web-app-with-processing.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/package.json +9 -1
|
@@ -0,0 +1,911 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural spec-dependence diagnostics.
|
|
3
|
+
*
|
|
4
|
+
* ## The rule
|
|
5
|
+
*
|
|
6
|
+
* A `ResourceGraphDefinition` is fixed at build time. `schema.spec.*` values
|
|
7
|
+
* are *runtime* references: KRO substitutes them per instance, long after the
|
|
8
|
+
* composition function has run. So a spec value may decide what a field
|
|
9
|
+
* **contains** (a value position), but it may never decide what the graph
|
|
10
|
+
* **looks like** — which resources exist, how many list entries there are, or
|
|
11
|
+
* what an object's keys are called (a structural position).
|
|
12
|
+
*
|
|
13
|
+
* When a composition puts a spec value in a structural position, the
|
|
14
|
+
* composition function still runs: the schema proxy hands the author a
|
|
15
|
+
* `KubernetesRef` and JavaScript does something plausible-but-wrong with it.
|
|
16
|
+
* An `if` takes the truthy branch because a proxy object is truthy. A `.map()`
|
|
17
|
+
* runs exactly once against a sentinel element. `Object.keys()` yields one
|
|
18
|
+
* fabricated key. The RGD that comes out is well-formed and silently encodes
|
|
19
|
+
* the build-time guess for every instance.
|
|
20
|
+
*
|
|
21
|
+
* This module turns that silence into a KRO-mode serialization error.
|
|
22
|
+
*
|
|
23
|
+
* ## What is detected
|
|
24
|
+
*
|
|
25
|
+
* Two independent detectors, deliberately narrow so that legitimate
|
|
26
|
+
* value-position uses (`replicas: spec.replicas`, `` url: `http://${spec.host}` ``,
|
|
27
|
+
* CEL ternaries in status) stay silent:
|
|
28
|
+
*
|
|
29
|
+
* 1. {@link scanRgdResourcesForStructuralSpecArtifacts} — reads the RGD that is
|
|
30
|
+
* about to be emitted and reports build-time residue that can only come from
|
|
31
|
+
* a structural use. This is evidence-based: every finding is a concrete
|
|
32
|
+
* artifact in the output, so there are no false positives by construction.
|
|
33
|
+
* - `spec-derived-key`: an object key (or resource id) that is a schema
|
|
34
|
+
* reference marker — `{ [spec.key]: v }`, or `Object.fromEntries(...)`
|
|
35
|
+
* over a spec collection.
|
|
36
|
+
* - `runtime-map-enumerated`: the `__typekroSchemaKey` sentinel, which the
|
|
37
|
+
* schema proxy hands out when a composition enumerates a
|
|
38
|
+
* `Record<string, V>` spec field (`Object.keys(spec.settings)`,
|
|
39
|
+
* `{ ...spec.settings }`) whose keys are not knowable until runtime.
|
|
40
|
+
* - `collection-length-collapsed`: a `$item` element sentinel surviving in
|
|
41
|
+
* a resource that carries no `forEach` dimension — a `.map()` / `for-of`
|
|
42
|
+
* over a spec collection that was flattened to a single element instead
|
|
43
|
+
* of being compiled to KRO's `forEach`. A backstop: the serializer
|
|
44
|
+
* normally rewrites `$item` either into a `forEach` loop variable or
|
|
45
|
+
* into a whole-array reference (`ports: ${schema.spec.ports}`), both of
|
|
46
|
+
* which are correct, so this catches only what those miss.
|
|
47
|
+
*
|
|
48
|
+
* 2. {@link detectStructuralSpecPredicates} — parses the composition source and
|
|
49
|
+
* reports branch predicates that are spec-derived in a form the control-flow
|
|
50
|
+
* analyzer cannot compile to `includeWhen`, so the branch was decided at
|
|
51
|
+
* build time:
|
|
52
|
+
* - `uncompiled-predicate`: the `if` / ternary test reaches the spec only
|
|
53
|
+
* through a local binding (`const enabled = spec.enabled; if (enabled)`,
|
|
54
|
+
* `const { enabled } = spec;`), so the analyzer's lexical `spec` check
|
|
55
|
+
* misses it and no `includeWhen` is attached.
|
|
56
|
+
* - `switch-discriminator`: a `switch` on a spec-derived discriminant.
|
|
57
|
+
* There is no `switch` lowering at all; the proxy matches no `case`, so
|
|
58
|
+
* `default` wins for every instance.
|
|
59
|
+
* - `collection-length-read`: `.length` on a spec-derived collection or
|
|
60
|
+
* string. The proxy always answers `1`, so the value is a build-time
|
|
61
|
+
* constant no matter what the instance holds.
|
|
62
|
+
*
|
|
63
|
+
* A branch only counts when it BUILDS something. That question is settled
|
|
64
|
+
* by an allowlist, not by the callee's name: nothing in the source says
|
|
65
|
+
* whether an imported binding is a factory, and casing least of all — most
|
|
66
|
+
* of this repo's factories are camelCase (`helmRelease`, `serviceAccount`,
|
|
67
|
+
* `persistentVolumeClaim`, `observedResource`, `singleton`). So any call in
|
|
68
|
+
* the branch counts as building structure unless it is a pure JavaScript
|
|
69
|
+
* builtin (`String(…)`, `JSON.stringify(…)`, `Math.max(…)`, `items.map(…)`)
|
|
70
|
+
* or a `Cel.*` runtime expression. See {@link isPureHelperCall}.
|
|
71
|
+
*
|
|
72
|
+
* ## What is NOT detected
|
|
73
|
+
*
|
|
74
|
+
* Static analysis of arbitrary JavaScript cannot be complete, and this check
|
|
75
|
+
* does not pretend otherwise. Known blind spots:
|
|
76
|
+
*
|
|
77
|
+
* - A predicate laundered through a **helper function** or a module-level
|
|
78
|
+
* constant: `if (isEnabled(spec))`. The alias resolver only walks
|
|
79
|
+
* top-level `const`/`let` bindings in the composition body.
|
|
80
|
+
* - A predicate on a value read back **out of a resource** rather than off
|
|
81
|
+
* the spec proxy.
|
|
82
|
+
* - Structure decided by a spec value that is **consumed by a factory** and
|
|
83
|
+
* collapsed inside it, if the collapse leaves no marker, sentinel or
|
|
84
|
+
* `$item` residue in the emitted RGD.
|
|
85
|
+
* - A branch whose only call is a **method named like a builtin**
|
|
86
|
+
* (`registry.push(…)`, `catalog.map(…)`): the pure-helper allowlist matches
|
|
87
|
+
* on the method name without resolving the receiver, so such a call is
|
|
88
|
+
* assumed pure. No factory in `src/factories/**` carries one of those
|
|
89
|
+
* names, and a new one must not.
|
|
90
|
+
* - Anything reached through `eval`, dynamic imports, or a composition whose
|
|
91
|
+
* source cannot be parsed (the AST detector degrades to silence, never to
|
|
92
|
+
* a false positive).
|
|
93
|
+
*
|
|
94
|
+
* Detector 1 is the backstop for most of these: however the structure was
|
|
95
|
+
* decided, if the decision left residue in the RGD it is reported.
|
|
96
|
+
*/
|
|
97
|
+
import { Parser } from 'acorn';
|
|
98
|
+
import * as estraverse from 'estraverse';
|
|
99
|
+
import { TypeKroError } from '../errors.js';
|
|
100
|
+
import { getComponentLogger } from '../logging/index.js';
|
|
101
|
+
const logger = getComponentLogger('structural-spec-dependence');
|
|
102
|
+
/**
|
|
103
|
+
* Sentinel key handed out by the schema proxy's `ownKeys` trap for
|
|
104
|
+
* `Record<string, V>` spec fields, whose real keys only exist at runtime.
|
|
105
|
+
* Must stay in sync with `src/core/references/schema-proxy.ts`.
|
|
106
|
+
*/
|
|
107
|
+
const RUNTIME_MAP_SENTINEL = '__typekroSchemaKey';
|
|
108
|
+
/** Prefix of the schema-reference marker string emitted by the schema proxy. */
|
|
109
|
+
const SCHEMA_REF_MARKER_PREFIX = '__KUBERNETES_REF___schema___';
|
|
110
|
+
/**
|
|
111
|
+
* A schema reference ending in the proxy's `$item` element sentinel, which it
|
|
112
|
+
* appends when a spec collection is iterated. Anchored to a schema path rather
|
|
113
|
+
* than matching a bare `$item`, so a shell script or template in a ConfigMap
|
|
114
|
+
* that happens to contain `$item` is not mistaken for one.
|
|
115
|
+
*
|
|
116
|
+
* Survives into the RGD only when the iteration was NOT compiled into a KRO
|
|
117
|
+
* `forEach` dimension.
|
|
118
|
+
*/
|
|
119
|
+
const COLLECTION_ITEM_REFERENCE = /(?:schema\.spec|__KUBERNETES_REF___schema___spec)[A-Za-z0-9_.$[\]?]*\.\$item/;
|
|
120
|
+
/** Thrown when a KRO-mode RGD would silently encode a build-time guess. */
|
|
121
|
+
export class StructuralSpecDependenceError extends TypeKroError {
|
|
122
|
+
graphName;
|
|
123
|
+
findings;
|
|
124
|
+
constructor(message, graphName, findings) {
|
|
125
|
+
super(message, 'STRUCTURAL_SPEC_DEPENDENCE', { graphName, findings });
|
|
126
|
+
this.graphName = graphName;
|
|
127
|
+
this.findings = findings;
|
|
128
|
+
this.name = 'StructuralSpecDependenceError';
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
// Detector 1 — emitted-RGD scan
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
function schemaPathFromMarker(text) {
|
|
135
|
+
const match = new RegExp(`${SCHEMA_REF_MARKER_PREFIX}([A-Za-z0-9_.$[\\]?]+?)__`).exec(text);
|
|
136
|
+
return match?.[1] === undefined ? undefined : trimProxySentinelSuffix(match[1]);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Trim the proxy's own placeholder segments so the reported path names the spec
|
|
140
|
+
* field the author wrote, not the sentinel the proxy appended.
|
|
141
|
+
*/
|
|
142
|
+
function trimProxySentinelSuffix(path) {
|
|
143
|
+
return path
|
|
144
|
+
.replace(new RegExp(`(?:\\.${RUNTIME_MAP_SENTINEL})+$`), '')
|
|
145
|
+
.replace(/(?:\.\$item)+$/, '');
|
|
146
|
+
}
|
|
147
|
+
/** Collect every `schema.spec.<path>` / marker path mentioned anywhere in `text`. */
|
|
148
|
+
function schemaPathsIn(text) {
|
|
149
|
+
const paths = new Set();
|
|
150
|
+
const markerPattern = new RegExp(`${SCHEMA_REF_MARKER_PREFIX}([A-Za-z0-9_.$[\\]?]+?)__`, 'g');
|
|
151
|
+
for (const match of text.matchAll(markerPattern)) {
|
|
152
|
+
if (match[1])
|
|
153
|
+
paths.add(trimProxySentinelSuffix(match[1]));
|
|
154
|
+
}
|
|
155
|
+
for (const match of text.matchAll(/schema\.(spec\.[A-Za-z0-9_.$[\]?]+)/g)) {
|
|
156
|
+
if (match[1])
|
|
157
|
+
paths.add(trimProxySentinelSuffix(match[1]));
|
|
158
|
+
}
|
|
159
|
+
return [...paths].filter((path) => path !== '' && path !== 'spec');
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Walk an already-serialized template (plain JSON data — no proxies, no
|
|
163
|
+
* `KubernetesRef` objects) reporting every object key and every string leaf.
|
|
164
|
+
*/
|
|
165
|
+
function walkTemplate(value, path, visitor) {
|
|
166
|
+
if (typeof value === 'string') {
|
|
167
|
+
visitor.onString(path, value);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (Array.isArray(value)) {
|
|
171
|
+
value.forEach((entry, index) => walkTemplate(entry, `${path}[${index}]`, visitor));
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (value === null || typeof value !== 'object')
|
|
175
|
+
return;
|
|
176
|
+
for (const [key, child] of Object.entries(value)) {
|
|
177
|
+
const childPath = path ? `${path}.${key}` : key;
|
|
178
|
+
visitor.onKey(childPath, key, child);
|
|
179
|
+
walkTemplate(child, childPath, visitor);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Report build-time residue in the RGD that is about to be emitted.
|
|
184
|
+
*
|
|
185
|
+
* Operates on the serialized manifest rather than on live composition state,
|
|
186
|
+
* so every finding is backed by a concrete artifact in the output.
|
|
187
|
+
*/
|
|
188
|
+
export function scanRgdResourcesForStructuralSpecArtifacts(resources) {
|
|
189
|
+
const findings = [];
|
|
190
|
+
for (const entry of resources) {
|
|
191
|
+
const id = entry.id;
|
|
192
|
+
if (id.includes(SCHEMA_REF_MARKER_PREFIX) || id.includes(RUNTIME_MAP_SENTINEL)) {
|
|
193
|
+
findings.push({
|
|
194
|
+
kind: 'spec-derived-key',
|
|
195
|
+
location: `resource id "${id}"`,
|
|
196
|
+
specPaths: schemaPathsIn(id),
|
|
197
|
+
detail: 'the resource id itself was built from a runtime spec value, so the RGD names one ' +
|
|
198
|
+
'resource after a build-time placeholder instead of one resource per instance value',
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
// A `forEach` dimension IS the legitimate way to let a spec collection
|
|
202
|
+
// decide how many resources exist, and its own source expression lives
|
|
203
|
+
// outside the template. When one is present, a surviving `$item` in the
|
|
204
|
+
// template is the loop variable doing its job, not a collapsed collection.
|
|
205
|
+
const hasForEach = Array.isArray(entry.forEach) && entry.forEach.length > 0;
|
|
206
|
+
// Template paths already reported via their key, so the string walk that
|
|
207
|
+
// follows does not report the same leaf twice.
|
|
208
|
+
const seenAtPath = new Set();
|
|
209
|
+
walkTemplate(entry.template, '', {
|
|
210
|
+
onKey(path, key, child) {
|
|
211
|
+
if (key.includes(SCHEMA_REF_MARKER_PREFIX)) {
|
|
212
|
+
const specPath = schemaPathFromMarker(key);
|
|
213
|
+
findings.push({
|
|
214
|
+
kind: 'spec-derived-key',
|
|
215
|
+
location: `resource "${id}" at template.${path}`,
|
|
216
|
+
specPaths: specPath ? [specPath] : [],
|
|
217
|
+
detail: 'an object key was built from a runtime spec value; KRO substitutes references ' +
|
|
218
|
+
'in values only, so the RGD carries the unsubstituted reference as a literal key',
|
|
219
|
+
});
|
|
220
|
+
seenAtPath.add(path);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (key.includes(RUNTIME_MAP_SENTINEL)) {
|
|
224
|
+
findings.push({
|
|
225
|
+
kind: 'runtime-map-enumerated',
|
|
226
|
+
location: `resource "${id}" at template.${path}`,
|
|
227
|
+
// The sentinel key carries no path, but the value the proxy paired
|
|
228
|
+
// with it does (`${schema.spec.settings.__typekroSchemaKey}`).
|
|
229
|
+
specPaths: typeof child === 'string' ? schemaPathsIn(child) : [],
|
|
230
|
+
detail: 'the composition enumerated a map-typed spec field at build time; its real keys ' +
|
|
231
|
+
'exist only per instance, so the RGD encodes one placeholder key',
|
|
232
|
+
});
|
|
233
|
+
seenAtPath.add(path);
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
onString(path, value) {
|
|
237
|
+
if (seenAtPath.has(path))
|
|
238
|
+
return;
|
|
239
|
+
if (value.includes(RUNTIME_MAP_SENTINEL)) {
|
|
240
|
+
findings.push({
|
|
241
|
+
kind: 'runtime-map-enumerated',
|
|
242
|
+
location: `resource "${id}" at template.${path}`,
|
|
243
|
+
specPaths: schemaPathsIn(value),
|
|
244
|
+
detail: 'the composition enumerated a map-typed spec field at build time; its real keys ' +
|
|
245
|
+
'exist only per instance, so the RGD encodes one placeholder key',
|
|
246
|
+
});
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (!hasForEach && COLLECTION_ITEM_REFERENCE.test(value)) {
|
|
250
|
+
const specPaths = schemaPathsIn(value);
|
|
251
|
+
findings.push({
|
|
252
|
+
kind: 'collection-length-collapsed',
|
|
253
|
+
location: `resource "${id}" at template.${path}`,
|
|
254
|
+
specPaths,
|
|
255
|
+
detail: 'a spec collection was iterated at build time and flattened to a single element; ' +
|
|
256
|
+
'the RGD encodes exactly one entry regardless of how many the instance supplies',
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
return findings;
|
|
263
|
+
}
|
|
264
|
+
/** Read the composition's spec parameter name (`(spec) => ...` → `spec`). */
|
|
265
|
+
function specParamNameOf(fn) {
|
|
266
|
+
const first = fn.params?.[0];
|
|
267
|
+
if (!first)
|
|
268
|
+
return undefined;
|
|
269
|
+
if (first.type === 'Identifier')
|
|
270
|
+
return first.name;
|
|
271
|
+
return undefined;
|
|
272
|
+
}
|
|
273
|
+
/** The composition's own function node, whichever form the source parsed into. */
|
|
274
|
+
function findCompositionFunction(program) {
|
|
275
|
+
let found;
|
|
276
|
+
estraverse.traverse(program, {
|
|
277
|
+
enter(node) {
|
|
278
|
+
if (found)
|
|
279
|
+
return estraverse.VisitorOption.Break;
|
|
280
|
+
const type = node.type;
|
|
281
|
+
if (type === 'ArrowFunctionExpression' ||
|
|
282
|
+
type === 'FunctionExpression' ||
|
|
283
|
+
type === 'FunctionDeclaration') {
|
|
284
|
+
found = node;
|
|
285
|
+
return estraverse.VisitorOption.Break;
|
|
286
|
+
}
|
|
287
|
+
return undefined;
|
|
288
|
+
},
|
|
289
|
+
fallback: 'iteration',
|
|
290
|
+
});
|
|
291
|
+
return found;
|
|
292
|
+
}
|
|
293
|
+
/** Unwrap parenthesis-like and TypeScript-only wrapper nodes. */
|
|
294
|
+
function unwrap(node) {
|
|
295
|
+
let current = node;
|
|
296
|
+
while (current &&
|
|
297
|
+
(current.type === 'ChainExpression' ||
|
|
298
|
+
current.type === 'ParenthesizedExpression' ||
|
|
299
|
+
current.type === 'TSAsExpression' ||
|
|
300
|
+
current.type === 'TSNonNullExpression' ||
|
|
301
|
+
current.type === 'TSSatisfiesExpression' ||
|
|
302
|
+
current.type === 'AwaitExpression')) {
|
|
303
|
+
current = (current.expression ?? current.argument);
|
|
304
|
+
}
|
|
305
|
+
return current;
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* The `spec.<path>` a node names, when the node is a **pure member chain**
|
|
309
|
+
* rooted at the spec parameter or at a pure alias — nothing else.
|
|
310
|
+
*
|
|
311
|
+
* A computed segment (`spec.users[name]`) resolves to the object's path,
|
|
312
|
+
* because a runtime-keyed read still depends on the object it reads from.
|
|
313
|
+
*/
|
|
314
|
+
function pureSpecChainPath(node, specParam, aliases) {
|
|
315
|
+
const current = unwrap(node);
|
|
316
|
+
if (!current)
|
|
317
|
+
return undefined;
|
|
318
|
+
if (current.type === 'Identifier') {
|
|
319
|
+
const name = current.name;
|
|
320
|
+
if (name === specParam)
|
|
321
|
+
return 'spec';
|
|
322
|
+
return aliases.get(name);
|
|
323
|
+
}
|
|
324
|
+
if (current.type !== 'MemberExpression' && current.type !== 'OptionalMemberExpression') {
|
|
325
|
+
return undefined;
|
|
326
|
+
}
|
|
327
|
+
const objectPath = pureSpecChainPath(current.object, specParam, aliases);
|
|
328
|
+
if (!objectPath)
|
|
329
|
+
return undefined;
|
|
330
|
+
if (current.computed)
|
|
331
|
+
return objectPath;
|
|
332
|
+
const property = current.property?.name;
|
|
333
|
+
if (!property)
|
|
334
|
+
return undefined;
|
|
335
|
+
// `toResourceGraph` hands the whole schema proxy in, so the first hop off the
|
|
336
|
+
// parameter is `.spec` and must not be doubled up.
|
|
337
|
+
if (objectPath === 'spec' && property === 'spec')
|
|
338
|
+
return 'spec';
|
|
339
|
+
return `${objectPath}.${property}`;
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Spec paths an expression depends on: pure spec chains combined by operators
|
|
343
|
+
* that pass their operands through unchanged.
|
|
344
|
+
*
|
|
345
|
+
* The walk stops at calls and function bodies — a helper's return value and a
|
|
346
|
+
* callback's internals are opaque, and following them is what turns
|
|
347
|
+
* `resolved.users.map((u) => spec.users[u.name])` into a bogus claim that the
|
|
348
|
+
* resulting array is spec-derived.
|
|
349
|
+
*/
|
|
350
|
+
function specPathsOf(node, specParam, aliases) {
|
|
351
|
+
const paths = new Set();
|
|
352
|
+
const visit = (raw) => {
|
|
353
|
+
const current = unwrap(raw);
|
|
354
|
+
if (!current)
|
|
355
|
+
return;
|
|
356
|
+
const chain = pureSpecChainPath(current, specParam, aliases);
|
|
357
|
+
if (chain) {
|
|
358
|
+
paths.add(chain);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
switch (current.type) {
|
|
362
|
+
case 'UnaryExpression':
|
|
363
|
+
visit(current.argument);
|
|
364
|
+
return;
|
|
365
|
+
case 'BinaryExpression':
|
|
366
|
+
case 'LogicalExpression':
|
|
367
|
+
visit(current.left);
|
|
368
|
+
visit(current.right);
|
|
369
|
+
return;
|
|
370
|
+
case 'ConditionalExpression':
|
|
371
|
+
visit(current.test);
|
|
372
|
+
visit(current.consequent);
|
|
373
|
+
visit(current.alternate);
|
|
374
|
+
return;
|
|
375
|
+
case 'TemplateLiteral':
|
|
376
|
+
for (const expression of (current.expressions ?? []))
|
|
377
|
+
visit(expression);
|
|
378
|
+
return;
|
|
379
|
+
case 'SequenceExpression':
|
|
380
|
+
for (const expression of (current.expressions ?? []))
|
|
381
|
+
visit(expression);
|
|
382
|
+
return;
|
|
383
|
+
default:
|
|
384
|
+
// Calls, functions, object/array literals and everything else are
|
|
385
|
+
// opaque on purpose. See {@link SpecAliasScope}.
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
visit(node);
|
|
390
|
+
return [...paths].filter((path) => path !== 'spec').sort();
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Collect top-level bindings that alias a pure spec member chain.
|
|
394
|
+
*
|
|
395
|
+
* Two forms only: `const enabled = spec.enabled;` and
|
|
396
|
+
* `const { enabled, mode: storageMode } = spec;`.
|
|
397
|
+
*/
|
|
398
|
+
function buildSpecAliasScope(body, specParam) {
|
|
399
|
+
const aliases = new Map();
|
|
400
|
+
for (const statement of body) {
|
|
401
|
+
if (statement.type !== 'VariableDeclaration')
|
|
402
|
+
continue;
|
|
403
|
+
for (const declarator of (statement.declarations ?? [])) {
|
|
404
|
+
const init = declarator.init;
|
|
405
|
+
if (!init)
|
|
406
|
+
continue;
|
|
407
|
+
const initPath = pureSpecChainPath(init, specParam, aliases);
|
|
408
|
+
if (!initPath)
|
|
409
|
+
continue;
|
|
410
|
+
const id = declarator.id;
|
|
411
|
+
if (id.type === 'Identifier') {
|
|
412
|
+
aliases.set(id.name, initPath);
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
if (id.type === 'ObjectPattern') {
|
|
416
|
+
for (const property of (id.properties ?? [])) {
|
|
417
|
+
if (property.type !== 'Property')
|
|
418
|
+
continue;
|
|
419
|
+
const key = property.key?.name;
|
|
420
|
+
const local = property.value?.name;
|
|
421
|
+
if (!key || !local)
|
|
422
|
+
continue;
|
|
423
|
+
aliases.set(local, `${initPath}.${key}`);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return aliases;
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Global functions that only compute a value from their arguments.
|
|
432
|
+
*
|
|
433
|
+
* Casing says nothing about whether a call builds a resource — most of the
|
|
434
|
+
* repo's own factories are camelCase (`helmRelease`, `serviceAccount`,
|
|
435
|
+
* `persistentVolumeClaim`) — so {@link branchBuildsStructure} assumes a call
|
|
436
|
+
* builds structure and this allowlist is what buys silence back. Everything
|
|
437
|
+
* listed here is a pure JavaScript builtin: adding to it can only make the
|
|
438
|
+
* check quieter, so entries must be builtins, never project functions.
|
|
439
|
+
*/
|
|
440
|
+
const PURE_HELPER_GLOBALS = new Set([
|
|
441
|
+
'Boolean',
|
|
442
|
+
'BigInt',
|
|
443
|
+
'Number',
|
|
444
|
+
'String',
|
|
445
|
+
'Symbol',
|
|
446
|
+
'decodeURI',
|
|
447
|
+
'decodeURIComponent',
|
|
448
|
+
'encodeURI',
|
|
449
|
+
'encodeURIComponent',
|
|
450
|
+
'isFinite',
|
|
451
|
+
'isNaN',
|
|
452
|
+
'parseFloat',
|
|
453
|
+
'parseInt',
|
|
454
|
+
'structuredClone',
|
|
455
|
+
]);
|
|
456
|
+
/**
|
|
457
|
+
* Namespaces whose every member call computes a value: `JSON.stringify(x)`,
|
|
458
|
+
* `Math.max(a, b)`, `Object.keys(x)`, and TypeKro's own `Cel.expr(...)` /
|
|
459
|
+
* `Cel.template(...)`, which are runtime expressions rather than structure.
|
|
460
|
+
*/
|
|
461
|
+
const PURE_HELPER_NAMESPACES = new Set([
|
|
462
|
+
'Array',
|
|
463
|
+
'Boolean',
|
|
464
|
+
'Cel',
|
|
465
|
+
'Date',
|
|
466
|
+
'JSON',
|
|
467
|
+
'Math',
|
|
468
|
+
'Number',
|
|
469
|
+
'Object',
|
|
470
|
+
'Promise',
|
|
471
|
+
'Reflect',
|
|
472
|
+
'RegExp',
|
|
473
|
+
'String',
|
|
474
|
+
'Symbol',
|
|
475
|
+
]);
|
|
476
|
+
/**
|
|
477
|
+
* Builtin constructors that produce a plain value rather than graph structure.
|
|
478
|
+
* `if (flag) { index = new Map(…) }` is a value computation; nothing else in
|
|
479
|
+
* this repo is reached with `new`, and no factory is a class.
|
|
480
|
+
*/
|
|
481
|
+
const PURE_HELPER_CONSTRUCTORS = new Set([
|
|
482
|
+
'Array',
|
|
483
|
+
'Date',
|
|
484
|
+
'Error',
|
|
485
|
+
'Map',
|
|
486
|
+
'Number',
|
|
487
|
+
'Object',
|
|
488
|
+
'RegExp',
|
|
489
|
+
'Set',
|
|
490
|
+
'String',
|
|
491
|
+
'TypeError',
|
|
492
|
+
'URL',
|
|
493
|
+
'URLSearchParams',
|
|
494
|
+
'WeakMap',
|
|
495
|
+
'WeakSet',
|
|
496
|
+
]);
|
|
497
|
+
/**
|
|
498
|
+
* Builtin array/string/map methods. These reshape, read or accumulate into a
|
|
499
|
+
* value; none of them registers a resource, because in TypeKro a resource
|
|
500
|
+
* enters the graph when its FACTORY is called, not when the returned object is
|
|
501
|
+
* pushed somewhere. Allowlisting `.map` costs nothing either: a factory called
|
|
502
|
+
* inside the callback is still reached by the walk, which never stops at a
|
|
503
|
+
* pure call.
|
|
504
|
+
*
|
|
505
|
+
* The receiver is not checked, so a project function that happened to share
|
|
506
|
+
* one of these names would be exempted by mistake. None does — every one of
|
|
507
|
+
* these names was checked against the exports of `src/factories/**` — and a
|
|
508
|
+
* new factory must not take one.
|
|
509
|
+
*/
|
|
510
|
+
const PURE_HELPER_METHODS = new Set([
|
|
511
|
+
'at', 'charAt', 'concat', 'endsWith', 'entries', 'every', 'fill', 'filter',
|
|
512
|
+
'find', 'findIndex', 'findLast', 'findLastIndex', 'flat', 'flatMap',
|
|
513
|
+
'forEach', 'get', 'has', 'includes', 'indexOf', 'join', 'keys', 'lastIndexOf', 'map',
|
|
514
|
+
'match', 'normalize', 'padEnd', 'padStart', 'pop', 'push', 'reduce',
|
|
515
|
+
'reduceRight', 'repeat', 'replace', 'replaceAll', 'reverse', 'shift',
|
|
516
|
+
'slice', 'some', 'sort', 'split', 'splice', 'startsWith', 'substring',
|
|
517
|
+
'toFixed', 'toLowerCase', 'toString', 'toUpperCase', 'trim', 'trimEnd',
|
|
518
|
+
'trimStart', 'unshift', 'valueOf', 'values',
|
|
519
|
+
]);
|
|
520
|
+
/**
|
|
521
|
+
* True when a call is a known pure helper — the ONLY calls
|
|
522
|
+
* {@link branchBuildsStructure} lets through.
|
|
523
|
+
*
|
|
524
|
+
* The default runs the other way round on purpose. There is no reliable way to
|
|
525
|
+
* tell a factory call from any other call by looking at the source: the callee
|
|
526
|
+
* is an imported binding this module cannot resolve, and its name carries no
|
|
527
|
+
* signal (`helmRelease`, `observedResource`, `singleton` and
|
|
528
|
+
* `kubernetesComposition` all build structure; `ConfigMap` and
|
|
529
|
+
* `simple.Deployment` do too). So an unrecognised call — and any `new` that is
|
|
530
|
+
* not a builtin constructor — is treated as structure-building, and only the
|
|
531
|
+
* builtins above are exempt.
|
|
532
|
+
*/
|
|
533
|
+
function isPureHelperCall(node) {
|
|
534
|
+
const callee = unwrap(node.callee);
|
|
535
|
+
if (!callee)
|
|
536
|
+
return false;
|
|
537
|
+
if (node.type === 'NewExpression') {
|
|
538
|
+
return callee.type === 'Identifier' && PURE_HELPER_CONSTRUCTORS.has(callee.name);
|
|
539
|
+
}
|
|
540
|
+
if (callee.type === 'Identifier')
|
|
541
|
+
return PURE_HELPER_GLOBALS.has(callee.name);
|
|
542
|
+
if (callee.type !== 'MemberExpression' && callee.type !== 'OptionalMemberExpression') {
|
|
543
|
+
return false;
|
|
544
|
+
}
|
|
545
|
+
// `fns[i]()` names nothing this side of runtime.
|
|
546
|
+
if (callee.computed)
|
|
547
|
+
return false;
|
|
548
|
+
const method = callee.property?.name;
|
|
549
|
+
if (!method)
|
|
550
|
+
return false;
|
|
551
|
+
const namespace = (unwrap(callee.object) ?? {});
|
|
552
|
+
if (namespace.type === 'Identifier' &&
|
|
553
|
+
PURE_HELPER_NAMESPACES.has(namespace.name)) {
|
|
554
|
+
return true;
|
|
555
|
+
}
|
|
556
|
+
return PURE_HELPER_METHODS.has(method);
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* True when the subtree contains a call that may build graph structure — a
|
|
560
|
+
* factory, a nested composition, or any helper that wraps one.
|
|
561
|
+
*
|
|
562
|
+
* The walk does not stop at a pure helper, so `spec.items.map(() => Deployment(…))`
|
|
563
|
+
* still counts: `.map` is exempt, the `Deployment(…)` inside it is not.
|
|
564
|
+
*/
|
|
565
|
+
function branchBuildsStructure(node) {
|
|
566
|
+
if (!node)
|
|
567
|
+
return false;
|
|
568
|
+
let found = false;
|
|
569
|
+
estraverse.traverse(node, {
|
|
570
|
+
enter(raw) {
|
|
571
|
+
if (found)
|
|
572
|
+
return estraverse.VisitorOption.Break;
|
|
573
|
+
const current = raw;
|
|
574
|
+
if (current.type !== 'CallExpression' && current.type !== 'NewExpression')
|
|
575
|
+
return undefined;
|
|
576
|
+
if (isPureHelperCall(current))
|
|
577
|
+
return undefined;
|
|
578
|
+
found = true;
|
|
579
|
+
return estraverse.VisitorOption.Break;
|
|
580
|
+
},
|
|
581
|
+
fallback: 'iteration',
|
|
582
|
+
});
|
|
583
|
+
return found;
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* The enumerated operand when `node` reads an object's keys — `Object.keys(x)`,
|
|
587
|
+
* `Object.values(x)`, `Object.entries(x)`, or a spread `{ ...x }` / `[ ...x ]`.
|
|
588
|
+
*/
|
|
589
|
+
function mapEnumerationTarget(node) {
|
|
590
|
+
if (node.type === 'SpreadElement' || node.type === 'RestElement') {
|
|
591
|
+
return node.argument;
|
|
592
|
+
}
|
|
593
|
+
if (node.type !== 'CallExpression')
|
|
594
|
+
return undefined;
|
|
595
|
+
const callee = node.callee;
|
|
596
|
+
if (callee?.type !== 'MemberExpression')
|
|
597
|
+
return undefined;
|
|
598
|
+
const object = callee.object?.name;
|
|
599
|
+
const method = callee.property?.name;
|
|
600
|
+
if (object !== 'Object')
|
|
601
|
+
return undefined;
|
|
602
|
+
if (method !== 'keys' && method !== 'values' && method !== 'entries')
|
|
603
|
+
return undefined;
|
|
604
|
+
return node.arguments?.[0];
|
|
605
|
+
}
|
|
606
|
+
function excerpt(source, node, limit = 80) {
|
|
607
|
+
const start = node.start;
|
|
608
|
+
const end = node.end;
|
|
609
|
+
if (typeof start !== 'number' || typeof end !== 'number')
|
|
610
|
+
return '<expression>';
|
|
611
|
+
const text = source.slice(start, end).replace(/\s+/g, ' ').trim();
|
|
612
|
+
return text.length > limit ? `${text.slice(0, limit - 1)}…` : text;
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Report spec-derived structure decisions visible in the composition source
|
|
616
|
+
* that the control-flow analyzer does not compile into KRO constructs.
|
|
617
|
+
*
|
|
618
|
+
* Degrades to an empty result — never to a guess — when the source cannot be
|
|
619
|
+
* parsed or the spec parameter is not a plain identifier.
|
|
620
|
+
*/
|
|
621
|
+
function analyzeCompositionSource(compositionSource) {
|
|
622
|
+
if (!compositionSource)
|
|
623
|
+
return { findings: [], mapEnumerationSites: [] };
|
|
624
|
+
let program;
|
|
625
|
+
try {
|
|
626
|
+
program = Parser.parse(compositionSource, {
|
|
627
|
+
ecmaVersion: 2022,
|
|
628
|
+
sourceType: 'script',
|
|
629
|
+
ranges: true,
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
catch {
|
|
633
|
+
try {
|
|
634
|
+
program = Parser.parse(`(${compositionSource})`, {
|
|
635
|
+
ecmaVersion: 2022,
|
|
636
|
+
sourceType: 'script',
|
|
637
|
+
ranges: true,
|
|
638
|
+
});
|
|
639
|
+
// Offsets shift by the added paren; excerpts stay readable either way.
|
|
640
|
+
}
|
|
641
|
+
catch (error) {
|
|
642
|
+
logger.debug('Composition source could not be parsed for structural analysis', {
|
|
643
|
+
error: error instanceof Error ? error.message : String(error),
|
|
644
|
+
});
|
|
645
|
+
return { findings: [], mapEnumerationSites: [] };
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
const fn = findCompositionFunction(program);
|
|
649
|
+
const specParam = fn ? specParamNameOf(fn) : undefined;
|
|
650
|
+
if (!fn || !specParam)
|
|
651
|
+
return { findings: [], mapEnumerationSites: [] };
|
|
652
|
+
const bodyNode = fn.body;
|
|
653
|
+
const body = bodyNode?.type === 'BlockStatement' ? bodyNode.body : [];
|
|
654
|
+
const aliases = buildSpecAliasScope(body, specParam);
|
|
655
|
+
const findings = [];
|
|
656
|
+
const mapEnumerationSites = [];
|
|
657
|
+
const seen = new Set();
|
|
658
|
+
const push = (finding) => {
|
|
659
|
+
const key = `${finding.kind}|${finding.location}|${finding.specPaths.join(',')}`;
|
|
660
|
+
if (seen.has(key))
|
|
661
|
+
return;
|
|
662
|
+
seen.add(key);
|
|
663
|
+
findings.push(finding);
|
|
664
|
+
};
|
|
665
|
+
/**
|
|
666
|
+
* A predicate is *compiled* when the analyzer's own gate would fire: the test
|
|
667
|
+
* lexically mentions the spec parameter. Those become `includeWhen` and stay
|
|
668
|
+
* silent here. Anything spec-derived that the gate misses decided the branch
|
|
669
|
+
* at build time.
|
|
670
|
+
*/
|
|
671
|
+
const mentionsSpecLexically = (node) => {
|
|
672
|
+
let found = false;
|
|
673
|
+
estraverse.traverse(node, {
|
|
674
|
+
enter(raw) {
|
|
675
|
+
if (found)
|
|
676
|
+
return estraverse.VisitorOption.Break;
|
|
677
|
+
const current = raw;
|
|
678
|
+
if (current.type === 'Identifier' && current.name === specParam) {
|
|
679
|
+
found = true;
|
|
680
|
+
return estraverse.VisitorOption.Break;
|
|
681
|
+
}
|
|
682
|
+
return undefined;
|
|
683
|
+
},
|
|
684
|
+
fallback: 'iteration',
|
|
685
|
+
});
|
|
686
|
+
return found;
|
|
687
|
+
};
|
|
688
|
+
/**
|
|
689
|
+
* `.length` inside a status expression is FINE: the JS→CEL analyzer rewrites
|
|
690
|
+
* `schema.spec.x.length` to `size(schema.spec.x)`, which KRO evaluates per
|
|
691
|
+
* instance. Only a `.length` that lands in a resource TEMPLATE is a
|
|
692
|
+
* build-time constant. Status expressions live in the composition's `return`
|
|
693
|
+
* statement and inside explicit `Cel.*(…)` arguments, so track that context
|
|
694
|
+
* and stay silent there.
|
|
695
|
+
*/
|
|
696
|
+
let celContextDepth = 0;
|
|
697
|
+
const opensCelContext = (node) => {
|
|
698
|
+
if (node.type === 'ReturnStatement')
|
|
699
|
+
return true;
|
|
700
|
+
if (node.type !== 'CallExpression')
|
|
701
|
+
return false;
|
|
702
|
+
const callee = node.callee;
|
|
703
|
+
return (callee?.type === 'MemberExpression' &&
|
|
704
|
+
callee.object?.name === 'Cel');
|
|
705
|
+
};
|
|
706
|
+
estraverse.traverse(fn, {
|
|
707
|
+
leave(raw) {
|
|
708
|
+
if (opensCelContext(raw))
|
|
709
|
+
celContextDepth -= 1;
|
|
710
|
+
},
|
|
711
|
+
enter(raw) {
|
|
712
|
+
const node = raw;
|
|
713
|
+
if (opensCelContext(node))
|
|
714
|
+
celContextDepth += 1;
|
|
715
|
+
if (node.type === 'IfStatement' || node.type === 'ConditionalExpression') {
|
|
716
|
+
const test = node.test;
|
|
717
|
+
const branchCarriesResources = branchBuildsStructure(node.consequent) ||
|
|
718
|
+
branchBuildsStructure(node.alternate);
|
|
719
|
+
if (!branchCarriesResources)
|
|
720
|
+
return undefined;
|
|
721
|
+
if (mentionsSpecLexically(test))
|
|
722
|
+
return undefined;
|
|
723
|
+
const specPaths = specPathsOf(test, specParam, aliases);
|
|
724
|
+
if (specPaths.length === 0)
|
|
725
|
+
return undefined;
|
|
726
|
+
push({
|
|
727
|
+
kind: 'uncompiled-predicate',
|
|
728
|
+
location: `condition \`${excerpt(compositionSource, test)}\``,
|
|
729
|
+
specPaths,
|
|
730
|
+
detail: 'the branch decides what the composition builds and reaches the spec only through a ' +
|
|
731
|
+
'local binding, so TypeKro could not compile it to a KRO includeWhen and the ' +
|
|
732
|
+
'build-time branch is baked into the RGD',
|
|
733
|
+
});
|
|
734
|
+
return undefined;
|
|
735
|
+
}
|
|
736
|
+
if (node.type === 'SwitchStatement') {
|
|
737
|
+
if (!branchBuildsStructure(node))
|
|
738
|
+
return undefined;
|
|
739
|
+
const specPaths = specPathsOf(node.discriminant, specParam, aliases);
|
|
740
|
+
if (specPaths.length === 0)
|
|
741
|
+
return undefined;
|
|
742
|
+
push({
|
|
743
|
+
kind: 'switch-discriminator',
|
|
744
|
+
location: `switch (${excerpt(compositionSource, node.discriminant, 60)})`,
|
|
745
|
+
specPaths,
|
|
746
|
+
detail: 'a switch on a spec value selects which resources exist; TypeKro has no lowering for ' +
|
|
747
|
+
'switch, so no case matches the runtime reference and the default branch is baked ' +
|
|
748
|
+
'into the RGD for every instance',
|
|
749
|
+
});
|
|
750
|
+
return undefined;
|
|
751
|
+
}
|
|
752
|
+
if ((node.type === 'MemberExpression' || node.type === 'OptionalMemberExpression') &&
|
|
753
|
+
!node.computed &&
|
|
754
|
+
node.property?.name === 'length') {
|
|
755
|
+
if (celContextDepth > 0)
|
|
756
|
+
return undefined;
|
|
757
|
+
// A pure chain only: `spec.items.length`, not a `.length` read on a
|
|
758
|
+
// build-time-shaped local that merely mentions the spec somewhere.
|
|
759
|
+
const chain = pureSpecChainPath(node.object, specParam, aliases);
|
|
760
|
+
if (!chain || chain === 'spec')
|
|
761
|
+
return undefined;
|
|
762
|
+
push({
|
|
763
|
+
kind: 'collection-length-read',
|
|
764
|
+
location: `\`${excerpt(compositionSource, node, 60)}\``,
|
|
765
|
+
specPaths: [chain],
|
|
766
|
+
detail: 'the schema proxy answers `.length` with the build-time placeholder 1, so the emitted ' +
|
|
767
|
+
'value is a constant rather than the instance’s actual length',
|
|
768
|
+
});
|
|
769
|
+
return undefined;
|
|
770
|
+
}
|
|
771
|
+
const enumerated = mapEnumerationTarget(node);
|
|
772
|
+
if (enumerated) {
|
|
773
|
+
const chain = pureSpecChainPath(enumerated, specParam, aliases);
|
|
774
|
+
if (chain && chain !== 'spec') {
|
|
775
|
+
const site = { location: excerpt(compositionSource, node, 60), specPath: chain };
|
|
776
|
+
if (!mapEnumerationSites.some((existing) => existing.specPath === site.specPath)) {
|
|
777
|
+
mapEnumerationSites.push(site);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
return undefined;
|
|
782
|
+
},
|
|
783
|
+
fallback: 'iteration',
|
|
784
|
+
});
|
|
785
|
+
return { findings, mapEnumerationSites };
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* Branch predicates and `.length` reads the control-flow analyzer does not
|
|
789
|
+
* compile. See {@link analyzeCompositionSource}.
|
|
790
|
+
*/
|
|
791
|
+
export function detectStructuralSpecPredicates(compositionSource) {
|
|
792
|
+
return analyzeCompositionSource(compositionSource).findings;
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* Sites where the composition reads the keys of a spec field at build time.
|
|
796
|
+
*
|
|
797
|
+
* These are CANDIDATES, never findings. Enumerating an *object-typed* spec
|
|
798
|
+
* field is legitimate — the schema proxy is shape-aware and yields the real
|
|
799
|
+
* declared names — and the AST cannot tell that apart from a map-typed field,
|
|
800
|
+
* whose keys exist only per instance. Only the emitted RGD can, by leaving the
|
|
801
|
+
* `__typekroSchemaKey` sentinel behind; but that sentinel carries no path when
|
|
802
|
+
* it was consumed as a plain string. Reporting these alongside such a finding
|
|
803
|
+
* lets the message point at the lines worth reading without claiming any one
|
|
804
|
+
* of them is the culprit.
|
|
805
|
+
*/
|
|
806
|
+
export function detectMapEnumerationSites(compositionSource) {
|
|
807
|
+
return analyzeCompositionSource(compositionSource).mapEnumerationSites;
|
|
808
|
+
}
|
|
809
|
+
// ---------------------------------------------------------------------------
|
|
810
|
+
// Reporting
|
|
811
|
+
// ---------------------------------------------------------------------------
|
|
812
|
+
/** Remedies, ordered most-specific-first for the kinds present. */
|
|
813
|
+
function remediesFor(kinds) {
|
|
814
|
+
const remedies = [];
|
|
815
|
+
if (kinds.has('uncompiled-predicate')) {
|
|
816
|
+
remedies.push('Inline the spec reference into the condition itself (`if (spec.enabled) { … }`) so TypeKro ' +
|
|
817
|
+
'compiles it to a KRO includeWhen.');
|
|
818
|
+
}
|
|
819
|
+
remedies.push('Make it a BUILD-TIME factory option: take the value as an argument to the factory that ' +
|
|
820
|
+
'builds the composition, so the structure is decided before the RGD is emitted, and ship ' +
|
|
821
|
+
'one RGD per shape.');
|
|
822
|
+
remedies.push('Keep the structure FIXED and pass the spec value as a plain field: every resource, list ' +
|
|
823
|
+
'entry and key always exists, and the runtime value only decides what a field contains.');
|
|
824
|
+
return remedies;
|
|
825
|
+
}
|
|
826
|
+
function formatFinding(finding, index) {
|
|
827
|
+
const paths = finding.specPaths.length > 0
|
|
828
|
+
? finding.specPaths.map((path) => `\`${path}\``).join(', ')
|
|
829
|
+
: '(unresolved spec path)';
|
|
830
|
+
return [
|
|
831
|
+
` ${index + 1}. [${finding.kind}] ${finding.location}`,
|
|
832
|
+
` spec value: ${paths}`,
|
|
833
|
+
` ${finding.detail}`,
|
|
834
|
+
].join('\n');
|
|
835
|
+
}
|
|
836
|
+
/** Render the diagnostic text shared by the error and the escape-hatch warning. */
|
|
837
|
+
export function formatStructuralSpecDependence(graphName, findings, mapEnumerationSites = []) {
|
|
838
|
+
const kinds = new Set(findings.map((finding) => finding.kind));
|
|
839
|
+
const remedies = remediesFor(kinds)
|
|
840
|
+
.map((remedy, index) => ` ${String.fromCharCode(97 + index)}) ${remedy}`)
|
|
841
|
+
.join('\n');
|
|
842
|
+
const candidates = mapEnumerationSites.length > 0
|
|
843
|
+
? [
|
|
844
|
+
'',
|
|
845
|
+
'A placeholder key above carries no spec path, because it was consumed as a plain',
|
|
846
|
+
'string. The composition reads spec keys at build time in these places — one of them',
|
|
847
|
+
'reads a map-typed field whose keys only exist per instance:',
|
|
848
|
+
mapEnumerationSites
|
|
849
|
+
.map((site) => ` - \`${site.location}\` (\`${site.specPath}\`)`)
|
|
850
|
+
.join('\n'),
|
|
851
|
+
]
|
|
852
|
+
: [];
|
|
853
|
+
return [
|
|
854
|
+
`Resource graph "${graphName}" lets a runtime spec value decide build-time STRUCTURE. ` +
|
|
855
|
+
'TypeKro can only serialize a ResourceGraphDefinition whose shape is fixed: `schema.spec.*` ' +
|
|
856
|
+
'is substituted per instance, so it may decide what a field CONTAINS but never which ' +
|
|
857
|
+
'resources, list entries or keys EXIST.',
|
|
858
|
+
'',
|
|
859
|
+
`${findings.length} structural use${findings.length === 1 ? '' : 's'} of a spec value:`,
|
|
860
|
+
findings.map(formatFinding).join('\n'),
|
|
861
|
+
...candidates,
|
|
862
|
+
'',
|
|
863
|
+
'Two shapes are legitimate:',
|
|
864
|
+
remedies,
|
|
865
|
+
'',
|
|
866
|
+
'To keep the current behavior while migrating, pass ' +
|
|
867
|
+
'`{ allowStructuralSpecDependence: true }` in the composition options — the RGD is emitted ' +
|
|
868
|
+
'unchanged and this becomes a warning.',
|
|
869
|
+
].join('\n');
|
|
870
|
+
}
|
|
871
|
+
/**
|
|
872
|
+
* Whether the structural spec-dependence check ignores per-composition escape
|
|
873
|
+
* hatches.
|
|
874
|
+
*
|
|
875
|
+
* `TYPEKRO_STRUCTURAL_SPEC=strict` makes every structural use an error even
|
|
876
|
+
* when the composition sets `allowStructuralSpecDependence`, so a repository
|
|
877
|
+
* can audit what its shipped graphs still depend on without editing them.
|
|
878
|
+
* Resolved in ONE place, matching `isStrictCelDiagnosticsEnabled`.
|
|
879
|
+
*/
|
|
880
|
+
export function isStructuralSpecDependenceStrict() {
|
|
881
|
+
return process.env.TYPEKRO_STRUCTURAL_SPEC === 'strict';
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* Gate KRO-mode serialization on structural spec-dependence.
|
|
885
|
+
*
|
|
886
|
+
* Throws {@link StructuralSpecDependenceError} unless `allow` is set, in which
|
|
887
|
+
* case the same report is logged as a warning naming every path.
|
|
888
|
+
*/
|
|
889
|
+
export function assertNoStructuralSpecDependence(input) {
|
|
890
|
+
const source = analyzeCompositionSource(input.compositionSource);
|
|
891
|
+
const findings = [
|
|
892
|
+
...scanRgdResourcesForStructuralSpecArtifacts(input.resources),
|
|
893
|
+
...source.findings,
|
|
894
|
+
];
|
|
895
|
+
if (findings.length === 0)
|
|
896
|
+
return;
|
|
897
|
+
// Offer the enumeration candidates only when a sentinel finding could not
|
|
898
|
+
// name its own path — the AST cannot prove which site produced it.
|
|
899
|
+
const needsCandidates = findings.some((finding) => finding.kind === 'runtime-map-enumerated' && finding.specPaths.length === 0);
|
|
900
|
+
const message = formatStructuralSpecDependence(input.graphName, findings, needsCandidates ? source.mapEnumerationSites : []);
|
|
901
|
+
if (input.allow && !isStructuralSpecDependenceStrict()) {
|
|
902
|
+
logger.warn(message, {
|
|
903
|
+
graphName: input.graphName,
|
|
904
|
+
specPaths: [...new Set(findings.flatMap((finding) => finding.specPaths))],
|
|
905
|
+
kinds: [...new Set(findings.map((finding) => finding.kind))],
|
|
906
|
+
});
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
throw new StructuralSpecDependenceError(message, input.graphName, findings);
|
|
910
|
+
}
|
|
911
|
+
//# sourceMappingURL=structural-spec-dependence.js.map
|