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.
Files changed (333) hide show
  1. clm/__init__.py +230 -0
  2. clm/__main__.py +25 -0
  3. clm/cross_cutting/__init__.py +50 -0
  4. clm/cross_cutting/_test_runner.py +36 -0
  5. clm/cross_cutting/async_utils.py +25 -0
  6. clm/cross_cutting/cel_builder.py +123 -0
  7. clm/cross_cutting/cel_converter.py +697 -0
  8. clm/cross_cutting/cel_engine.py +252 -0
  9. clm/cross_cutting/cel_ops.py +72 -0
  10. clm/cross_cutting/cel_ops_db.py +157 -0
  11. clm/cross_cutting/cel_ops_files.py +99 -0
  12. clm/cross_cutting/cel_ops_state.py +71 -0
  13. clm/cross_cutting/cel_ops_strings.py +64 -0
  14. clm/cross_cutting/cel_resolver.py +63 -0
  15. clm/cross_cutting/cel_slices.py +56 -0
  16. clm/cross_cutting/cel_store.py +330 -0
  17. clm/cross_cutting/clm_logger.py +140 -0
  18. clm/cross_cutting/config/__init__.py +155 -0
  19. clm/cross_cutting/config/config_constants.py +392 -0
  20. clm/cross_cutting/config/env_parameters.py +90 -0
  21. clm/cross_cutting/config/logging.py +378 -0
  22. clm/cross_cutting/config/settings.py +504 -0
  23. clm/cross_cutting/context.py +5 -0
  24. clm/cross_cutting/domain_models.py +55 -0
  25. clm/cross_cutting/domain_types.py +61 -0
  26. clm/cross_cutting/effects.py +67 -0
  27. clm/cross_cutting/errors.py +153 -0
  28. clm/cross_cutting/governance.py +331 -0
  29. clm/cross_cutting/guards.py +41 -0
  30. clm/cross_cutting/mcard.py +321 -0
  31. clm/cross_cutting/native.py +185 -0
  32. clm/cross_cutting/observability.py +289 -0
  33. clm/cross_cutting/pcard.py +434 -0
  34. clm/cross_cutting/registry.py +71 -0
  35. clm/cross_cutting/resources.py +84 -0
  36. clm/cross_cutting/secrets.py +44 -0
  37. clm/cross_cutting/subprocess_utils.py +118 -0
  38. clm/cross_cutting/telemetry.py +164 -0
  39. clm/cross_cutting/testing/__init__.py +8 -0
  40. clm/cross_cutting/testing/comparator.py +187 -0
  41. clm/cross_cutting/testing/datasets.py +87 -0
  42. clm/cross_cutting/timeutil.py +20 -0
  43. clm/cross_cutting/types/new_type_check.py +0 -0
  44. clm/cross_cutting/utils/__init__.py +1 -0
  45. clm/cross_cutting/utils/url_safety.py +50 -0
  46. clm/cross_cutting/vcard.py +560 -0
  47. clm/cross_cutting/writer.py +118 -0
  48. clm/layer0/README.md +50 -0
  49. clm/layer0/__init__.py +223 -0
  50. clm/layer0/algebra.py +60 -0
  51. clm/layer0/algebra_baldwin.py +133 -0
  52. clm/layer0/algebra_baldwin_generators.py +36 -0
  53. clm/layer0/algebra_equivalence.py +262 -0
  54. clm/layer0/algebra_ingest.py +306 -0
  55. clm/layer0/algebra_manifest_io.py +156 -0
  56. clm/layer0/assembler.py +481 -0
  57. clm/layer0/cel/__init__.py +23 -0
  58. clm/layer0/cel_eval.py +491 -0
  59. clm/layer0/codec.py +106 -0
  60. clm/layer0/context.py +122 -0
  61. clm/layer0/db.py +160 -0
  62. clm/layer0/exceptions.py +73 -0
  63. clm/layer0/fibration.py +106 -0
  64. clm/layer0/gates.py +50 -0
  65. clm/layer0/hash.py +80 -0
  66. clm/layer0/loader.py +826 -0
  67. clm/layer0/math_utils.py +163 -0
  68. clm/layer0/mcard.py +96 -0
  69. clm/layer0/model/__init__.py +0 -0
  70. clm/layer0/model/action.py +223 -0
  71. clm/layer0/model/card.py +280 -0
  72. clm/layer0/model/card_triad.py +427 -0
  73. clm/layer0/model/compatibility.py +60 -0
  74. clm/layer0/model/dictionary.py +46 -0
  75. clm/layer0/model/dots.py +331 -0
  76. clm/layer0/model/event_producer.py +88 -0
  77. clm/layer0/model/g_time.py +89 -0
  78. clm/layer0/model/handle.py +163 -0
  79. clm/layer0/model/hash/__init__.py +6 -0
  80. clm/layer0/model/hash/algorithms/__init__.py +3 -0
  81. clm/layer0/model/hash/algorithms/custom_hash.py +7 -0
  82. clm/layer0/model/hash/algorithms/local_sha256.py +178 -0
  83. clm/layer0/model/hash/constants.py +53 -0
  84. clm/layer0/model/hash/enums.py +60 -0
  85. clm/layer0/model/hash/validator.py +307 -0
  86. clm/layer0/model/interpreter.py +275 -0
  87. clm/layer0/model/pagination.py +45 -0
  88. clm/layer0/model/pcard.py +716 -0
  89. clm/layer0/model/schema.py +44 -0
  90. clm/layer0/model/utils/__init__.py +1 -0
  91. clm/layer0/model/utils/content_analyzer.py +152 -0
  92. clm/layer0/model/validators/__init__.py +1 -0
  93. clm/layer0/model/validators/base_validator.py +44 -0
  94. clm/layer0/model/validators/binary_validator.py +66 -0
  95. clm/layer0/model/validators/text_validator.py +82 -0
  96. clm/layer0/model/validators/validation_registry.py +49 -0
  97. clm/layer0/model/vcard.py +823 -0
  98. clm/layer0/model/vcard_ext/__init__.py +39 -0
  99. clm/layer0/model/vcard_ext/core.py +60 -0
  100. clm/layer0/model/vcard_ext/network.py +23 -0
  101. clm/layer0/model/vcard_ext/observability.py +63 -0
  102. clm/layer0/model/vcard_ext/storage.py +60 -0
  103. clm/layer0/model/vcard_ext/vendors.py +211 -0
  104. clm/layer0/model/vcard_sandwich.py +119 -0
  105. clm/layer0/model/vcard_vocabulary.py +463 -0
  106. clm/layer0/model/workflow.py +274 -0
  107. clm/layer0/narrative.py +431 -0
  108. clm/layer0/ontology/collection.py +125 -0
  109. clm/layer0/parser.py +429 -0
  110. clm/layer0/relations.py +73 -0
  111. clm/layer0/rule_evaluator.py +251 -0
  112. clm/layer0/schema/__init__.py +38 -0
  113. clm/layer0/schema/ddl.py +97 -0
  114. clm/layer0/templates/abstract.yaml +99 -0
  115. clm/layer0/templates/balanced.yaml +253 -0
  116. clm/layer0/templates/concrete.yaml +172 -0
  117. clm/layer0/type_registry.py +223 -0
  118. clm/layer0/types.py +204 -0
  119. clm/layer0/utils.py +886 -0
  120. clm/layer0/verifier_core.py +262 -0
  121. clm/layer1/__init__.py +31 -0
  122. clm/layer1/action_dispatcher.py +275 -0
  123. clm/layer1/adapter_registry.py +233 -0
  124. clm/layer1/algebra_baldwin_generators.py +192 -0
  125. clm/layer1/arrow_compiler.py +525 -0
  126. clm/layer1/batch_processor.py +152 -0
  127. clm/layer1/cel/__init__.py +27 -0
  128. clm/layer1/cel/builder.py +173 -0
  129. clm/layer1/engine.py +759 -0
  130. clm/layer1/engine_async.py +89 -0
  131. clm/layer1/eval_callback.py +49 -0
  132. clm/layer1/gate_composer.py +229 -0
  133. clm/layer1/hoare_sandwich.py +298 -0
  134. clm/layer1/identity_guard.py +348 -0
  135. clm/layer1/matrix.py +138 -0
  136. clm/layer1/neural_runtime.py +221 -0
  137. clm/layer1/operations/__init__.py +123 -0
  138. clm/layer1/operations/augment.py +156 -0
  139. clm/layer1/operations/base.py +109 -0
  140. clm/layer1/operations/builtins.py +338 -0
  141. clm/layer1/operations/dispatch.py +144 -0
  142. clm/layer1/operations/exclude.py +107 -0
  143. clm/layer1/operations/handle.py +197 -0
  144. clm/layer1/operations/identity.py +139 -0
  145. clm/layer1/operations/invert.py +66 -0
  146. clm/layer1/operations/loader.py +116 -0
  147. clm/layer1/operations/manipulators/__init__.py +23 -0
  148. clm/layer1/operations/manipulators/document.py +211 -0
  149. clm/layer1/operations/manipulators/media.py +134 -0
  150. clm/layer1/operations/manipulators/structured.py +159 -0
  151. clm/layer1/operations/manipulators/tabular.py +334 -0
  152. clm/layer1/operations/port.py +65 -0
  153. clm/layer1/operations/services.py +529 -0
  154. clm/layer1/operations/split.py +117 -0
  155. clm/layer1/operations/substitute.py +75 -0
  156. clm/layer1/petri.py +1052 -0
  157. clm/layer1/pocketflow_orchestrator.py +408 -0
  158. clm/layer1/pocketflow_scheduler.py +519 -0
  159. clm/layer1/protocols.py +197 -0
  160. clm/layer1/runtime_adapter.py +718 -0
  161. clm/layer1/runtimes/__init__.py +71 -0
  162. clm/layer1/runtimes/_generated_assertions.py +125 -0
  163. clm/layer1/runtimes/_generated_types.py +42 -0
  164. clm/layer1/runtimes/base.py +265 -0
  165. clm/layer1/runtimes/binary.py +101 -0
  166. clm/layer1/runtimes/factory.py +203 -0
  167. clm/layer1/runtimes/javascript.py +321 -0
  168. clm/layer1/runtimes/javascript_runtime.js +33 -0
  169. clm/layer1/runtimes/js_scripts/isomorphism.mjs +22 -0
  170. clm/layer1/runtimes/js_scripts/loader.mjs +52 -0
  171. clm/layer1/runtimes/lambda_calc.py +126 -0
  172. clm/layer1/runtimes/python.py +288 -0
  173. clm/layer1/runtimes/python_runtime.py +41 -0
  174. clm/layer1/runtimes/script.py +114 -0
  175. clm/layer1/sandbox.py +334 -0
  176. clm/layer1/savepoint_guard.py +136 -0
  177. clm/layer1/sparse_net.py +122 -0
  178. clm/layer1/spec_tester.py +165 -0
  179. clm/layer1/store.py +295 -0
  180. clm/layer1/test_runner.py +498 -0
  181. clm/layer1/type_inspector.py +166 -0
  182. clm/layer1/verifier.py +772 -0
  183. clm/layer1/vm.py +91 -0
  184. clm/layer2/__init__.py +78 -0
  185. clm/layer2/card_collection.py +678 -0
  186. clm/layer2/classifier.py +479 -0
  187. clm/layer2/engine/__init__.py +1 -0
  188. clm/layer2/engine/abstract_sql_engine.py +320 -0
  189. clm/layer2/engine/base.py +193 -0
  190. clm/layer2/engine/duckdb_engine.py +576 -0
  191. clm/layer2/engine/sqlite_engine.py +518 -0
  192. clm/layer2/fiber/__init__.py +31 -0
  193. clm/layer2/fiber/context.py +89 -0
  194. clm/layer2/fiber/disposable.py +163 -0
  195. clm/layer2/fiber/fiber.py +125 -0
  196. clm/layer2/fiber/hoare.py +191 -0
  197. clm/layer2/file_io.py +376 -0
  198. clm/layer2/file_registrar.py +77 -0
  199. clm/layer2/ingest.py +202 -0
  200. clm/layer2/lineage.py +322 -0
  201. clm/layer2/mcard_fs.py +262 -0
  202. clm/layer2/merkle.py +11 -0
  203. clm/layer2/mime.py +244 -0
  204. clm/layer2/mime_detector.py +245 -0
  205. clm/layer2/private_collection.py +118 -0
  206. clm/layer2/shared_store.py +11 -0
  207. clm/layer2/storage/__init__.py +190 -0
  208. clm/layer2/storage/db_bitemporal.py +141 -0
  209. clm/layer2/storage/db_driver.py +331 -0
  210. clm/layer2/storage/db_hyperlinks.py +214 -0
  211. clm/layer2/storage/db_pool.py +139 -0
  212. clm/layer2/storage/db_repository.py +229 -0
  213. clm/layer2/storage/savepoint.py +78 -0
  214. clm/layer2/storage/sqlite_wal.py +74 -0
  215. clm/layer2/tridb.py +245 -0
  216. clm/layer2/vcard_receipt.py +71 -0
  217. clm/layer2/vfs.py +17 -0
  218. clm/layer3/__init__.py +141 -0
  219. clm/layer3/agency/__init__.py +19 -0
  220. clm/layer3/agency/config.py +220 -0
  221. clm/layer3/agency/providers/__init__.py +15 -0
  222. clm/layer3/agency/providers/base.py +91 -0
  223. clm/layer3/agency/providers/mlc_llm.py +169 -0
  224. clm/layer3/agency/providers/ollama.py +203 -0
  225. clm/layer3/agency/router.py +71 -0
  226. clm/layer3/agency/runtime.py +524 -0
  227. clm/layer3/gateway.py +353 -0
  228. clm/layer3/open_interpreter_adapter.py +159 -0
  229. clm/layer3/rag/__init__.py +70 -0
  230. clm/layer3/rag/cli.py +228 -0
  231. clm/layer3/rag/config.py +153 -0
  232. clm/layer3/rag/embeddings/__init__.py +16 -0
  233. clm/layer3/rag/embeddings/base.py +79 -0
  234. clm/layer3/rag/embeddings/ollama.py +211 -0
  235. clm/layer3/rag/embeddings/vision.py +290 -0
  236. clm/layer3/rag/engine.py +276 -0
  237. clm/layer3/rag/graph/__init__.py +22 -0
  238. clm/layer3/rag/graph/community.py +198 -0
  239. clm/layer3/rag/graph/engine.py +433 -0
  240. clm/layer3/rag/graph/extractor.py +255 -0
  241. clm/layer3/rag/graph/schema.py +115 -0
  242. clm/layer3/rag/graph/store.py +641 -0
  243. clm/layer3/rag/indexer.py +333 -0
  244. clm/layer3/rag/llm_providers.json +52 -0
  245. clm/layer3/rag/semantic_versioning.py +313 -0
  246. clm/layer3/rag/vector/__init__.py +28 -0
  247. clm/layer3/rag/vector/handle_vector_store.py +749 -0
  248. clm/layer3/rag/vector/schema.py +151 -0
  249. clm/layer3/rag/vector/store.py +632 -0
  250. clm/layer3/reticulum/__init__.py +55 -0
  251. clm/layer3/reticulum/bridge.py +74 -0
  252. clm/layer3/reticulum/daemon.py +88 -0
  253. clm/layer3/reticulum/discovery.py +164 -0
  254. clm/layer3/reticulum/identity.py +107 -0
  255. clm/layer3/reticulum/media.py +148 -0
  256. clm/layer3/reticulum/transport.py +133 -0
  257. clm/layer3/satori/__init__.py +209 -0
  258. clm/layer3/satori/alpha_conversion.py +168 -0
  259. clm/layer3/satori/beta_reduction.py +359 -0
  260. clm/layer3/satori/continuation.py +38 -0
  261. clm/layer3/satori/eta_conversion.py +205 -0
  262. clm/layer3/satori/free_variables.py +163 -0
  263. clm/layer3/satori/io_effects.py +370 -0
  264. clm/layer3/satori/lambda_runtime.py +726 -0
  265. clm/layer3/satori/lambda_term.py +230 -0
  266. clm/layer3/satori/sheaf_audit.py +261 -0
  267. clm/layer3/satori/speech_act.py +279 -0
  268. clm/layer4/README.md +9 -0
  269. clm/layer4/__init__.py +67 -0
  270. clm/layer4/_sovereign_executor.py +580 -0
  271. clm/layer4/action_commands.py +719 -0
  272. clm/layer4/api.py +236 -0
  273. clm/layer4/bootstrap/__init__.py +13 -0
  274. clm/layer4/bootstrap/bootstrapper.py +118 -0
  275. clm/layer4/bootstrap/cleanup.py +44 -0
  276. clm/layer4/bootstrap/clm_hooks.py +49 -0
  277. clm/layer4/bootstrap/genesis.py +77 -0
  278. clm/layer4/bootstrap/injection.py +125 -0
  279. clm/layer4/cli.py +549 -0
  280. clm/layer4/collection_manager.py +553 -0
  281. clm/layer4/commands/__init__.py +81 -0
  282. clm/layer4/commands/action_cmd.py +42 -0
  283. clm/layer4/commands/check_cmd.py +82 -0
  284. clm/layer4/commands/envelope.py +73 -0
  285. clm/layer4/commands/evaluate_cmd.py +224 -0
  286. clm/layer4/commands/mcard_cmd.py +24 -0
  287. clm/layer4/commands/petri_cmd.py +92 -0
  288. clm/layer4/commands/tridb_cmd.py +319 -0
  289. clm/layer4/commands/version_cmd.py +27 -0
  290. clm/layer4/diff_commands.py +167 -0
  291. clm/layer4/eoa_sponsor.py +232 -0
  292. clm/layer4/events.py +69 -0
  293. clm/layer4/flux_gateway.py +133 -0
  294. clm/layer4/gateway.py +91 -0
  295. clm/layer4/loader.py +307 -0
  296. clm/layer4/mcard_commands.py +166 -0
  297. clm/layer4/membrane.py +273 -0
  298. clm/layer4/navigation.py +50 -0
  299. clm/layer4/protocol/protocol_entry_python.py +108 -0
  300. clm/layer4/protocol/python_sync.py +126 -0
  301. clm/layer4/runner.py +150 -0
  302. clm/layer4/satori.py +262 -0
  303. clm/layer4/server/python_sync_client.py +150 -0
  304. clm/layer4/server/python_sync_server.py +139 -0
  305. clm/layer4/server/service_api.py +315 -0
  306. clm/layer4/server/start_servers.py +107 -0
  307. clm/layer4/server/stop_servers.py +24 -0
  308. clm/layer4/simulated_optimization_fix.py +0 -0
  309. clm/layer4/storage.py +463 -0
  310. clm/layer4/tridb_admin.py +268 -0
  311. clm/layer4/websocket/test_ws_client.py +104 -0
  312. clm/layer4/websocket/test_ws_server.py +138 -0
  313. clm/layer4/websocket/ws_server_local.py +160 -0
  314. clm/layer5/__init__.py +51 -0
  315. clm/layer5/async_batch_writer.py +133 -0
  316. clm/layer5/learning.py +276 -0
  317. clm/layer5/telemetry_hook.py +107 -0
  318. clm/layer5/type_lattice.py +238 -0
  319. clm/py.typed +0 -0
  320. clm/schemas/__init__.py +652 -0
  321. clm/schemas/abstract_face_schema.yaml +77 -0
  322. clm/schemas/cel_schema.yaml +416 -0
  323. clm/schemas/lifecycle/__init__.py +45 -0
  324. clm/schemas/lifecycle/models.py +123 -0
  325. clm/schemas/lifecycle/service.py +364 -0
  326. clm/schemas/mcard_schema.sql +192 -0
  327. clm/schemas/mcard_vector_schema.sql +283 -0
  328. clm/schemas/mlp-context-v1.json +91 -0
  329. clm_kernel-0.0.0.dist-info/METADATA +121 -0
  330. clm_kernel-0.0.0.dist-info/RECORD +333 -0
  331. clm_kernel-0.0.0.dist-info/WHEEL +4 -0
  332. clm_kernel-0.0.0.dist-info/entry_points.txt +3 -0
  333. clm_kernel-0.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,719 @@
1
+ """CLI Subcommands for Action and Workflow Execution, Validation, and Catalog (Sprint 396 / DEL-396-01).
2
+
3
+ Provides user-facing command-line operations:
4
+ - clm action validate <action.yaml> [--json]
5
+ - clm action run <action.yaml> [--input <uri_or_path>] [--bind key=val...] [--output-handle <name>] [--json] [--dry-run]
6
+ - clm action list [--catalog-dir <path>] [--json]
7
+ - clm workflow validate <workflow.yaml> [--json]
8
+ - clm workflow run <workflow.yaml> [--bind key=val...] [--workers <int>] [--json]
9
+
10
+ Acceptance Criteria:
11
+ - AC-396-01-01: clm action validate <path> returns 0 for valid manifests, non-zero for invalid ones.
12
+ - AC-396-01-02: clm action run <path> executes action, commits output MCard, outputs VCard receipt, and exits 0.
13
+ - AC-396-01-03: clm workflow run <path> executes multi-step DAG and returns all terminal MCard URIs.
14
+
15
+ Layer 4 module.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import argparse
21
+ import json
22
+ from pathlib import Path
23
+ from typing import Any
24
+
25
+ import yaml
26
+ from jsonschema import ValidationError as JsonSchemaValidationError
27
+ from jsonschema import validate
28
+
29
+ from clm.cross_cutting.clm_logger import get_logger
30
+ from clm.cross_cutting.domain_models import MCard
31
+ from clm.layer0.exceptions import CLMError
32
+ from clm.layer0.model.action import ActionManifest
33
+ from clm.layer0.model.workflow import WorkflowManifest
34
+ from clm.layer1.hoare_sandwich import HoareSandwichEngine, HoareSandwichResult
35
+ from clm.layer1.pocketflow_orchestrator import (
36
+ PocketFlowWorkflowOrchestrator,
37
+ WorkflowExecutionResult,
38
+ )
39
+ from clm.layer2.ingest import get_default_fs, ingest_file
40
+ from clm.layer2.mcard_fs import MCardFileSystem
41
+
42
+ _logger = get_logger("clm.layer4.action_commands")
43
+
44
+ _REPO_ROOT = Path(__file__).resolve().parents[4]
45
+ _ACTION_SCHEMA_PATH = _REPO_ROOT / "types" / "schemas" / "action_schema.json"
46
+ _WORKFLOW_SCHEMA_PATH = _REPO_ROOT / "types" / "schemas" / "workflow_schema.json"
47
+
48
+
49
+ def _load_schema(schema_path: Path) -> dict[str, Any]:
50
+ if not schema_path.is_file():
51
+ alt_path = Path.cwd() / "types" / "schemas" / schema_path.name
52
+ if alt_path.is_file():
53
+ return json.loads(alt_path.read_text(encoding="utf-8"))
54
+ raise FileNotFoundError(f"Cannot locate schema at {schema_path}")
55
+ return json.loads(schema_path.read_text(encoding="utf-8"))
56
+
57
+
58
+ def _parse_input_bindings(
59
+ bindings_list: list[str] | None,
60
+ fs: MCardFileSystem | None = None,
61
+ ) -> dict[str, Any]:
62
+ """Parse list of 'key=val' strings, auto-ingesting local file paths into CAS."""
63
+ bindings: dict[str, Any] = {}
64
+ if not bindings_list:
65
+ return bindings
66
+
67
+ target_fs = fs or get_default_fs()
68
+ for item in bindings_list:
69
+ if "=" not in item:
70
+ continue
71
+ key, val = item.split("=", 1)
72
+ key = key.strip()
73
+ val = val.strip()
74
+
75
+ # Check if val is a local file that should be auto-ingested
76
+ val_path = Path(val)
77
+ if val_path.is_file() and not val.startswith("mcard:"):
78
+ try:
79
+ card = ingest_file(path=val_path, fs=target_fs)
80
+ bindings[key] = card
81
+ continue
82
+ except Exception as e:
83
+ _logger.warning(f"Could not auto-ingest file '{val}' into CAS: {e}")
84
+ bindings[key] = val_path.read_text(encoding="utf-8")
85
+ continue
86
+
87
+ # Check if val is an mcard URI
88
+ if val.startswith("mcard:") or val.startswith("mcard://"):
89
+ clean_uri = val
90
+ if clean_uri.startswith("mcard://"):
91
+ clean_uri = "mcard:" + clean_uri[len("mcard://") :]
92
+ try:
93
+ card = target_fs.read_file(clean_uri)
94
+ if card is not None:
95
+ bindings[key] = card
96
+ continue
97
+ except Exception as e:
98
+ _logger.debug(f"Could not resolve MCard from {clean_uri}: {e}")
99
+
100
+ bindings[key] = val
101
+ return bindings
102
+
103
+
104
+ # ── Action CLI Commands ──────────────────────────────────────────────
105
+
106
+
107
+ def action_validate_cmd(path: str, as_json: bool = False) -> int:
108
+ """AC-396-01-01: Validate an Action manifest against JSON Schema & Pydantic domain model."""
109
+ manifest_path = Path(path)
110
+ if not manifest_path.is_file():
111
+ err_msg = f"Action manifest file not found: {path}"
112
+ if as_json:
113
+ print(json.dumps({"status": "error", "error": err_msg}))
114
+ else:
115
+ print(f"❌ Error: {err_msg}")
116
+ return 1
117
+
118
+ try:
119
+ content = manifest_path.read_text(encoding="utf-8")
120
+ raw_dict = (
121
+ yaml.safe_load(content)
122
+ if manifest_path.suffix in (".yaml", ".yml")
123
+ else json.loads(content)
124
+ )
125
+
126
+ if not isinstance(raw_dict, dict):
127
+ raise ValueError("Manifest content must be a JSON/YAML mapping")
128
+
129
+ schema = _load_schema(_ACTION_SCHEMA_PATH)
130
+ validate(instance=raw_dict, schema=schema)
131
+
132
+ manifest = ActionManifest.from_dict(raw_dict)
133
+
134
+ if as_json:
135
+ print(
136
+ json.dumps(
137
+ {
138
+ "status": "valid",
139
+ "name": manifest.clm.name,
140
+ "operator": manifest.clm.operator,
141
+ "inputs": [i.name for i in manifest.inputs],
142
+ "outputs": [o.name for o in manifest.outputs],
143
+ "preconditions": len(manifest.preconditions),
144
+ "postconditions": len(manifest.postconditions),
145
+ },
146
+ indent=2,
147
+ )
148
+ )
149
+ else:
150
+ print(f"✅ Action manifest '{path}' is valid.")
151
+ print(f" Name: {manifest.clm.name}")
152
+ print(f" Operator: {manifest.clm.operator}")
153
+ print(f" Inputs: {', '.join(i.name for i in manifest.inputs)}")
154
+ print(f" Outputs: {', '.join(o.name for o in manifest.outputs)}")
155
+ print(
156
+ f" Hoare Sandwich: {len(manifest.preconditions)} pre-conditions, {len(manifest.postconditions)} post-conditions"
157
+ )
158
+ return 0
159
+
160
+ except (JsonSchemaValidationError, ValueError, CLMError) as e:
161
+ if as_json:
162
+ print(json.dumps({"status": "invalid", "file": path, "error": str(e)}))
163
+ else:
164
+ print(f"❌ Validation failed for '{path}': {e}")
165
+ return 1
166
+ except Exception as e:
167
+ if as_json:
168
+ print(json.dumps({"status": "error", "file": path, "error": str(e)}))
169
+ else:
170
+ print(f"❌ Unexpected error validating '{path}': {e}")
171
+ return 1
172
+
173
+
174
+ def action_run_cmd(
175
+ path: str,
176
+ input_uri: str | None = None,
177
+ bindings: list[str] | None = None,
178
+ output_handle: str | None = None,
179
+ as_json: bool = False,
180
+ dry_run: bool = False,
181
+ fs: MCardFileSystem | None = None,
182
+ ) -> int:
183
+ """AC-396-01-02: Execute an Action manifest within the Hoare Sandwich."""
184
+ manifest_path = Path(path)
185
+ if not manifest_path.is_file():
186
+ err_msg = f"Action manifest file not found: {path}"
187
+ if as_json:
188
+ print(json.dumps({"status": "error", "error": err_msg}))
189
+ else:
190
+ print(f"❌ Error: {err_msg}")
191
+ return 1
192
+
193
+ target_fs = fs or get_default_fs()
194
+
195
+ try:
196
+ manifest = ActionManifest.from_yaml(manifest_path)
197
+ except Exception as e:
198
+ err_msg = f"Failed to parse Action manifest '{path}': {e}"
199
+ if as_json:
200
+ print(json.dumps({"status": "error", "error": err_msg}))
201
+ else:
202
+ print(f"❌ {err_msg}")
203
+ return 1
204
+
205
+ input_map = _parse_input_bindings(bindings, fs=target_fs)
206
+
207
+ if input_uri:
208
+ input_path = Path(input_uri)
209
+ resolved_card: Any = input_uri
210
+ if input_path.is_file() and not input_uri.startswith("mcard:"):
211
+ try:
212
+ resolved_card = ingest_file(path=input_path, fs=target_fs)
213
+ except Exception as e:
214
+ _logger.warning(f"Could not ingest '{input_uri}' into CAS: {e}")
215
+ resolved_card = input_path.read_text(encoding="utf-8")
216
+ elif input_uri.startswith("mcard:") or input_uri.startswith("mcard://"):
217
+ clean_uri = input_uri
218
+ if clean_uri.startswith("mcard://"):
219
+ clean_uri = "mcard:" + clean_uri[len("mcard://") :]
220
+ try:
221
+ card = target_fs.read_file(clean_uri)
222
+ if card is not None:
223
+ resolved_card = card
224
+ except Exception as e:
225
+ _logger.debug(f"Could not resolve MCard from {clean_uri}: {e}")
226
+
227
+ if manifest.inputs:
228
+ primary_name = manifest.inputs[0].name
229
+ if primary_name not in input_map:
230
+ input_map[primary_name] = resolved_card
231
+ else:
232
+ input_map["input"] = resolved_card
233
+
234
+ # Resolve any remaining string URIs in input_map
235
+ for k, v in list(input_map.items()):
236
+ if isinstance(v, str) and (v.startswith("mcard:") or v.startswith("mcard://")):
237
+ clean_uri = v
238
+ if clean_uri.startswith("mcard://"):
239
+ clean_uri = "mcard:" + clean_uri[len("mcard://") :]
240
+ try:
241
+ card = target_fs.read_file(clean_uri)
242
+ if card is not None:
243
+ input_map[k] = card
244
+ except Exception as e:
245
+ _logger.debug(f"Could not resolve MCard from {clean_uri}: {e}")
246
+
247
+ if dry_run:
248
+ input_descriptions = {}
249
+ for k, v in input_map.items():
250
+ if isinstance(v, MCard):
251
+ input_descriptions[k] = v.uri
252
+ else:
253
+ input_descriptions[k] = str(v)[:60]
254
+
255
+ dry_run_report = {
256
+ "status": "dry_run",
257
+ "action": manifest.clm.name,
258
+ "operator": manifest.clm.operator,
259
+ "resolved_inputs": input_descriptions,
260
+ "expected_outputs": [o.name for o in manifest.outputs],
261
+ "preconditions": [p.rule for p in manifest.preconditions],
262
+ "postconditions": [p.rule for p in manifest.postconditions],
263
+ }
264
+ if as_json:
265
+ print(json.dumps(dry_run_report, indent=2))
266
+ else:
267
+ print(f"🔍 Dry run successful for Action '{manifest.clm.name}':")
268
+ print(f" Operator: {manifest.clm.operator}")
269
+ print(f" Inputs: {input_descriptions}")
270
+ print(f" Outputs: {[o.name for o in manifest.outputs]}")
271
+ return 0
272
+
273
+ try:
274
+ result: HoareSandwichResult = HoareSandwichEngine.execute(
275
+ manifest=manifest,
276
+ input_bindings=input_map,
277
+ fs=target_fs,
278
+ )
279
+
280
+ if result.is_success:
281
+ output_uris = {}
282
+ for name, card in result.outputs.items():
283
+ card_uri = card.uri
284
+ if card_uri.startswith("mcard:"):
285
+ rest = card_uri[len("mcard:") :].lstrip("/")
286
+ output_uris[name] = f"mcard://{rest}"
287
+ else:
288
+ output_uris[name] = card_uri
289
+
290
+ receipt_uri = None
291
+ if result.receipt:
292
+ r_uri = result.receipt.uri
293
+ if r_uri.startswith("mcard:"):
294
+ receipt_uri = f"mcard://{r_uri[len('mcard:'):].lstrip('/')}"
295
+ else:
296
+ receipt_uri = r_uri
297
+
298
+ if as_json:
299
+ print(
300
+ json.dumps(
301
+ {
302
+ "status": "success",
303
+ "action": result.action_name,
304
+ "outputs": output_uris,
305
+ "receipt": receipt_uri,
306
+ "metrics": result.metrics,
307
+ },
308
+ indent=2,
309
+ )
310
+ )
311
+ else:
312
+ print(f"🚀 Action '{result.action_name}' executed successfully.")
313
+ for name, out_uri in output_uris.items():
314
+ print(f" Output [{name}]: {out_uri}")
315
+ if receipt_uri:
316
+ print(f" VCard Receipt: {receipt_uri}")
317
+ wall_ms = result.metrics.get("wall_time_ms", 0.0)
318
+ print(f" Execution time: {wall_ms:.2f}ms")
319
+ return 0
320
+ else:
321
+ if as_json:
322
+ print(
323
+ json.dumps(
324
+ {
325
+ "status": "failed",
326
+ "action": result.action_name,
327
+ "error": result.error,
328
+ "rollback_occurred": result.rollback_occurred,
329
+ "rollback_time_ms": result.rollback_time_ms,
330
+ },
331
+ indent=2,
332
+ )
333
+ )
334
+ else:
335
+ print(f"❌ Action '{result.action_name}' failed: {result.error}")
336
+ if result.rollback_occurred:
337
+ print(
338
+ f" Transactional rollback completed in {result.rollback_time_ms:.3f}ms"
339
+ )
340
+ return 1
341
+
342
+ except Exception as e:
343
+ if as_json:
344
+ print(json.dumps({"status": "error", "error": str(e)}))
345
+ else:
346
+ print(f"❌ Execution exception: {e}")
347
+ return 1
348
+
349
+
350
+ def action_list_cmd(
351
+ catalog_dir: str | None = None,
352
+ as_json: bool = False,
353
+ ) -> int:
354
+ """List exemplar actions available in the catalog."""
355
+ cat_path = Path(catalog_dir) if catalog_dir else (_REPO_ROOT / "sample_pcards" / "actions")
356
+
357
+ if not cat_path.is_dir():
358
+ err_msg = f"Catalog directory not found: {cat_path}"
359
+ if as_json:
360
+ print(json.dumps({"status": "error", "error": err_msg}))
361
+ else:
362
+ print(f"❌ Error: {err_msg}")
363
+ return 1
364
+
365
+ actions_info = []
366
+ for f in sorted(cat_path.iterdir()):
367
+ if f.suffix in (".yaml", ".yml", ".json") and f.name != "README.md":
368
+ try:
369
+ content = f.read_text(encoding="utf-8")
370
+ raw = (
371
+ yaml.safe_load(content)
372
+ if f.suffix in (".yaml", ".yml")
373
+ else json.loads(content)
374
+ )
375
+ if isinstance(raw, dict) and raw.get("clm", {}).get("type") == "action":
376
+ manifest = ActionManifest.from_dict(raw)
377
+ actions_info.append(
378
+ {
379
+ "file": f.name,
380
+ "name": manifest.clm.name,
381
+ "operator": manifest.clm.operator,
382
+ "description": manifest.clm.description or "",
383
+ "inputs": [i.name for i in manifest.inputs],
384
+ "outputs": [o.name for o in manifest.outputs],
385
+ }
386
+ )
387
+ except Exception:
388
+ continue
389
+
390
+ if as_json:
391
+ print(json.dumps({"catalog": str(cat_path), "actions": actions_info}, indent=2))
392
+ else:
393
+ print(f"📦 Canonical Exemplar Action Catalog ({cat_path}):")
394
+ print(f"{'Name':<35} {'Operator':<12} {'File':<30}")
395
+ print("-" * 80)
396
+ for act in actions_info:
397
+ print(f"{act['name']:<35} {act['operator']:<12} {act['file']:<30}")
398
+ return 0
399
+
400
+
401
+ # ── Workflow CLI Commands ────────────────────────────────────────────
402
+
403
+
404
+ def workflow_validate_cmd(path: str, as_json: bool = False) -> int:
405
+ """Validate a Workflow manifest against JSON Schema & Pydantic domain model."""
406
+ manifest_path = Path(path)
407
+ if not manifest_path.is_file():
408
+ err_msg = f"Workflow manifest file not found: {path}"
409
+ if as_json:
410
+ print(json.dumps({"status": "error", "error": err_msg}))
411
+ else:
412
+ print(f"❌ Error: {err_msg}")
413
+ return 1
414
+
415
+ try:
416
+ content = manifest_path.read_text(encoding="utf-8")
417
+ raw_dict = (
418
+ yaml.safe_load(content)
419
+ if manifest_path.suffix in (".yaml", ".yml")
420
+ else json.loads(content)
421
+ )
422
+
423
+ if not isinstance(raw_dict, dict):
424
+ raise ValueError("Manifest content must be a JSON/YAML mapping")
425
+
426
+ schema = _load_schema(_WORKFLOW_SCHEMA_PATH)
427
+ validate(instance=raw_dict, schema=schema)
428
+
429
+ workflow = WorkflowManifest.from_dict(raw_dict)
430
+
431
+ if as_json:
432
+ print(
433
+ json.dumps(
434
+ {
435
+ "status": "valid",
436
+ "name": workflow.clm.name,
437
+ "steps": len(workflow.steps),
438
+ "inputs": [i.name for i in workflow.inputs],
439
+ "outputs": [o.name for o in workflow.outputs],
440
+ },
441
+ indent=2,
442
+ )
443
+ )
444
+ else:
445
+ print(f"✅ Workflow manifest '{path}' is valid.")
446
+ print(f" Name: {workflow.clm.name}")
447
+ print(f" Steps: {len(workflow.steps)} DAG nodes")
448
+ print(f" Inputs: {', '.join(i.name for i in workflow.inputs)}")
449
+ print(f" Outputs: {', '.join(o.name for o in workflow.outputs)}")
450
+ return 0
451
+
452
+ except (JsonSchemaValidationError, ValueError, CLMError) as e:
453
+ if as_json:
454
+ print(json.dumps({"status": "invalid", "file": path, "error": str(e)}))
455
+ else:
456
+ print(f"❌ Validation failed for '{path}': {e}")
457
+ return 1
458
+ except Exception as e:
459
+ if as_json:
460
+ print(json.dumps({"status": "error", "file": path, "error": str(e)}))
461
+ else:
462
+ print(f"❌ Unexpected error validating '{path}': {e}")
463
+ return 1
464
+
465
+
466
+ def workflow_run_cmd(
467
+ path: str,
468
+ bindings: list[str] | None = None,
469
+ as_json: bool = False,
470
+ fs: MCardFileSystem | None = None,
471
+ max_workers: int = 4,
472
+ ) -> int:
473
+ """AC-396-01-03: Execute a Pocket Flow Monadic Workflow DAG pipeline."""
474
+ manifest_path = Path(path)
475
+ if not manifest_path.is_file():
476
+ err_msg = f"Workflow manifest file not found: {path}"
477
+ if as_json:
478
+ print(json.dumps({"status": "error", "error": err_msg}))
479
+ else:
480
+ print(f"❌ Error: {err_msg}")
481
+ return 1
482
+
483
+ target_fs = fs or get_default_fs()
484
+
485
+ try:
486
+ content = manifest_path.read_text(encoding="utf-8")
487
+ raw = (
488
+ yaml.safe_load(content)
489
+ if manifest_path.suffix in (".yaml", ".yml")
490
+ else json.loads(content)
491
+ )
492
+ workflow = WorkflowManifest.from_dict(raw)
493
+ except Exception as e:
494
+ err_msg = f"Failed to parse Workflow manifest '{path}': {e}"
495
+ if as_json:
496
+ print(json.dumps({"status": "error", "error": err_msg}))
497
+ else:
498
+ print(f"❌ {err_msg}")
499
+ return 1
500
+
501
+ input_map = _parse_input_bindings(bindings, fs=target_fs)
502
+
503
+ # Resolve any mcard:// URIs into full MCard instances from CAS
504
+ for k, v in list(input_map.items()):
505
+ if isinstance(v, str) and (v.startswith("mcard:") or v.startswith("mcard://")):
506
+ clean_uri = v
507
+ if clean_uri.startswith("mcard://"):
508
+ clean_uri = "mcard:" + clean_uri[len("mcard://") :]
509
+ try:
510
+ card = target_fs.read_file(clean_uri)
511
+ if card is not None:
512
+ input_map[k] = card
513
+ except Exception as e:
514
+ _logger.debug(f"Could not resolve MCard from {clean_uri}: {e}")
515
+
516
+ orchestrator = PocketFlowWorkflowOrchestrator(base_dir=manifest_path.parent)
517
+
518
+ try:
519
+ result: WorkflowExecutionResult = orchestrator.execute(
520
+ workflow=workflow,
521
+ inputs=input_map,
522
+ fs=target_fs,
523
+ max_workers=max_workers,
524
+ raise_on_failure=False,
525
+ )
526
+
527
+ if result.is_success:
528
+ output_uris = {}
529
+ for name, card in result.outputs.items():
530
+ c_uri = card.uri
531
+ if c_uri.startswith("mcard:"):
532
+ output_uris[name] = f"mcard://{c_uri[len('mcard:'):].lstrip('/')}"
533
+ else:
534
+ output_uris[name] = c_uri
535
+
536
+ receipt_uris = {}
537
+ for step_id, r_card in result.step_receipts.items():
538
+ r_uri = r_card.uri
539
+ if r_uri.startswith("mcard:"):
540
+ receipt_uris[step_id] = f"mcard://{r_uri[len('mcard:'):].lstrip('/')}"
541
+ else:
542
+ receipt_uris[step_id] = r_uri
543
+
544
+ if as_json:
545
+ print(
546
+ json.dumps(
547
+ {
548
+ "status": "success",
549
+ "workflow": result.workflow_name,
550
+ "outputs": output_uris,
551
+ "receipts": receipt_uris,
552
+ "metrics": result.metrics,
553
+ },
554
+ indent=2,
555
+ )
556
+ )
557
+ else:
558
+ print(f"🚀 Workflow '{result.workflow_name}' completed successfully.")
559
+ print(f" Steps executed: {len(result.step_results)}")
560
+ for name, uri_out in output_uris.items():
561
+ print(f" Terminal Output [{name}]: {uri_out}")
562
+ for step_id, r_uri in receipt_uris.items():
563
+ print(f" Step [{step_id}] VCard: {r_uri}")
564
+ wall_ms = result.metrics.get("wall_time_ms", 0.0)
565
+ print(f" Total pipeline time: {wall_ms:.2f}ms")
566
+ return 0
567
+ else:
568
+ if as_json:
569
+ print(
570
+ json.dumps(
571
+ {
572
+ "status": "failed",
573
+ "workflow": result.workflow_name,
574
+ "failed_step": result.failed_step,
575
+ "error": result.error,
576
+ },
577
+ indent=2,
578
+ )
579
+ )
580
+ else:
581
+ print(
582
+ f"❌ Workflow '{result.workflow_name}' failed at step '{result.failed_step}': {result.error}"
583
+ )
584
+ return 1
585
+
586
+ except Exception as e:
587
+ if as_json:
588
+ print(json.dumps({"status": "error", "error": str(e)}))
589
+ else:
590
+ print(f"❌ Workflow execution exception: {e}")
591
+ return 1
592
+
593
+
594
+ # ── Top-Level Subcommand Handlers ────────────────────────────────────
595
+
596
+
597
+ def handle_action_cli(args: list[str], fs: MCardFileSystem | None = None) -> int:
598
+ """Entry point for `clm action ...` subcommands."""
599
+ parser = argparse.ArgumentParser(
600
+ prog="clm action",
601
+ description="CLM Algebraic Action Operations (Sprint 396 / DEL-396-01)",
602
+ )
603
+ subparsers = parser.add_subparsers(dest="subcommand", help="Action operation")
604
+
605
+ # validate
606
+ validate_p = subparsers.add_parser(
607
+ "validate", help="Statically validate an Action manifest"
608
+ )
609
+ validate_p.add_argument("path", help="Path to action.yaml or action.json")
610
+ validate_p.add_argument(
611
+ "--json", action="store_true", help="Output validation result as JSON"
612
+ )
613
+
614
+ # run
615
+ run_p = subparsers.add_parser(
616
+ "run", help="Execute an Action manifest inside Hoare Sandwich"
617
+ )
618
+ run_p.add_argument("path", help="Path to action.yaml or action.json")
619
+ run_p.add_argument(
620
+ "--input", "-i", default=None, help="Input MCard URI or local file path"
621
+ )
622
+ run_p.add_argument(
623
+ "--bind",
624
+ "-b",
625
+ action="append",
626
+ dest="bindings",
627
+ help="Input binding in key=value format (repeatable)",
628
+ )
629
+ run_p.add_argument(
630
+ "--output-handle", "-o", default=None, help="Optional output handle name"
631
+ )
632
+ run_p.add_argument("--json", action="store_true", help="Output result as JSON")
633
+ run_p.add_argument(
634
+ "--dry-run",
635
+ action="store_true",
636
+ help="Validate and resolve inputs without running operator",
637
+ )
638
+
639
+ # list
640
+ list_p = subparsers.add_parser(
641
+ "list", help="List exemplar actions in the canonical catalog"
642
+ )
643
+ list_p.add_argument(
644
+ "--catalog-dir", default=None, help="Custom catalog directory path"
645
+ )
646
+ list_p.add_argument("--json", action="store_true", help="Output list as JSON")
647
+
648
+ parsed = parser.parse_args(args)
649
+
650
+ if parsed.subcommand == "validate":
651
+ return action_validate_cmd(parsed.path, as_json=parsed.json)
652
+ elif parsed.subcommand == "run":
653
+ return action_run_cmd(
654
+ parsed.path,
655
+ input_uri=parsed.input,
656
+ bindings=parsed.bindings,
657
+ output_handle=parsed.output_handle,
658
+ as_json=parsed.json,
659
+ dry_run=parsed.dry_run,
660
+ fs=fs,
661
+ )
662
+ elif parsed.subcommand == "list":
663
+ return action_list_cmd(catalog_dir=parsed.catalog_dir, as_json=parsed.json)
664
+ else:
665
+ parser.print_help()
666
+ return 1
667
+
668
+
669
+ def handle_workflow_cli(args: list[str], fs: MCardFileSystem | None = None) -> int:
670
+ """Entry point for `clm workflow ...` subcommands."""
671
+ parser = argparse.ArgumentParser(
672
+ prog="clm workflow",
673
+ description="CLM Pocket Flow Workflow Orchestration (Sprint 396 / DEL-396-01)",
674
+ )
675
+ subparsers = parser.add_subparsers(dest="subcommand", help="Workflow operation")
676
+
677
+ # validate
678
+ validate_p = subparsers.add_parser(
679
+ "validate", help="Statically validate a Workflow manifest"
680
+ )
681
+ validate_p.add_argument("path", help="Path to workflow.yaml or workflow.json")
682
+ validate_p.add_argument(
683
+ "--json", action="store_true", help="Output validation result as JSON"
684
+ )
685
+
686
+ # run
687
+ run_p = subparsers.add_parser("run", help="Execute a multi-step workflow DAG")
688
+ run_p.add_argument("path", help="Path to workflow.yaml or workflow.json")
689
+ run_p.add_argument(
690
+ "--bind",
691
+ "-b",
692
+ action="append",
693
+ dest="bindings",
694
+ help="Initial input binding in key=value format (repeatable)",
695
+ )
696
+ run_p.add_argument(
697
+ "--workers",
698
+ "-w",
699
+ type=int,
700
+ default=4,
701
+ help="Maximum concurrent worker threads for parallel stages",
702
+ )
703
+ run_p.add_argument("--json", action="store_true", help="Output result as JSON")
704
+
705
+ parsed = parser.parse_args(args)
706
+
707
+ if parsed.subcommand == "validate":
708
+ return workflow_validate_cmd(parsed.path, as_json=parsed.json)
709
+ elif parsed.subcommand == "run":
710
+ return workflow_run_cmd(
711
+ parsed.path,
712
+ bindings=parsed.bindings,
713
+ as_json=parsed.json,
714
+ fs=fs,
715
+ max_workers=parsed.workers,
716
+ )
717
+ else:
718
+ parser.print_help()
719
+ return 1