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,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
|