clm-kernel 0.0.0__py3-none-any.whl
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.
- clm/__init__.py +230 -0
- clm/__main__.py +25 -0
- clm/cross_cutting/__init__.py +50 -0
- clm/cross_cutting/_test_runner.py +36 -0
- clm/cross_cutting/async_utils.py +25 -0
- clm/cross_cutting/cel_builder.py +123 -0
- clm/cross_cutting/cel_converter.py +697 -0
- clm/cross_cutting/cel_engine.py +252 -0
- clm/cross_cutting/cel_ops.py +72 -0
- clm/cross_cutting/cel_ops_db.py +157 -0
- clm/cross_cutting/cel_ops_files.py +99 -0
- clm/cross_cutting/cel_ops_state.py +71 -0
- clm/cross_cutting/cel_ops_strings.py +64 -0
- clm/cross_cutting/cel_resolver.py +63 -0
- clm/cross_cutting/cel_slices.py +56 -0
- clm/cross_cutting/cel_store.py +330 -0
- clm/cross_cutting/clm_logger.py +140 -0
- clm/cross_cutting/config/__init__.py +155 -0
- clm/cross_cutting/config/config_constants.py +392 -0
- clm/cross_cutting/config/env_parameters.py +90 -0
- clm/cross_cutting/config/logging.py +378 -0
- clm/cross_cutting/config/settings.py +504 -0
- clm/cross_cutting/context.py +5 -0
- clm/cross_cutting/domain_models.py +55 -0
- clm/cross_cutting/domain_types.py +61 -0
- clm/cross_cutting/effects.py +67 -0
- clm/cross_cutting/errors.py +153 -0
- clm/cross_cutting/governance.py +331 -0
- clm/cross_cutting/guards.py +41 -0
- clm/cross_cutting/mcard.py +321 -0
- clm/cross_cutting/native.py +185 -0
- clm/cross_cutting/observability.py +289 -0
- clm/cross_cutting/pcard.py +434 -0
- clm/cross_cutting/registry.py +71 -0
- clm/cross_cutting/resources.py +84 -0
- clm/cross_cutting/secrets.py +44 -0
- clm/cross_cutting/subprocess_utils.py +118 -0
- clm/cross_cutting/telemetry.py +164 -0
- clm/cross_cutting/testing/__init__.py +8 -0
- clm/cross_cutting/testing/comparator.py +187 -0
- clm/cross_cutting/testing/datasets.py +87 -0
- clm/cross_cutting/timeutil.py +20 -0
- clm/cross_cutting/types/new_type_check.py +0 -0
- clm/cross_cutting/utils/__init__.py +1 -0
- clm/cross_cutting/utils/url_safety.py +50 -0
- clm/cross_cutting/vcard.py +560 -0
- clm/cross_cutting/writer.py +118 -0
- clm/layer0/README.md +50 -0
- clm/layer0/__init__.py +223 -0
- clm/layer0/algebra.py +60 -0
- clm/layer0/algebra_baldwin.py +133 -0
- clm/layer0/algebra_baldwin_generators.py +36 -0
- clm/layer0/algebra_equivalence.py +262 -0
- clm/layer0/algebra_ingest.py +306 -0
- clm/layer0/algebra_manifest_io.py +156 -0
- clm/layer0/assembler.py +481 -0
- clm/layer0/cel/__init__.py +23 -0
- clm/layer0/cel_eval.py +491 -0
- clm/layer0/codec.py +106 -0
- clm/layer0/context.py +122 -0
- clm/layer0/db.py +160 -0
- clm/layer0/exceptions.py +73 -0
- clm/layer0/fibration.py +106 -0
- clm/layer0/gates.py +50 -0
- clm/layer0/hash.py +80 -0
- clm/layer0/loader.py +826 -0
- clm/layer0/math_utils.py +163 -0
- clm/layer0/mcard.py +96 -0
- clm/layer0/model/__init__.py +0 -0
- clm/layer0/model/action.py +223 -0
- clm/layer0/model/card.py +280 -0
- clm/layer0/model/card_triad.py +427 -0
- clm/layer0/model/compatibility.py +60 -0
- clm/layer0/model/dictionary.py +46 -0
- clm/layer0/model/dots.py +331 -0
- clm/layer0/model/event_producer.py +88 -0
- clm/layer0/model/g_time.py +89 -0
- clm/layer0/model/handle.py +163 -0
- clm/layer0/model/hash/__init__.py +6 -0
- clm/layer0/model/hash/algorithms/__init__.py +3 -0
- clm/layer0/model/hash/algorithms/custom_hash.py +7 -0
- clm/layer0/model/hash/algorithms/local_sha256.py +178 -0
- clm/layer0/model/hash/constants.py +53 -0
- clm/layer0/model/hash/enums.py +60 -0
- clm/layer0/model/hash/validator.py +307 -0
- clm/layer0/model/interpreter.py +275 -0
- clm/layer0/model/pagination.py +45 -0
- clm/layer0/model/pcard.py +716 -0
- clm/layer0/model/schema.py +44 -0
- clm/layer0/model/utils/__init__.py +1 -0
- clm/layer0/model/utils/content_analyzer.py +152 -0
- clm/layer0/model/validators/__init__.py +1 -0
- clm/layer0/model/validators/base_validator.py +44 -0
- clm/layer0/model/validators/binary_validator.py +66 -0
- clm/layer0/model/validators/text_validator.py +82 -0
- clm/layer0/model/validators/validation_registry.py +49 -0
- clm/layer0/model/vcard.py +823 -0
- clm/layer0/model/vcard_ext/__init__.py +39 -0
- clm/layer0/model/vcard_ext/core.py +60 -0
- clm/layer0/model/vcard_ext/network.py +23 -0
- clm/layer0/model/vcard_ext/observability.py +63 -0
- clm/layer0/model/vcard_ext/storage.py +60 -0
- clm/layer0/model/vcard_ext/vendors.py +211 -0
- clm/layer0/model/vcard_sandwich.py +119 -0
- clm/layer0/model/vcard_vocabulary.py +463 -0
- clm/layer0/model/workflow.py +274 -0
- clm/layer0/narrative.py +431 -0
- clm/layer0/ontology/collection.py +125 -0
- clm/layer0/parser.py +429 -0
- clm/layer0/relations.py +73 -0
- clm/layer0/rule_evaluator.py +251 -0
- clm/layer0/schema/__init__.py +38 -0
- clm/layer0/schema/ddl.py +97 -0
- clm/layer0/templates/abstract.yaml +99 -0
- clm/layer0/templates/balanced.yaml +253 -0
- clm/layer0/templates/concrete.yaml +172 -0
- clm/layer0/type_registry.py +223 -0
- clm/layer0/types.py +204 -0
- clm/layer0/utils.py +886 -0
- clm/layer0/verifier_core.py +262 -0
- clm/layer1/__init__.py +31 -0
- clm/layer1/action_dispatcher.py +275 -0
- clm/layer1/adapter_registry.py +233 -0
- clm/layer1/algebra_baldwin_generators.py +192 -0
- clm/layer1/arrow_compiler.py +525 -0
- clm/layer1/batch_processor.py +152 -0
- clm/layer1/cel/__init__.py +27 -0
- clm/layer1/cel/builder.py +173 -0
- clm/layer1/engine.py +759 -0
- clm/layer1/engine_async.py +89 -0
- clm/layer1/eval_callback.py +49 -0
- clm/layer1/gate_composer.py +229 -0
- clm/layer1/hoare_sandwich.py +298 -0
- clm/layer1/identity_guard.py +348 -0
- clm/layer1/matrix.py +138 -0
- clm/layer1/neural_runtime.py +221 -0
- clm/layer1/operations/__init__.py +123 -0
- clm/layer1/operations/augment.py +156 -0
- clm/layer1/operations/base.py +109 -0
- clm/layer1/operations/builtins.py +338 -0
- clm/layer1/operations/dispatch.py +144 -0
- clm/layer1/operations/exclude.py +107 -0
- clm/layer1/operations/handle.py +197 -0
- clm/layer1/operations/identity.py +139 -0
- clm/layer1/operations/invert.py +66 -0
- clm/layer1/operations/loader.py +116 -0
- clm/layer1/operations/manipulators/__init__.py +23 -0
- clm/layer1/operations/manipulators/document.py +211 -0
- clm/layer1/operations/manipulators/media.py +134 -0
- clm/layer1/operations/manipulators/structured.py +159 -0
- clm/layer1/operations/manipulators/tabular.py +334 -0
- clm/layer1/operations/port.py +65 -0
- clm/layer1/operations/services.py +529 -0
- clm/layer1/operations/split.py +117 -0
- clm/layer1/operations/substitute.py +75 -0
- clm/layer1/petri.py +1052 -0
- clm/layer1/pocketflow_orchestrator.py +408 -0
- clm/layer1/pocketflow_scheduler.py +519 -0
- clm/layer1/protocols.py +197 -0
- clm/layer1/runtime_adapter.py +718 -0
- clm/layer1/runtimes/__init__.py +71 -0
- clm/layer1/runtimes/_generated_assertions.py +125 -0
- clm/layer1/runtimes/_generated_types.py +42 -0
- clm/layer1/runtimes/base.py +265 -0
- clm/layer1/runtimes/binary.py +101 -0
- clm/layer1/runtimes/factory.py +203 -0
- clm/layer1/runtimes/javascript.py +321 -0
- clm/layer1/runtimes/javascript_runtime.js +33 -0
- clm/layer1/runtimes/js_scripts/isomorphism.mjs +22 -0
- clm/layer1/runtimes/js_scripts/loader.mjs +52 -0
- clm/layer1/runtimes/lambda_calc.py +126 -0
- clm/layer1/runtimes/python.py +288 -0
- clm/layer1/runtimes/python_runtime.py +41 -0
- clm/layer1/runtimes/script.py +114 -0
- clm/layer1/sandbox.py +334 -0
- clm/layer1/savepoint_guard.py +136 -0
- clm/layer1/sparse_net.py +122 -0
- clm/layer1/spec_tester.py +165 -0
- clm/layer1/store.py +295 -0
- clm/layer1/test_runner.py +498 -0
- clm/layer1/type_inspector.py +166 -0
- clm/layer1/verifier.py +772 -0
- clm/layer1/vm.py +91 -0
- clm/layer2/__init__.py +78 -0
- clm/layer2/card_collection.py +678 -0
- clm/layer2/classifier.py +479 -0
- clm/layer2/engine/__init__.py +1 -0
- clm/layer2/engine/abstract_sql_engine.py +320 -0
- clm/layer2/engine/base.py +193 -0
- clm/layer2/engine/duckdb_engine.py +576 -0
- clm/layer2/engine/sqlite_engine.py +518 -0
- clm/layer2/fiber/__init__.py +31 -0
- clm/layer2/fiber/context.py +89 -0
- clm/layer2/fiber/disposable.py +163 -0
- clm/layer2/fiber/fiber.py +125 -0
- clm/layer2/fiber/hoare.py +191 -0
- clm/layer2/file_io.py +376 -0
- clm/layer2/file_registrar.py +77 -0
- clm/layer2/ingest.py +202 -0
- clm/layer2/lineage.py +322 -0
- clm/layer2/mcard_fs.py +262 -0
- clm/layer2/merkle.py +11 -0
- clm/layer2/mime.py +244 -0
- clm/layer2/mime_detector.py +245 -0
- clm/layer2/private_collection.py +118 -0
- clm/layer2/shared_store.py +11 -0
- clm/layer2/storage/__init__.py +190 -0
- clm/layer2/storage/db_bitemporal.py +141 -0
- clm/layer2/storage/db_driver.py +331 -0
- clm/layer2/storage/db_hyperlinks.py +214 -0
- clm/layer2/storage/db_pool.py +139 -0
- clm/layer2/storage/db_repository.py +229 -0
- clm/layer2/storage/savepoint.py +78 -0
- clm/layer2/storage/sqlite_wal.py +74 -0
- clm/layer2/tridb.py +245 -0
- clm/layer2/vcard_receipt.py +71 -0
- clm/layer2/vfs.py +17 -0
- clm/layer3/__init__.py +141 -0
- clm/layer3/agency/__init__.py +19 -0
- clm/layer3/agency/config.py +220 -0
- clm/layer3/agency/providers/__init__.py +15 -0
- clm/layer3/agency/providers/base.py +91 -0
- clm/layer3/agency/providers/mlc_llm.py +169 -0
- clm/layer3/agency/providers/ollama.py +203 -0
- clm/layer3/agency/router.py +71 -0
- clm/layer3/agency/runtime.py +524 -0
- clm/layer3/gateway.py +353 -0
- clm/layer3/open_interpreter_adapter.py +159 -0
- clm/layer3/rag/__init__.py +70 -0
- clm/layer3/rag/cli.py +228 -0
- clm/layer3/rag/config.py +153 -0
- clm/layer3/rag/embeddings/__init__.py +16 -0
- clm/layer3/rag/embeddings/base.py +79 -0
- clm/layer3/rag/embeddings/ollama.py +211 -0
- clm/layer3/rag/embeddings/vision.py +290 -0
- clm/layer3/rag/engine.py +276 -0
- clm/layer3/rag/graph/__init__.py +22 -0
- clm/layer3/rag/graph/community.py +198 -0
- clm/layer3/rag/graph/engine.py +433 -0
- clm/layer3/rag/graph/extractor.py +255 -0
- clm/layer3/rag/graph/schema.py +115 -0
- clm/layer3/rag/graph/store.py +641 -0
- clm/layer3/rag/indexer.py +333 -0
- clm/layer3/rag/llm_providers.json +52 -0
- clm/layer3/rag/semantic_versioning.py +313 -0
- clm/layer3/rag/vector/__init__.py +28 -0
- clm/layer3/rag/vector/handle_vector_store.py +749 -0
- clm/layer3/rag/vector/schema.py +151 -0
- clm/layer3/rag/vector/store.py +632 -0
- clm/layer3/reticulum/__init__.py +55 -0
- clm/layer3/reticulum/bridge.py +74 -0
- clm/layer3/reticulum/daemon.py +88 -0
- clm/layer3/reticulum/discovery.py +164 -0
- clm/layer3/reticulum/identity.py +107 -0
- clm/layer3/reticulum/media.py +148 -0
- clm/layer3/reticulum/transport.py +133 -0
- clm/layer3/satori/__init__.py +209 -0
- clm/layer3/satori/alpha_conversion.py +168 -0
- clm/layer3/satori/beta_reduction.py +359 -0
- clm/layer3/satori/continuation.py +38 -0
- clm/layer3/satori/eta_conversion.py +205 -0
- clm/layer3/satori/free_variables.py +163 -0
- clm/layer3/satori/io_effects.py +370 -0
- clm/layer3/satori/lambda_runtime.py +726 -0
- clm/layer3/satori/lambda_term.py +230 -0
- clm/layer3/satori/sheaf_audit.py +261 -0
- clm/layer3/satori/speech_act.py +279 -0
- clm/layer4/README.md +9 -0
- clm/layer4/__init__.py +67 -0
- clm/layer4/_sovereign_executor.py +580 -0
- clm/layer4/action_commands.py +719 -0
- clm/layer4/api.py +236 -0
- clm/layer4/bootstrap/__init__.py +13 -0
- clm/layer4/bootstrap/bootstrapper.py +118 -0
- clm/layer4/bootstrap/cleanup.py +44 -0
- clm/layer4/bootstrap/clm_hooks.py +49 -0
- clm/layer4/bootstrap/genesis.py +77 -0
- clm/layer4/bootstrap/injection.py +125 -0
- clm/layer4/cli.py +549 -0
- clm/layer4/collection_manager.py +553 -0
- clm/layer4/commands/__init__.py +81 -0
- clm/layer4/commands/action_cmd.py +42 -0
- clm/layer4/commands/check_cmd.py +82 -0
- clm/layer4/commands/envelope.py +73 -0
- clm/layer4/commands/evaluate_cmd.py +224 -0
- clm/layer4/commands/mcard_cmd.py +24 -0
- clm/layer4/commands/petri_cmd.py +92 -0
- clm/layer4/commands/tridb_cmd.py +319 -0
- clm/layer4/commands/version_cmd.py +27 -0
- clm/layer4/diff_commands.py +167 -0
- clm/layer4/eoa_sponsor.py +232 -0
- clm/layer4/events.py +69 -0
- clm/layer4/flux_gateway.py +133 -0
- clm/layer4/gateway.py +91 -0
- clm/layer4/loader.py +307 -0
- clm/layer4/mcard_commands.py +166 -0
- clm/layer4/membrane.py +273 -0
- clm/layer4/navigation.py +50 -0
- clm/layer4/protocol/protocol_entry_python.py +108 -0
- clm/layer4/protocol/python_sync.py +126 -0
- clm/layer4/runner.py +150 -0
- clm/layer4/satori.py +262 -0
- clm/layer4/server/python_sync_client.py +150 -0
- clm/layer4/server/python_sync_server.py +139 -0
- clm/layer4/server/service_api.py +315 -0
- clm/layer4/server/start_servers.py +107 -0
- clm/layer4/server/stop_servers.py +24 -0
- clm/layer4/simulated_optimization_fix.py +0 -0
- clm/layer4/storage.py +463 -0
- clm/layer4/tridb_admin.py +268 -0
- clm/layer4/websocket/test_ws_client.py +104 -0
- clm/layer4/websocket/test_ws_server.py +138 -0
- clm/layer4/websocket/ws_server_local.py +160 -0
- clm/layer5/__init__.py +51 -0
- clm/layer5/async_batch_writer.py +133 -0
- clm/layer5/learning.py +276 -0
- clm/layer5/telemetry_hook.py +107 -0
- clm/layer5/type_lattice.py +238 -0
- clm/py.typed +0 -0
- clm/schemas/__init__.py +652 -0
- clm/schemas/abstract_face_schema.yaml +77 -0
- clm/schemas/cel_schema.yaml +416 -0
- clm/schemas/lifecycle/__init__.py +45 -0
- clm/schemas/lifecycle/models.py +123 -0
- clm/schemas/lifecycle/service.py +364 -0
- clm/schemas/mcard_schema.sql +192 -0
- clm/schemas/mcard_vector_schema.sql +283 -0
- clm/schemas/mlp-context-v1.json +91 -0
- clm_kernel-0.0.0.dist-info/METADATA +121 -0
- clm_kernel-0.0.0.dist-info/RECORD +333 -0
- clm_kernel-0.0.0.dist-info/WHEEL +4 -0
- clm_kernel-0.0.0.dist-info/entry_points.txt +3 -0
- clm_kernel-0.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,697 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""CLM CEL Converter — AST-based Python→CEL conversion and schema validation.
|
|
3
|
+
|
|
4
|
+
Promoted from the root-level ``convert_actual_to_cel_v2.py`` (Sprint 120).
|
|
5
|
+
This module is the canonical, importable home for the conversion pipeline.
|
|
6
|
+
|
|
7
|
+
Public API:
|
|
8
|
+
parse_expr_to_cel(expr_str) — convert a Python expression string to a CEL dict
|
|
9
|
+
convert_yaml_file(path, ...) — convert all ``actual`` fields in a YAML file
|
|
10
|
+
load_schema(path) — load cel_schema.yaml into a dict of op definitions
|
|
11
|
+
validate_cel(cel, ...) — validate a CEL dict against the schema
|
|
12
|
+
validate_yaml_file(path, ...) — validate all CEL ``actual`` fields in a YAML file
|
|
13
|
+
|
|
14
|
+
CLI:
|
|
15
|
+
python -m clm.tools.cel_converter <file.yaml> [--dry-run] [--backup] [--validate]
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import ast
|
|
19
|
+
import os
|
|
20
|
+
import sys
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
from clm.cross_cutting.clm_logger import get_logger
|
|
24
|
+
|
|
25
|
+
_logger = get_logger("clm.tools.cel_converter")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _ast_to_cel(node):
|
|
29
|
+
"""Convert an AST node to a CEL dict. Returns None if not convertible."""
|
|
30
|
+
if node is None:
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
# --- String literal ---
|
|
34
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, str):
|
|
35
|
+
return node.value
|
|
36
|
+
|
|
37
|
+
# --- Numeric literal ---
|
|
38
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)):
|
|
39
|
+
return node.value
|
|
40
|
+
|
|
41
|
+
# --- List literal ---
|
|
42
|
+
if isinstance(node, ast.List):
|
|
43
|
+
return [_ast_to_cel(elt) for elt in node.elts]
|
|
44
|
+
|
|
45
|
+
# --- Boolean literal ---
|
|
46
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, bool):
|
|
47
|
+
return node.value
|
|
48
|
+
|
|
49
|
+
# --- Tuple literal ---
|
|
50
|
+
if isinstance(node, ast.Tuple):
|
|
51
|
+
return tuple(_ast_to_cel(elt) for elt in node.elts)
|
|
52
|
+
|
|
53
|
+
# --- Dict literal ---
|
|
54
|
+
if isinstance(node, ast.Dict):
|
|
55
|
+
return {_ast_to_cel(k): _ast_to_cel(v) for k, v in zip(node.keys, node.values)}
|
|
56
|
+
|
|
57
|
+
# --- Name (identifier) ---
|
|
58
|
+
if isinstance(node, ast.Name):
|
|
59
|
+
name = node.id
|
|
60
|
+
if name == 'True':
|
|
61
|
+
return True
|
|
62
|
+
if name == 'False':
|
|
63
|
+
return False
|
|
64
|
+
if name == 'None':
|
|
65
|
+
return None
|
|
66
|
+
return {"op": "value_ref", "name": name}
|
|
67
|
+
|
|
68
|
+
# --- Subscript: inputs['key'], outputs['key'], retrieve('key')[N], call()[N]['key'] ---
|
|
69
|
+
if isinstance(node, ast.Subscript):
|
|
70
|
+
if isinstance(node.value, ast.Name) and node.value.id == 'inputs':
|
|
71
|
+
key = _ast_to_cel(node.slice)
|
|
72
|
+
if isinstance(key, str):
|
|
73
|
+
return {"ref": f"inputs.{key}"}
|
|
74
|
+
if isinstance(node.value, ast.Name) and node.value.id == 'outputs':
|
|
75
|
+
key = _ast_to_cel(node.slice)
|
|
76
|
+
if isinstance(key, str):
|
|
77
|
+
return {"ref": f"outputs.{key}"}
|
|
78
|
+
# General subscript: convert inner then apply index/dict_get
|
|
79
|
+
base = _ast_to_cel(node.value)
|
|
80
|
+
idx = _ast_to_cel(node.slice)
|
|
81
|
+
if isinstance(idx, int):
|
|
82
|
+
return {"op": "index", "value": base, "position": idx}
|
|
83
|
+
if isinstance(idx, str):
|
|
84
|
+
return {"op": "dict_get", "value": base, "key": idx}
|
|
85
|
+
|
|
86
|
+
# --- Attribute access: repo.method, json.loads, os.path.join ---
|
|
87
|
+
if isinstance(node, ast.Attribute):
|
|
88
|
+
if isinstance(node.value, ast.Name):
|
|
89
|
+
# os.path.join -> path_join
|
|
90
|
+
if node.value.id == 'os' and node.attr == 'path':
|
|
91
|
+
return {"_partial": "os.path"}
|
|
92
|
+
# json.loads -> json_parse
|
|
93
|
+
if node.value.id == 'json' and node.attr == 'loads':
|
|
94
|
+
return {"_partial": "json.loads"}
|
|
95
|
+
# time.sleep -> sleep
|
|
96
|
+
if node.value.id == 'time' and node.attr == 'sleep':
|
|
97
|
+
return {"_partial": "time.sleep"}
|
|
98
|
+
# os.environ -> env
|
|
99
|
+
if node.value.id == 'os' and node.attr == 'environ':
|
|
100
|
+
return {"_partial": "os.environ"}
|
|
101
|
+
# shutil.copy -> copy_file
|
|
102
|
+
if node.value.id == 'shutil' and node.attr == 'copy':
|
|
103
|
+
return {"_partial": "shutil.copy"}
|
|
104
|
+
return {"_partial": f"{node.value.id}.{node.attr}"}
|
|
105
|
+
if isinstance(node.value, ast.Attribute):
|
|
106
|
+
# os.path.join -> path_join
|
|
107
|
+
if node.value.value.id == 'os' and node.value.attr == 'path' and node.attr == 'join':
|
|
108
|
+
return {"_partial": "path_join"}
|
|
109
|
+
# os.path.basename -> path_basename (Sprint 126.1)
|
|
110
|
+
if node.value.value.id == 'os' and node.value.attr == 'path' and node.attr == 'basename':
|
|
111
|
+
return {"_partial": "path_basename"}
|
|
112
|
+
# os.environ.pop -> unset_env
|
|
113
|
+
if node.value.value.id == 'os' and node.value.attr == 'environ' and node.attr == 'pop':
|
|
114
|
+
return {"_partial": "unset_env"}
|
|
115
|
+
# os.environ.__setitem__ -> set_env
|
|
116
|
+
if node.value.value.id == 'os' and node.value.attr == 'environ' and node.attr == '__setitem__':
|
|
117
|
+
return {"_partial": "set_env"}
|
|
118
|
+
return {"_partial": f"{_attr_chain(node.value)}.{node.attr}"}
|
|
119
|
+
|
|
120
|
+
# --- Handle chained .get() calls on dicts ---
|
|
121
|
+
if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) and node.func.attr == 'get':
|
|
122
|
+
val = _ast_to_cel(node.func.value)
|
|
123
|
+
if val is not None:
|
|
124
|
+
key = _ast_to_cel(node.args[0])
|
|
125
|
+
default = _ast_to_cel(node.args[1]) if len(node.args) > 1 else None
|
|
126
|
+
cel = {"op": "dict_get", "value": val, "key": key}
|
|
127
|
+
if default is not None:
|
|
128
|
+
cel["default"] = default
|
|
129
|
+
return cel
|
|
130
|
+
|
|
131
|
+
# --- Handle chained .replace() calls ---
|
|
132
|
+
if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) and node.func.attr == 'replace':
|
|
133
|
+
val = _ast_to_cel(node.func.value)
|
|
134
|
+
if val is not None:
|
|
135
|
+
old = _ast_to_cel(node.args[0])
|
|
136
|
+
new = _ast_to_cel(node.args[1])
|
|
137
|
+
return {"op": "replace", "value": val, "old": old, "new": new}
|
|
138
|
+
|
|
139
|
+
# --- Handle str.join: "sep".join(parts) -> string_join (Sprint 126.1) ---
|
|
140
|
+
if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) and node.func.attr == 'join':
|
|
141
|
+
sep = _ast_to_cel(node.func.value)
|
|
142
|
+
if sep is not None and len(node.args) == 1:
|
|
143
|
+
parts = _ast_to_cel(node.args[0])
|
|
144
|
+
return {"op": "string_join", "separator": sep, "parts": parts}
|
|
145
|
+
|
|
146
|
+
# --- Handle `open(path).read()` ---
|
|
147
|
+
if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) and node.func.attr == 'read':
|
|
148
|
+
inner = node.func.value
|
|
149
|
+
if isinstance(inner, ast.Call) and isinstance(inner.func, ast.Name) and inner.func.id == 'open':
|
|
150
|
+
path = _ast_to_cel(inner.args[0])
|
|
151
|
+
return {"op": "read_file", "path": path}
|
|
152
|
+
|
|
153
|
+
# --- Handle db.execute(...) patterns (consolidated in Sprint 127.3) ---
|
|
154
|
+
cel = _convert_db_execute_pattern(node)
|
|
155
|
+
if cel is not None:
|
|
156
|
+
return cel
|
|
157
|
+
|
|
158
|
+
# --- Call ---
|
|
159
|
+
if isinstance(node, ast.Call):
|
|
160
|
+
return _ast_call_to_cel(node)
|
|
161
|
+
|
|
162
|
+
# --- BoolOp: and/or ---
|
|
163
|
+
if isinstance(node, ast.BoolOp):
|
|
164
|
+
return _ast_boolop_to_cel(node)
|
|
165
|
+
|
|
166
|
+
# --- Compare: ==, !=, >=, <=, >, <, in ---
|
|
167
|
+
if isinstance(node, ast.Compare):
|
|
168
|
+
return _ast_compare_to_cel(node)
|
|
169
|
+
|
|
170
|
+
# --- BinOp: +, -, *, /, //, % ---
|
|
171
|
+
if isinstance(node, ast.BinOp):
|
|
172
|
+
return _ast_binop_to_cel(node)
|
|
173
|
+
|
|
174
|
+
# --- UnaryOp: not, +, - ---
|
|
175
|
+
if isinstance(node, ast.UnaryOp):
|
|
176
|
+
if isinstance(node.op, ast.Not):
|
|
177
|
+
return {"op": "compare", "left": _ast_to_cel(node.operand), "operator": "eq", "right": False}
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _make_db_query_cel(execute_node: ast.Call) -> dict | None:
|
|
184
|
+
"""Build a db_query CEL dict from a db.execute(...) call node."""
|
|
185
|
+
db_obj = execute_node.func.value
|
|
186
|
+
if not (isinstance(db_obj, ast.Name) and db_obj.id == 'db'):
|
|
187
|
+
return None
|
|
188
|
+
args = [_ast_to_cel(a) for a in execute_node.args]
|
|
189
|
+
cel = {"op": "db_query", "pillar": args[0], "sql": args[1]}
|
|
190
|
+
if len(args) > 2:
|
|
191
|
+
cel["params"] = args[2]
|
|
192
|
+
return cel
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _convert_db_execute_pattern(node: ast.AST) -> dict | None:
|
|
196
|
+
"""Convert any db.execute(...) call pattern to a db_query CEL dict.
|
|
197
|
+
|
|
198
|
+
Handles three patterns that were previously 5 separate code blocks:
|
|
199
|
+
- ``db.execute(pillar, sql)`` -> ``db_query``
|
|
200
|
+
- ``db.execute(pillar, sql).strip()`` -> ``db_query``
|
|
201
|
+
- ``int(db.execute(pillar, sql).strip())`` -> ``int_cast(db_query)``
|
|
202
|
+
"""
|
|
203
|
+
# Pattern: int(db.execute(...).strip())
|
|
204
|
+
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id == 'int':
|
|
205
|
+
arg = node.args[0] if node.args else None
|
|
206
|
+
if isinstance(arg, ast.Call) and isinstance(arg.func, ast.Attribute) and arg.func.attr == 'strip':
|
|
207
|
+
inner = arg.func.value
|
|
208
|
+
if isinstance(inner, ast.Call) and isinstance(inner.func, ast.Attribute) and inner.func.attr == 'execute':
|
|
209
|
+
cel = _make_db_query_cel(inner)
|
|
210
|
+
if cel is not None:
|
|
211
|
+
return {"op": "int_cast", "value": cel}
|
|
212
|
+
return None
|
|
213
|
+
|
|
214
|
+
# Pattern: db.execute(...).strip()
|
|
215
|
+
if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) and node.func.attr == 'strip':
|
|
216
|
+
inner = node.func.value
|
|
217
|
+
if isinstance(inner, ast.Call) and isinstance(inner.func, ast.Attribute) and inner.func.attr == 'execute':
|
|
218
|
+
return _make_db_query_cel(inner)
|
|
219
|
+
return None
|
|
220
|
+
|
|
221
|
+
# Pattern: db.execute(...)
|
|
222
|
+
if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) and node.func.attr == 'execute':
|
|
223
|
+
return _make_db_query_cel(node)
|
|
224
|
+
|
|
225
|
+
return None
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _attr_chain(node):
|
|
229
|
+
"""Convert chained attribute access to dotted string."""
|
|
230
|
+
if isinstance(node, ast.Name):
|
|
231
|
+
return node.id
|
|
232
|
+
if isinstance(node, ast.Attribute):
|
|
233
|
+
return f"{_attr_chain(node.value)}.{node.attr}"
|
|
234
|
+
return ""
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _partial_db_execute(args: list) -> dict:
|
|
238
|
+
"""Convert db.execute partial call to db_query CEL dict."""
|
|
239
|
+
cel = {"op": "db_query", "pillar": args[0], "sql": args[1]}
|
|
240
|
+
if len(args) > 2:
|
|
241
|
+
cel["params"] = args[2]
|
|
242
|
+
return cel
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
# Dispatch table for partial (attribute-chain) function calls (Sprint 127.3)
|
|
246
|
+
_PARTIAL_CONVERTERS: dict[str, Any] = {
|
|
247
|
+
"json.loads": lambda args: {"op": "json_parse", "value": args[0]},
|
|
248
|
+
"time.sleep": lambda args: {"op": "sleep", "seconds": args[0]},
|
|
249
|
+
"path_join": lambda args: {"op": "path_join", "parts": args},
|
|
250
|
+
"path_basename": lambda args: {"op": "path_basename", "value": args[0]},
|
|
251
|
+
"set_env": lambda args: {"op": "set_env", "var": args[0], "value": args[1]},
|
|
252
|
+
"unset_env": lambda args: {"op": "unset_env", "var": args[0]},
|
|
253
|
+
"shutil.copy": lambda args: {"op": "copy_file", "src": args[0], "dest": args[1]},
|
|
254
|
+
"db.execute": _partial_db_execute,
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
# Dispatch table for simple Name function calls (Sprint 127.3)
|
|
258
|
+
_NAME_CALL_CONVERTERS: dict[str, Any] = {
|
|
259
|
+
"len": lambda args, kw: {"op": "len", "value": args[0]},
|
|
260
|
+
"int": lambda args, kw: {"op": "int_cast", "value": args[0]},
|
|
261
|
+
"callable": lambda args, kw: {"op": "callable_check", "value": args[0]},
|
|
262
|
+
"store": lambda args, kw: {"op": "store", "key": args[0], "value": args[1]},
|
|
263
|
+
"retrieve": lambda args, kw: {"op": "retrieve", "key": args[0]},
|
|
264
|
+
"write_file": lambda args, kw: {"op": "write_file", "path": args[0], "content": args[1]},
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _ast_call_to_cel(node):
|
|
269
|
+
"""Convert a function call AST node to CEL."""
|
|
270
|
+
func = _ast_to_cel(node.func)
|
|
271
|
+
if func is None:
|
|
272
|
+
return None
|
|
273
|
+
|
|
274
|
+
# Handle partials (method references like json.loads) via dispatch table
|
|
275
|
+
if isinstance(func, dict) and "_partial" in func:
|
|
276
|
+
partial = func["_partial"]
|
|
277
|
+
args = [_ast_to_cel(a) for a in node.args]
|
|
278
|
+
converter = _PARTIAL_CONVERTERS.get(partial)
|
|
279
|
+
if converter is not None:
|
|
280
|
+
return converter(args)
|
|
281
|
+
return {"op": "call", "function": partial, "args": args}
|
|
282
|
+
|
|
283
|
+
# Handle simple Name calls via dispatch table
|
|
284
|
+
if isinstance(func, dict) and func.get("op") == "value_ref":
|
|
285
|
+
func_name = func["name"]
|
|
286
|
+
args = [_ast_to_cel(a) for a in node.args]
|
|
287
|
+
kwargs = {}
|
|
288
|
+
for kw in node.keywords:
|
|
289
|
+
kwargs[kw.arg] = _ast_to_cel(kw.value)
|
|
290
|
+
|
|
291
|
+
converter = _NAME_CALL_CONVERTERS.get(func_name)
|
|
292
|
+
if converter is not None:
|
|
293
|
+
return converter(args, kwargs)
|
|
294
|
+
if func_name == 'db' and len(args) >= 2:
|
|
295
|
+
return {"op": "db_query", "pillar": args[0], "sql": args[1], "params": args[2] if len(args) > 2 else None}
|
|
296
|
+
|
|
297
|
+
return {"op": "call", "function": func_name, "args": args, "kwargs": kwargs}
|
|
298
|
+
|
|
299
|
+
# Handle Attribute calls: repo.method(...)
|
|
300
|
+
if isinstance(func, dict) and "_partial" in func:
|
|
301
|
+
func_name = func["_partial"]
|
|
302
|
+
args = [_ast_to_cel(a) for a in node.args]
|
|
303
|
+
kwargs = {}
|
|
304
|
+
for kw in node.keywords:
|
|
305
|
+
kwargs[kw.arg] = _ast_to_cel(kw.value)
|
|
306
|
+
return {"op": "call", "function": func_name, "args": args, "kwargs": kwargs}
|
|
307
|
+
|
|
308
|
+
return None
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _ast_boolop_to_cel(node):
|
|
312
|
+
"""Convert BoolOp (and/or) to CEL sequence or compare."""
|
|
313
|
+
|
|
314
|
+
if isinstance(node.op, ast.And):
|
|
315
|
+
values = [_ast_to_cel(v) for v in node.values]
|
|
316
|
+
# Filter out None values
|
|
317
|
+
steps = [v for v in values if v is not None]
|
|
318
|
+
if steps:
|
|
319
|
+
return {"op": "sequence", "steps": steps}
|
|
320
|
+
return None
|
|
321
|
+
|
|
322
|
+
if isinstance(node.op, ast.Or):
|
|
323
|
+
# Check for `a or True` pattern
|
|
324
|
+
last = node.values[-1]
|
|
325
|
+
if isinstance(last, ast.Constant) and last.value is True:
|
|
326
|
+
# Convert remaining values, handling os.environ.__setitem__ specially
|
|
327
|
+
steps = []
|
|
328
|
+
for v in node.values[:-1]:
|
|
329
|
+
cel = _ast_to_cel(v)
|
|
330
|
+
if cel is not None:
|
|
331
|
+
steps.append(cel)
|
|
332
|
+
if steps:
|
|
333
|
+
return {"op": "sequence", "steps": steps}
|
|
334
|
+
return None
|
|
335
|
+
# Check for `__setitem__(...) or __setitem__(...)` pattern
|
|
336
|
+
setitem_steps = []
|
|
337
|
+
other_values = []
|
|
338
|
+
for v in node.values:
|
|
339
|
+
if isinstance(v, ast.Call) and isinstance(v.func, ast.Attribute) and v.func.attr == '__setitem__':
|
|
340
|
+
env = v.func.value
|
|
341
|
+
if isinstance(env, ast.Attribute) and env.attr == 'environ' and isinstance(env.value, ast.Name) and env.value.id == 'os':
|
|
342
|
+
args = [_ast_to_cel(a) for a in v.args]
|
|
343
|
+
setitem_steps.append({"op": "set_env", "var": args[0], "value": args[1]})
|
|
344
|
+
else:
|
|
345
|
+
cel = _ast_to_cel(v)
|
|
346
|
+
if cel is not None:
|
|
347
|
+
setitem_steps.append(cel)
|
|
348
|
+
else:
|
|
349
|
+
other_values.append(v)
|
|
350
|
+
if setitem_steps:
|
|
351
|
+
return {"op": "sequence", "steps": setitem_steps}
|
|
352
|
+
# General `a or fallback` pattern -> or_fallback
|
|
353
|
+
if len(other_values) == 2:
|
|
354
|
+
left = _ast_to_cel(other_values[0])
|
|
355
|
+
right = _ast_to_cel(other_values[1])
|
|
356
|
+
if left is not None and right is not None:
|
|
357
|
+
return {"op": "or_fallback", "value": left, "fallback": right}
|
|
358
|
+
return None
|
|
359
|
+
|
|
360
|
+
return None
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def _ast_compare_to_cel(node):
|
|
364
|
+
"""Convert Compare to CEL compare op.
|
|
365
|
+
|
|
366
|
+
Two pattern-specific shortcuts produce semantically cleaner CEL than the
|
|
367
|
+
generic compare wrapper:
|
|
368
|
+
- `os.environ.pop('VAR', None) is not False` → `unset_env` (drop the tautological compare)
|
|
369
|
+
- `store('key', value) is not None` → `sequence(store, not_null(retrieve(key)))`
|
|
370
|
+
"""
|
|
371
|
+
# --- Pattern: os.environ.pop('VAR', None) is not False → unset_env ---
|
|
372
|
+
if len(node.ops) == 1 and isinstance(node.ops[0], ast.IsNot) and \
|
|
373
|
+
isinstance(node.comparators[0], ast.Constant) and node.comparators[0].value is False:
|
|
374
|
+
inner = node.left
|
|
375
|
+
if isinstance(inner, ast.Call) and isinstance(inner.func, ast.Attribute) and inner.func.attr == 'pop':
|
|
376
|
+
env = inner.func.value
|
|
377
|
+
if isinstance(env, ast.Attribute) and env.attr == 'environ' and \
|
|
378
|
+
isinstance(env.value, ast.Name) and env.value.id == 'os':
|
|
379
|
+
args = [_ast_to_cel(a) for a in inner.args]
|
|
380
|
+
return {"op": "unset_env", "var": args[0]}
|
|
381
|
+
|
|
382
|
+
# --- Pattern: store('key', value) is not None → sequence(store, not_null) ---
|
|
383
|
+
if len(node.ops) == 1 and isinstance(node.ops[0], ast.IsNot) and \
|
|
384
|
+
isinstance(node.comparators[0], ast.Constant) and node.comparators[0].value is None:
|
|
385
|
+
inner = _ast_to_cel(node.left)
|
|
386
|
+
if isinstance(inner, dict) and inner.get("op") == "store":
|
|
387
|
+
return {
|
|
388
|
+
"op": "sequence",
|
|
389
|
+
"steps": [
|
|
390
|
+
inner,
|
|
391
|
+
{"op": "not_null", "value": {"op": "retrieve", "key": inner["key"]}}
|
|
392
|
+
]
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
# --- Generic compare ---
|
|
396
|
+
left = _ast_to_cel(node.left)
|
|
397
|
+
if left is None:
|
|
398
|
+
return None
|
|
399
|
+
|
|
400
|
+
ops_map = {
|
|
401
|
+
ast.Eq: "eq", ast.NotEq: "ne",
|
|
402
|
+
ast.Gt: "gt", ast.GtE: "gte",
|
|
403
|
+
ast.Lt: "lt", ast.LtE: "lte",
|
|
404
|
+
ast.In: "in",
|
|
405
|
+
ast.IsNot: "ne", ast.Is: "eq"
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
comparators = [_ast_to_cel(c) for c in node.comparators]
|
|
409
|
+
if len(node.ops) == 1 and len(comparators) == 1:
|
|
410
|
+
op = node.ops[0]
|
|
411
|
+
if type(op) in ops_map:
|
|
412
|
+
return {"op": "compare", "left": left, "operator": ops_map[type(op)], "right": comparators[0]}
|
|
413
|
+
|
|
414
|
+
# For chained comparisons, we can't easily represent them in CEL
|
|
415
|
+
return None
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _ast_binop_to_cel(node):
|
|
419
|
+
"""Convert BinOp to CEL."""
|
|
420
|
+
left = _ast_to_cel(node.left)
|
|
421
|
+
right = _ast_to_cel(node.right)
|
|
422
|
+
if left is None or right is None:
|
|
423
|
+
return None
|
|
424
|
+
|
|
425
|
+
if isinstance(node.op, ast.Add):
|
|
426
|
+
return {"op": "string_concat", "parts": [left, right]}
|
|
427
|
+
if isinstance(node.op, ast.Sub):
|
|
428
|
+
return {"op": "call", "function": "operator.sub", "args": [left, right]}
|
|
429
|
+
if isinstance(node.op, ast.Mult):
|
|
430
|
+
return {"op": "call", "function": "operator.mul", "args": [left, right]}
|
|
431
|
+
if isinstance(node.op, ast.Div):
|
|
432
|
+
return {"op": "call", "function": "operator.truediv", "args": [left, right]}
|
|
433
|
+
if isinstance(node.op, ast.Mod):
|
|
434
|
+
return {"op": "call", "function": "operator.mod", "args": [left, right]}
|
|
435
|
+
|
|
436
|
+
return None
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def _strip_or_true(node):
|
|
440
|
+
"""If expression is `something or True`, return the something."""
|
|
441
|
+
if isinstance(node, ast.BoolOp) and isinstance(node.op, ast.Or) and len(node.values) == 2:
|
|
442
|
+
last = node.values[-1]
|
|
443
|
+
if isinstance(last, ast.Constant) and last.value is True:
|
|
444
|
+
return node.values[0]
|
|
445
|
+
return None
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def _strip_method_chain(node, method_names):
|
|
449
|
+
"""Strip a chain of method calls like .strip(), .read(), etc."""
|
|
450
|
+
calls = []
|
|
451
|
+
current = node
|
|
452
|
+
while isinstance(current, ast.Call) and isinstance(current.func, ast.Attribute):
|
|
453
|
+
calls.append((current.func.attr, [_ast_to_cel(a) for a in current.args]))
|
|
454
|
+
current = current.func.value
|
|
455
|
+
calls.reverse()
|
|
456
|
+
return current, calls
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def parse_expr_to_cel(expr_str: str):
|
|
460
|
+
"""Parse a Python expression string and convert to a CEL dict.
|
|
461
|
+
|
|
462
|
+
Thin wrapper around the generic AST→CEL dispatcher (`_ast_to_cel`).
|
|
463
|
+
Strips the common `X or True` idiom before dispatch. All node-type
|
|
464
|
+
dispatch (calls, comparisons, boolops, subscripts, etc.) is handled
|
|
465
|
+
by `_ast_to_cel` and its helpers — this function does NOT re-implement
|
|
466
|
+
per-pattern logic.
|
|
467
|
+
"""
|
|
468
|
+
expr_str = expr_str.strip()
|
|
469
|
+
|
|
470
|
+
try:
|
|
471
|
+
tree = ast.parse(expr_str, mode='eval')
|
|
472
|
+
except SyntaxError:
|
|
473
|
+
return expr_str
|
|
474
|
+
|
|
475
|
+
node = tree.body
|
|
476
|
+
|
|
477
|
+
# --- Strip the common `X or True` wrapper ---
|
|
478
|
+
stripped = _strip_or_true(node)
|
|
479
|
+
if stripped is not None:
|
|
480
|
+
node = stripped
|
|
481
|
+
|
|
482
|
+
result = _ast_to_cel(node)
|
|
483
|
+
return result if result is not None else expr_str
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def _tuple_representer(dumper, data):
|
|
487
|
+
"""Represent a Python tuple as a YAML sequence (without !!python/tuple tag)."""
|
|
488
|
+
return dumper.represent_list(data)
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def convert_yaml_file(filepath: str, dry_run: bool = False):
|
|
492
|
+
"""Convert all `actual` fields in a YAML file to CEL."""
|
|
493
|
+
import yaml
|
|
494
|
+
|
|
495
|
+
with open(filepath, 'r', encoding='utf-8') as f:
|
|
496
|
+
content = f.read()
|
|
497
|
+
|
|
498
|
+
data = yaml.safe_load(content)
|
|
499
|
+
|
|
500
|
+
count = 0
|
|
501
|
+
skipped = 0
|
|
502
|
+
skipped_exprs = []
|
|
503
|
+
|
|
504
|
+
def _walk(node, path=''):
|
|
505
|
+
nonlocal count, skipped
|
|
506
|
+
if isinstance(node, dict):
|
|
507
|
+
if 'actual' in node and isinstance(node['actual'], str):
|
|
508
|
+
cel = parse_expr_to_cel(node['actual'])
|
|
509
|
+
if isinstance(cel, dict):
|
|
510
|
+
node['actual'] = cel
|
|
511
|
+
count += 1
|
|
512
|
+
else:
|
|
513
|
+
skipped += 1
|
|
514
|
+
skipped_exprs.append((path, node['actual']))
|
|
515
|
+
for k, v in node.items():
|
|
516
|
+
_walk(v, f"{path}.{k}")
|
|
517
|
+
elif isinstance(node, list):
|
|
518
|
+
for i, item in enumerate(node):
|
|
519
|
+
_walk(item, f"{path}[{i}]")
|
|
520
|
+
|
|
521
|
+
_walk(data)
|
|
522
|
+
|
|
523
|
+
print(f"Converted: {count}, Skipped: {skipped}")
|
|
524
|
+
if skipped > 0 and dry_run:
|
|
525
|
+
for p, e in skipped_exprs:
|
|
526
|
+
print(f" SKIP {p}: {e[:100]}")
|
|
527
|
+
|
|
528
|
+
if not dry_run:
|
|
529
|
+
with open(filepath, 'w', encoding='utf-8') as f:
|
|
530
|
+
yaml.SafeDumper.add_representer(tuple, _tuple_representer)
|
|
531
|
+
yaml.dump(data, f, default_flow_style=False, sort_keys=False, allow_unicode=True, Dumper=yaml.SafeDumper)
|
|
532
|
+
|
|
533
|
+
return count, skipped
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
# ---------------------------------------------------------------------------
|
|
537
|
+
# Schema validation (Sprint 120 Future Work §7.3)
|
|
538
|
+
# ---------------------------------------------------------------------------
|
|
539
|
+
|
|
540
|
+
_DEFAULT_SCHEMA_RELS = [
|
|
541
|
+
os.path.join(os.path.dirname(__file__), "..", "..", "..", "..", "..", "types", "schemas", "cel_schema.yaml"),
|
|
542
|
+
os.path.join(os.path.dirname(__file__), "..", "..", "..", "..", "types", "schemas", "cel_schema.yaml"),
|
|
543
|
+
]
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
def _find_default_schema() -> str | None:
|
|
547
|
+
"""Locate cel_schema.yaml relative to this module or the project root."""
|
|
548
|
+
for rel in _DEFAULT_SCHEMA_RELS:
|
|
549
|
+
p = os.path.normpath(rel)
|
|
550
|
+
if os.path.isfile(p):
|
|
551
|
+
return p
|
|
552
|
+
return None
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def load_schema(schema_path: str | None = None) -> dict[str, dict]:
|
|
556
|
+
"""Load cel_schema.yaml and return ``{op_name: {required: [...], optional: [...]}}``.
|
|
557
|
+
|
|
558
|
+
Args:
|
|
559
|
+
schema_path: Path to ``cel_schema.yaml``. If None, auto-discovers via
|
|
560
|
+
``_find_default_schema()``.
|
|
561
|
+
|
|
562
|
+
Returns:
|
|
563
|
+
Dict mapping operation name to its schema definition (``required``,
|
|
564
|
+
``optional``, ``description``, ``defaults``).
|
|
565
|
+
"""
|
|
566
|
+
import yaml
|
|
567
|
+
|
|
568
|
+
if schema_path is None:
|
|
569
|
+
schema_path = _find_default_schema()
|
|
570
|
+
if schema_path is None or not os.path.isfile(schema_path):
|
|
571
|
+
raise FileNotFoundError(f"CEL schema not found: {schema_path}")
|
|
572
|
+
|
|
573
|
+
with open(schema_path, "r", encoding="utf-8") as f:
|
|
574
|
+
data = yaml.safe_load(f)
|
|
575
|
+
|
|
576
|
+
operations = data.get("operations", {}) if isinstance(data, dict) else {}
|
|
577
|
+
return operations
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def validate_cel(cel: Any, schema: dict[str, dict] | None = None, schema_path: str | None = None) -> list[str]:
|
|
581
|
+
"""Validate a CEL dict against ``cel_schema.yaml``.
|
|
582
|
+
|
|
583
|
+
Checks:
|
|
584
|
+
- ``cel`` must be a dict with an ``op`` field.
|
|
585
|
+
- ``op`` must be a known operation in the schema.
|
|
586
|
+
- All ``required`` fields listed in the schema must be present.
|
|
587
|
+
- No unknown top-level fields (beyond ``required`` + ``optional`` + ``op``).
|
|
588
|
+
|
|
589
|
+
Args:
|
|
590
|
+
cel: The CEL dict to validate.
|
|
591
|
+
schema: Pre-loaded schema dict (from ``load_schema``). If None, loads it.
|
|
592
|
+
schema_path: Path to schema file (used if ``schema`` is None).
|
|
593
|
+
|
|
594
|
+
Returns:
|
|
595
|
+
List of validation error strings. Empty list = valid.
|
|
596
|
+
"""
|
|
597
|
+
if schema is None:
|
|
598
|
+
schema = load_schema(schema_path)
|
|
599
|
+
|
|
600
|
+
errors: list[str] = []
|
|
601
|
+
|
|
602
|
+
if not isinstance(cel, dict):
|
|
603
|
+
return [f"CEL value is not a dict (got {type(cel).__name__}: {cel!r})"]
|
|
604
|
+
|
|
605
|
+
op = cel.get("op")
|
|
606
|
+
if op is None:
|
|
607
|
+
return ["CEL dict missing 'op' field"]
|
|
608
|
+
|
|
609
|
+
op_def = schema.get(op)
|
|
610
|
+
if op_def is None:
|
|
611
|
+
return [f"Unknown CEL operation: '{op}'"]
|
|
612
|
+
|
|
613
|
+
required = op_def.get("required", [])
|
|
614
|
+
optional = op_def.get("optional", [])
|
|
615
|
+
allowed_keys = {"op"} | set(required) | set(optional)
|
|
616
|
+
|
|
617
|
+
for field in required:
|
|
618
|
+
if field not in cel:
|
|
619
|
+
errors.append(f"Operation '{op}' missing required field '{field}'")
|
|
620
|
+
|
|
621
|
+
for key in cel:
|
|
622
|
+
if key not in allowed_keys:
|
|
623
|
+
errors.append(f"Operation '{op}' has unknown field '{key}'")
|
|
624
|
+
|
|
625
|
+
return errors
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
def validate_yaml_file(filepath: str, schema_path: str | None = None) -> tuple[int, list[str]]:
|
|
629
|
+
"""Validate all CEL ``actual`` fields in a YAML file against the schema.
|
|
630
|
+
|
|
631
|
+
Walks the YAML tree, finds every dict with an ``actual`` key that is itself
|
|
632
|
+
a dict (i.e., already CEL-converted), and validates it.
|
|
633
|
+
|
|
634
|
+
Args:
|
|
635
|
+
filepath: Path to the YAML file.
|
|
636
|
+
schema_path: Path to ``cel_schema.yaml``. If None, auto-discovers.
|
|
637
|
+
|
|
638
|
+
Returns:
|
|
639
|
+
``(validated_count, errors)`` where errors is a list of strings.
|
|
640
|
+
"""
|
|
641
|
+
import yaml
|
|
642
|
+
|
|
643
|
+
with open(filepath, "r", encoding="utf-8") as f:
|
|
644
|
+
data = yaml.safe_load(f)
|
|
645
|
+
|
|
646
|
+
schema = load_schema(schema_path)
|
|
647
|
+
errors: list[str] = []
|
|
648
|
+
count = 0
|
|
649
|
+
|
|
650
|
+
def _walk(node, path=""):
|
|
651
|
+
nonlocal count
|
|
652
|
+
if isinstance(node, dict):
|
|
653
|
+
if "actual" in node and isinstance(node["actual"], dict):
|
|
654
|
+
count += 1
|
|
655
|
+
cel = node["actual"]
|
|
656
|
+
errs = validate_cel(cel, schema)
|
|
657
|
+
for e in errs:
|
|
658
|
+
errors.append(f"{path}.actual: {e}")
|
|
659
|
+
for k, v in node.items():
|
|
660
|
+
_walk(v, f"{path}.{k}")
|
|
661
|
+
elif isinstance(node, list):
|
|
662
|
+
for i, item in enumerate(node):
|
|
663
|
+
_walk(item, f"{path}[{i}]")
|
|
664
|
+
|
|
665
|
+
if data is not None:
|
|
666
|
+
_walk(data)
|
|
667
|
+
|
|
668
|
+
return count, errors
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
if __name__ == "__main__":
|
|
672
|
+
import argparse
|
|
673
|
+
|
|
674
|
+
parser = argparse.ArgumentParser(description="CLM CEL Converter — convert and validate YAML actual fields")
|
|
675
|
+
parser.add_argument("file", help="YAML file to process")
|
|
676
|
+
parser.add_argument("--dry-run", action="store_true", help="Preview changes without writing")
|
|
677
|
+
parser.add_argument("--backup", action="store_true", help="Create .bak backup")
|
|
678
|
+
parser.add_argument("--validate", action="store_true", help="Validate CEL dicts against schema (no conversion)")
|
|
679
|
+
args = parser.parse_args()
|
|
680
|
+
|
|
681
|
+
if args.validate:
|
|
682
|
+
count, errors = validate_yaml_file(args.file)
|
|
683
|
+
if errors:
|
|
684
|
+
print(f"Validation FAILED: {len(errors)} errors in {count} CEL dicts:")
|
|
685
|
+
for e in errors:
|
|
686
|
+
print(f" {e}")
|
|
687
|
+
sys.exit(1)
|
|
688
|
+
else:
|
|
689
|
+
print(f"Validation PASSED: {count} CEL dicts are valid.")
|
|
690
|
+
sys.exit(0)
|
|
691
|
+
|
|
692
|
+
if args.backup:
|
|
693
|
+
import shutil
|
|
694
|
+
shutil.copy(args.file, args.file + ".bak")
|
|
695
|
+
|
|
696
|
+
count, skipped = convert_yaml_file(args.file, dry_run=args.dry_run)
|
|
697
|
+
print(f"Done. Converted {count} expressions, skipped {skipped}.")
|