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,529 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Services Operations Module
|
|
3
|
+
==========================
|
|
4
|
+
|
|
5
|
+
Contains operations for managing long-running services (servers, daemons).
|
|
6
|
+
Moved from builtins.py to separate concerns.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from clm.cross_cutting.config.config_constants import (
|
|
12
|
+
DEFAULT_WS_HOST,
|
|
13
|
+
DEFAULT_WS_PORT,
|
|
14
|
+
ENV_WS_PORT,
|
|
15
|
+
ENV_WS_PORT_LEGACY,
|
|
16
|
+
SIGNALING_PORT_RELEASE_WAIT_SECS,
|
|
17
|
+
)
|
|
18
|
+
from clm.layer0.model.card import MCard
|
|
19
|
+
|
|
20
|
+
NETWORK_DEFAULT_HOST = DEFAULT_WS_HOST
|
|
21
|
+
PROCESS_TERMINATE_WAIT_SECONDS = SIGNALING_PORT_RELEASE_WAIT_SECS
|
|
22
|
+
STATIC_SERVER_START_WAIT_SECONDS = 1.5
|
|
23
|
+
WEBSOCKET_SERVER_START_WAIT_SECONDS = 3.0
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def op_static_server(impl: dict, target: MCard, ctx: dict) -> Any:
|
|
27
|
+
"""
|
|
28
|
+
Execute static_server builtin - manages HTTP static file server.
|
|
29
|
+
|
|
30
|
+
Actions: deploy, status, stop
|
|
31
|
+
Config: root_dir, port, host
|
|
32
|
+
"""
|
|
33
|
+
import os
|
|
34
|
+
import signal
|
|
35
|
+
import socket
|
|
36
|
+
import subprocess
|
|
37
|
+
import time
|
|
38
|
+
|
|
39
|
+
def _resolve_config_value(val: Any) -> Any:
|
|
40
|
+
"""Resolve environment variables in config values like '${VAR}'."""
|
|
41
|
+
if isinstance(val, str) and val.startswith("${") and val.endswith("}"):
|
|
42
|
+
env_var = val[2:-1]
|
|
43
|
+
return os.getenv(env_var, val)
|
|
44
|
+
return val
|
|
45
|
+
|
|
46
|
+
# Valid config keys: port, host, root_dir
|
|
47
|
+
config = impl.get("config", {})
|
|
48
|
+
action = ctx.get("action") or config.get("action", "status")
|
|
49
|
+
|
|
50
|
+
# Default to HTTP_PORT from .env matching the project configuration
|
|
51
|
+
default_port = int(os.getenv("HTTP_PORT", 5320))
|
|
52
|
+
raw_port = ctx.get("port") or config.get("port", default_port)
|
|
53
|
+
port = int(_resolve_config_value(raw_port))
|
|
54
|
+
|
|
55
|
+
host = ctx.get("host") or config.get("host", NETWORK_DEFAULT_HOST)
|
|
56
|
+
root_dir = ctx.get("root_dir") or config.get("root_dir", ".")
|
|
57
|
+
|
|
58
|
+
# Resolve root_dir relative to project root
|
|
59
|
+
project_root = os.getcwd()
|
|
60
|
+
if not os.path.isabs(root_dir):
|
|
61
|
+
root_dir = os.path.normpath(os.path.join(project_root, root_dir))
|
|
62
|
+
|
|
63
|
+
pid_file = os.path.join(project_root, f".static_server_{port}.pid")
|
|
64
|
+
|
|
65
|
+
def is_port_in_use(p: int) -> bool:
|
|
66
|
+
# Use lsof to check if port is in use (handles both IPv4 and IPv6)
|
|
67
|
+
try:
|
|
68
|
+
result = subprocess.run(
|
|
69
|
+
["lsof", "-ti", f":{p}"], capture_output=True, text=True
|
|
70
|
+
)
|
|
71
|
+
return result.returncode == 0 and result.stdout.strip() != ""
|
|
72
|
+
except (OSError, subprocess.SubprocessError):
|
|
73
|
+
# Fallback to socket check
|
|
74
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
75
|
+
return s.connect_ex(("localhost", p)) == 0
|
|
76
|
+
|
|
77
|
+
def get_pid_on_port(p: int):
|
|
78
|
+
try:
|
|
79
|
+
result = subprocess.run(
|
|
80
|
+
["lsof", "-ti", f":{p}"], capture_output=True, text=True
|
|
81
|
+
)
|
|
82
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
83
|
+
return int(result.stdout.strip().split("\n")[0])
|
|
84
|
+
except (OSError, subprocess.SubprocessError, ValueError):
|
|
85
|
+
pass
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
if action == "deploy":
|
|
89
|
+
# Check if already running
|
|
90
|
+
if is_port_in_use(port):
|
|
91
|
+
pid = get_pid_on_port(port)
|
|
92
|
+
return {
|
|
93
|
+
"success": True,
|
|
94
|
+
"message": "Server already running",
|
|
95
|
+
"pid": pid,
|
|
96
|
+
"port": port,
|
|
97
|
+
"url": f"http://{host}:{port}",
|
|
98
|
+
"status": "already_running",
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
# Verify root_dir exists
|
|
102
|
+
if not os.path.isdir(root_dir):
|
|
103
|
+
return {"success": False, "error": f"Directory not found: {root_dir}"}
|
|
104
|
+
|
|
105
|
+
# Start Python HTTP server as background process
|
|
106
|
+
try:
|
|
107
|
+
process = subprocess.Popen(
|
|
108
|
+
[
|
|
109
|
+
"python3",
|
|
110
|
+
"-m",
|
|
111
|
+
"http.server",
|
|
112
|
+
str(port),
|
|
113
|
+
"--bind",
|
|
114
|
+
host,
|
|
115
|
+
"--directory",
|
|
116
|
+
root_dir,
|
|
117
|
+
],
|
|
118
|
+
stdout=subprocess.DEVNULL,
|
|
119
|
+
stderr=subprocess.DEVNULL,
|
|
120
|
+
start_new_session=True,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# Save PID
|
|
124
|
+
with open(pid_file, "w") as f:
|
|
125
|
+
f.write(str(process.pid))
|
|
126
|
+
|
|
127
|
+
# Wait for server to start
|
|
128
|
+
time.sleep(STATIC_SERVER_START_WAIT_SECONDS)
|
|
129
|
+
|
|
130
|
+
if is_port_in_use(port):
|
|
131
|
+
return {
|
|
132
|
+
"success": True,
|
|
133
|
+
"message": "Server deployed successfully",
|
|
134
|
+
"pid": process.pid,
|
|
135
|
+
"port": port,
|
|
136
|
+
"url": f"http://{host}:{port}",
|
|
137
|
+
"root_dir": root_dir,
|
|
138
|
+
"status": "running",
|
|
139
|
+
}
|
|
140
|
+
else:
|
|
141
|
+
return {"success": False, "error": "Server started but not responding"}
|
|
142
|
+
|
|
143
|
+
except (OSError, subprocess.SubprocessError, ValueError) as e:
|
|
144
|
+
return {"success": False, "error": f"Failed to start server: {e}"}
|
|
145
|
+
|
|
146
|
+
elif action == "status":
|
|
147
|
+
is_running = is_port_in_use(port)
|
|
148
|
+
pid = get_pid_on_port(port)
|
|
149
|
+
|
|
150
|
+
saved_pid = None
|
|
151
|
+
if os.path.isfile(pid_file):
|
|
152
|
+
try:
|
|
153
|
+
with open(pid_file) as f:
|
|
154
|
+
saved_pid = int(f.read().strip())
|
|
155
|
+
except (OSError, ValueError):
|
|
156
|
+
pass
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
"success": True,
|
|
160
|
+
"running": is_running,
|
|
161
|
+
"pid": pid,
|
|
162
|
+
"saved_pid": saved_pid,
|
|
163
|
+
"port": port,
|
|
164
|
+
"url": f"http://{host}:{port}" if is_running else None,
|
|
165
|
+
"status": "running" if is_running else "stopped",
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
elif action == "stop":
|
|
169
|
+
pid = get_pid_on_port(port)
|
|
170
|
+
|
|
171
|
+
if not pid:
|
|
172
|
+
if os.path.isfile(pid_file):
|
|
173
|
+
os.remove(pid_file)
|
|
174
|
+
return {
|
|
175
|
+
"success": True,
|
|
176
|
+
"message": "No server running on this port",
|
|
177
|
+
"port": port,
|
|
178
|
+
"status": "stopped",
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
try:
|
|
182
|
+
os.kill(pid, signal.SIGTERM)
|
|
183
|
+
time.sleep(PROCESS_TERMINATE_WAIT_SECONDS)
|
|
184
|
+
|
|
185
|
+
if is_port_in_use(port):
|
|
186
|
+
os.kill(pid, signal.SIGKILL)
|
|
187
|
+
time.sleep(PROCESS_TERMINATE_WAIT_SECONDS)
|
|
188
|
+
|
|
189
|
+
if os.path.isfile(pid_file):
|
|
190
|
+
os.remove(pid_file)
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
"success": True,
|
|
194
|
+
"message": "Server stopped",
|
|
195
|
+
"pid": pid,
|
|
196
|
+
"port": port,
|
|
197
|
+
"status": "stopped",
|
|
198
|
+
}
|
|
199
|
+
except ProcessLookupError:
|
|
200
|
+
if os.path.isfile(pid_file):
|
|
201
|
+
os.remove(pid_file)
|
|
202
|
+
return {
|
|
203
|
+
"success": True,
|
|
204
|
+
"message": "Server was not running",
|
|
205
|
+
"port": port,
|
|
206
|
+
"status": "stopped",
|
|
207
|
+
}
|
|
208
|
+
except (OSError, subprocess.SubprocessError) as e:
|
|
209
|
+
return {"success": False, "error": f"Failed to stop server: {e}"}
|
|
210
|
+
|
|
211
|
+
else:
|
|
212
|
+
return {"success": False, "error": f"Unknown action: {action}"}
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def op_websocket_server(impl: dict, target: MCard, ctx: dict) -> Any:
|
|
216
|
+
"""
|
|
217
|
+
Execute websocket_server builtin - manages a Python-based WebSocket server.
|
|
218
|
+
|
|
219
|
+
Actions: deploy, status, stop
|
|
220
|
+
Config: port, host
|
|
221
|
+
|
|
222
|
+
Uses Python's asyncio-based WebSocket server internally.
|
|
223
|
+
"""
|
|
224
|
+
import os
|
|
225
|
+
import signal
|
|
226
|
+
import socket
|
|
227
|
+
import subprocess
|
|
228
|
+
import sys
|
|
229
|
+
import time
|
|
230
|
+
|
|
231
|
+
def _resolve_config_value(val: Any) -> Any:
|
|
232
|
+
"""Resolve environment variables in config values like '${VAR}'."""
|
|
233
|
+
if isinstance(val, str) and val.startswith("${") and val.endswith("}"):
|
|
234
|
+
env_var = val[2:-1]
|
|
235
|
+
return os.getenv(env_var, val)
|
|
236
|
+
return val
|
|
237
|
+
|
|
238
|
+
# Valid config keys: port, host, root_dir
|
|
239
|
+
config = impl.get("config", {})
|
|
240
|
+
action = ctx.get("action") or config.get("action", "status")
|
|
241
|
+
|
|
242
|
+
# Default to the canonical shared websocket port, with legacy fallback.
|
|
243
|
+
default_port = int(
|
|
244
|
+
os.getenv(ENV_WS_PORT, os.getenv(ENV_WS_PORT_LEGACY, str(DEFAULT_WS_PORT)))
|
|
245
|
+
)
|
|
246
|
+
raw_port = ctx.get("port") or config.get("port", default_port)
|
|
247
|
+
port = int(_resolve_config_value(raw_port))
|
|
248
|
+
|
|
249
|
+
host = ctx.get("host") or config.get("host", NETWORK_DEFAULT_HOST)
|
|
250
|
+
|
|
251
|
+
project_root = os.getcwd()
|
|
252
|
+
pid_file = os.path.join(project_root, f".websocket_server_{port}.pid")
|
|
253
|
+
|
|
254
|
+
def is_port_in_use(p: int) -> bool:
|
|
255
|
+
# Use lsof to check if port is in use (handles both IPv4 and IPv6)
|
|
256
|
+
try:
|
|
257
|
+
result = subprocess.run(
|
|
258
|
+
["lsof", "-ti", f":{p}"], capture_output=True, text=True
|
|
259
|
+
)
|
|
260
|
+
return result.returncode == 0 and result.stdout.strip() != ""
|
|
261
|
+
except (OSError, subprocess.SubprocessError):
|
|
262
|
+
# Fallback to socket check
|
|
263
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
264
|
+
return s.connect_ex(("localhost", p)) == 0
|
|
265
|
+
|
|
266
|
+
def get_pid_on_port(p: int):
|
|
267
|
+
try:
|
|
268
|
+
result = subprocess.run(
|
|
269
|
+
["lsof", "-ti", f":{p}"], capture_output=True, text=True
|
|
270
|
+
)
|
|
271
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
272
|
+
return int(result.stdout.strip().split("\n")[0])
|
|
273
|
+
except (OSError, subprocess.SubprocessError, ValueError):
|
|
274
|
+
pass
|
|
275
|
+
return None
|
|
276
|
+
|
|
277
|
+
if action == "deploy":
|
|
278
|
+
# Check if already running
|
|
279
|
+
if is_port_in_use(port):
|
|
280
|
+
pid = get_pid_on_port(port)
|
|
281
|
+
return {
|
|
282
|
+
"success": True,
|
|
283
|
+
"message": "WebSocket server already running",
|
|
284
|
+
"pid": pid,
|
|
285
|
+
"port": port,
|
|
286
|
+
"url": f"ws://{host}:{port}/",
|
|
287
|
+
"status": "already_running",
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
# Python WebSocket server script (inline)
|
|
291
|
+
ws_server_code = f'''
|
|
292
|
+
import asyncio
|
|
293
|
+
import json
|
|
294
|
+
import signal
|
|
295
|
+
import sys
|
|
296
|
+
import time
|
|
297
|
+
import yaml
|
|
298
|
+
|
|
299
|
+
try:
|
|
300
|
+
import websockets
|
|
301
|
+
except ImportError:
|
|
302
|
+
print("websockets module not found, using basic socket server")
|
|
303
|
+
sys.exit(1)
|
|
304
|
+
|
|
305
|
+
# Import PTR components for CLM execution
|
|
306
|
+
try:
|
|
307
|
+
from clm.layer0.loader import CLMChapterLoader
|
|
308
|
+
except ImportError:
|
|
309
|
+
print("Warning: clm not available, CLM execution disabled")
|
|
310
|
+
CLMChapterLoader = None
|
|
311
|
+
|
|
312
|
+
connected_clients = set()
|
|
313
|
+
|
|
314
|
+
async def execute_clm(clm_yaml, input_data):
|
|
315
|
+
"""Execute CLM code and return result"""
|
|
316
|
+
if not CLMChapterLoader:
|
|
317
|
+
return {{"error": "CLM execution not available - clm not installed"}}
|
|
318
|
+
|
|
319
|
+
import tempfile
|
|
320
|
+
import os
|
|
321
|
+
|
|
322
|
+
try:
|
|
323
|
+
start_time = time.time()
|
|
324
|
+
|
|
325
|
+
# Parse YAML to get metadata
|
|
326
|
+
clm_data = yaml.safe_load(clm_yaml)
|
|
327
|
+
|
|
328
|
+
# Write YAML to temporary file
|
|
329
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
|
|
330
|
+
f.write(clm_yaml)
|
|
331
|
+
temp_path = f.name
|
|
332
|
+
|
|
333
|
+
try:
|
|
334
|
+
# Load chapter from YAML file
|
|
335
|
+
chapter = CLMChapterLoader.load_from_yaml(temp_path)
|
|
336
|
+
|
|
337
|
+
# Execute chapter's action directly with provided input
|
|
338
|
+
result, state, logs = chapter.action.execute(input_data, {{}})
|
|
339
|
+
|
|
340
|
+
execution_time = (time.time() - start_time) * 1000 # ms
|
|
341
|
+
|
|
342
|
+
return {{
|
|
343
|
+
"success": True,
|
|
344
|
+
"result": result,
|
|
345
|
+
"executionTime": execution_time,
|
|
346
|
+
"chapter": clm_data.get("chapter", {{}}).get("title", "Unknown"),
|
|
347
|
+
"concept": clm_data.get("clm", {{}}).get("abstract", {{}}).get("concept", "Unknown"),
|
|
348
|
+
"logs": logs
|
|
349
|
+
}}
|
|
350
|
+
finally:
|
|
351
|
+
# Clean up temp file
|
|
352
|
+
if os.path.exists(temp_path):
|
|
353
|
+
os.unlink(temp_path)
|
|
354
|
+
|
|
355
|
+
except (OSError, subprocess.SubprocessError, ValueError) as e:
|
|
356
|
+
return {{
|
|
357
|
+
"success": False,
|
|
358
|
+
"error": str(e),
|
|
359
|
+
"executionTime": 0
|
|
360
|
+
}}
|
|
361
|
+
|
|
362
|
+
async def handler(websocket):
|
|
363
|
+
connected_clients.add(websocket)
|
|
364
|
+
try:
|
|
365
|
+
async for message in websocket:
|
|
366
|
+
try:
|
|
367
|
+
data = json.loads(message)
|
|
368
|
+
msg_type = data.get("type")
|
|
369
|
+
message_id = data.get("messageId")
|
|
370
|
+
|
|
371
|
+
if msg_type == "clm_execute":
|
|
372
|
+
# Execute CLM
|
|
373
|
+
clm_code = data.get("clm", "")
|
|
374
|
+
input_data = data.get("input", {{}})
|
|
375
|
+
|
|
376
|
+
result = await execute_clm(clm_code, input_data)
|
|
377
|
+
|
|
378
|
+
# Send response with same messageId
|
|
379
|
+
response = {{
|
|
380
|
+
"messageId": message_id,
|
|
381
|
+
**result
|
|
382
|
+
}}
|
|
383
|
+
await websocket.send(json.dumps(response))
|
|
384
|
+
|
|
385
|
+
elif msg_type == "ping":
|
|
386
|
+
# Respond to ping
|
|
387
|
+
await websocket.send(json.dumps({{
|
|
388
|
+
"messageId": message_id,
|
|
389
|
+
"type": "pong",
|
|
390
|
+
"timestamp": data.get("timestamp")
|
|
391
|
+
}}))
|
|
392
|
+
else:
|
|
393
|
+
# Echo unknown messages
|
|
394
|
+
response = json.dumps({{
|
|
395
|
+
"messageId": message_id,
|
|
396
|
+
"type": "echo",
|
|
397
|
+
"original": message,
|
|
398
|
+
"clients": len(connected_clients)
|
|
399
|
+
}})
|
|
400
|
+
await websocket.send(response)
|
|
401
|
+
|
|
402
|
+
except json.JSONDecodeError:
|
|
403
|
+
await websocket.send(json.dumps({{
|
|
404
|
+
"error": "Invalid JSON"
|
|
405
|
+
}}))
|
|
406
|
+
except Exception as e:
|
|
407
|
+
await websocket.send(json.dumps({{
|
|
408
|
+
"error": str(e)
|
|
409
|
+
}}))
|
|
410
|
+
|
|
411
|
+
except websockets.exceptions.ConnectionClosed:
|
|
412
|
+
pass
|
|
413
|
+
finally:
|
|
414
|
+
connected_clients.discard(websocket)
|
|
415
|
+
|
|
416
|
+
async def main():
|
|
417
|
+
async with websockets.serve(handler, "{host}", {port}):
|
|
418
|
+
print("WebSocket CLM Execution Server running on ws://{host}:{port}/")
|
|
419
|
+
print("Ready to handle CLM execution requests")
|
|
420
|
+
await asyncio.Future() # Run forever
|
|
421
|
+
|
|
422
|
+
if __name__ == "__main__":
|
|
423
|
+
try:
|
|
424
|
+
asyncio.run(main())
|
|
425
|
+
except KeyboardInterrupt:
|
|
426
|
+
pass
|
|
427
|
+
'''
|
|
428
|
+
|
|
429
|
+
# Start Python WebSocket server as background process
|
|
430
|
+
try:
|
|
431
|
+
process = subprocess.Popen(
|
|
432
|
+
[sys.executable, "-c", ws_server_code],
|
|
433
|
+
stdout=subprocess.DEVNULL,
|
|
434
|
+
stderr=subprocess.DEVNULL,
|
|
435
|
+
start_new_session=True,
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
# Save PID
|
|
439
|
+
with open(pid_file, "w") as f:
|
|
440
|
+
f.write(str(process.pid))
|
|
441
|
+
|
|
442
|
+
# Wait for server to start
|
|
443
|
+
time.sleep(WEBSOCKET_SERVER_START_WAIT_SECONDS)
|
|
444
|
+
|
|
445
|
+
if is_port_in_use(port):
|
|
446
|
+
return {
|
|
447
|
+
"success": True,
|
|
448
|
+
"message": "WebSocket server deployed successfully",
|
|
449
|
+
"pid": process.pid,
|
|
450
|
+
"port": port,
|
|
451
|
+
"url": f"ws://{host}:{port}/",
|
|
452
|
+
"status": "running",
|
|
453
|
+
}
|
|
454
|
+
else:
|
|
455
|
+
return {
|
|
456
|
+
"success": False,
|
|
457
|
+
"error": "Server started but not responding. Ensure 'websockets' package is installed: pip install websockets",
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
except (OSError, subprocess.SubprocessError, ValueError) as e:
|
|
461
|
+
return {"success": False, "error": f"Failed to start server: {e}"}
|
|
462
|
+
|
|
463
|
+
elif action == "status":
|
|
464
|
+
is_running = is_port_in_use(port)
|
|
465
|
+
pid = get_pid_on_port(port)
|
|
466
|
+
|
|
467
|
+
saved_pid = None
|
|
468
|
+
if os.path.isfile(pid_file):
|
|
469
|
+
try:
|
|
470
|
+
with open(pid_file) as f:
|
|
471
|
+
saved_pid = int(f.read().strip())
|
|
472
|
+
except (OSError, ValueError):
|
|
473
|
+
pass
|
|
474
|
+
|
|
475
|
+
return {
|
|
476
|
+
"success": True,
|
|
477
|
+
"running": is_running,
|
|
478
|
+
"pid": pid,
|
|
479
|
+
"saved_pid": saved_pid,
|
|
480
|
+
"port": port,
|
|
481
|
+
"url": f"ws://{host}:{port}/" if is_running else None,
|
|
482
|
+
"status": "running" if is_running else "stopped",
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
elif action == "stop":
|
|
486
|
+
pid = get_pid_on_port(port)
|
|
487
|
+
|
|
488
|
+
if not pid:
|
|
489
|
+
if os.path.isfile(pid_file):
|
|
490
|
+
os.remove(pid_file)
|
|
491
|
+
return {
|
|
492
|
+
"success": True,
|
|
493
|
+
"message": "No WebSocket server running on this port",
|
|
494
|
+
"port": port,
|
|
495
|
+
"status": "stopped",
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
try:
|
|
499
|
+
os.kill(pid, signal.SIGTERM)
|
|
500
|
+
time.sleep(PROCESS_TERMINATE_WAIT_SECONDS)
|
|
501
|
+
|
|
502
|
+
if is_port_in_use(port):
|
|
503
|
+
os.kill(pid, signal.SIGKILL)
|
|
504
|
+
time.sleep(PROCESS_TERMINATE_WAIT_SECONDS)
|
|
505
|
+
|
|
506
|
+
if os.path.isfile(pid_file):
|
|
507
|
+
os.remove(pid_file)
|
|
508
|
+
|
|
509
|
+
return {
|
|
510
|
+
"success": True,
|
|
511
|
+
"message": "WebSocket server stopped",
|
|
512
|
+
"pid": pid,
|
|
513
|
+
"port": port,
|
|
514
|
+
"status": "stopped",
|
|
515
|
+
}
|
|
516
|
+
except ProcessLookupError:
|
|
517
|
+
if os.path.isfile(pid_file):
|
|
518
|
+
os.remove(pid_file)
|
|
519
|
+
return {
|
|
520
|
+
"success": True,
|
|
521
|
+
"message": "WebSocket server was not running",
|
|
522
|
+
"port": port,
|
|
523
|
+
"status": "stopped",
|
|
524
|
+
}
|
|
525
|
+
except (OSError, subprocess.SubprocessError) as e:
|
|
526
|
+
return {"success": False, "error": f"Failed to stop server: {e}"}
|
|
527
|
+
|
|
528
|
+
else:
|
|
529
|
+
return {"success": False, "error": f"Unknown action: {action}"}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Baldwin Split Operator (DEL-388-02).
|
|
2
|
+
|
|
3
|
+
Decomposition Operator (÷): A -> A1 ⊗ A2.
|
|
4
|
+
Decomposes a compound MCard/PCard manifest into orthogonal components or
|
|
5
|
+
partitions input tokens across parallel branches, preserving triadic
|
|
6
|
+
syntactic closure (abstract_spec, concrete_impl, balanced_exp).
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import copy
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
import yaml
|
|
14
|
+
|
|
15
|
+
from clm.layer0.model.card import MCard
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _parse_manifest(target: MCard | str | dict[str, Any]) -> dict[str, Any]:
|
|
19
|
+
"""Extract dict manifest from target MCard or string."""
|
|
20
|
+
if isinstance(target, dict):
|
|
21
|
+
return copy.deepcopy(target)
|
|
22
|
+
if isinstance(target, MCard):
|
|
23
|
+
raw = target.get_content(as_text=True)
|
|
24
|
+
else:
|
|
25
|
+
raw = str(target)
|
|
26
|
+
try:
|
|
27
|
+
data = yaml.safe_load(raw)
|
|
28
|
+
if isinstance(data, dict):
|
|
29
|
+
return data
|
|
30
|
+
except Exception:
|
|
31
|
+
pass
|
|
32
|
+
return {
|
|
33
|
+
"abstract_spec": {"type": "raw_content", "payload": raw},
|
|
34
|
+
"concrete_impl": {"runtime": "python", "length": len(raw)},
|
|
35
|
+
"balanced_exp": {"invariants": ["noetherian_conservation"]},
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def ensure_triadic_closure(data: dict[str, Any], suffix: str = "") -> dict[str, Any]:
|
|
40
|
+
"""Guarantee that data contains abstract_spec, concrete_impl, and balanced_exp."""
|
|
41
|
+
res = copy.deepcopy(data)
|
|
42
|
+
if "abstract_spec" not in res:
|
|
43
|
+
res["abstract_spec"] = res.get("abstract", {
|
|
44
|
+
"type": f"pcard/abstract_{suffix}" if suffix else "pcard/abstract",
|
|
45
|
+
"interface": "universal",
|
|
46
|
+
})
|
|
47
|
+
if "concrete_impl" not in res:
|
|
48
|
+
res["concrete_impl"] = res.get("concrete", {
|
|
49
|
+
"type": f"pcard/concrete_{suffix}" if suffix else "pcard/concrete",
|
|
50
|
+
"runtime": "python",
|
|
51
|
+
})
|
|
52
|
+
if "balanced_exp" not in res:
|
|
53
|
+
res["balanced_exp"] = res.get("balanced", {
|
|
54
|
+
"type": f"pcard/balanced_{suffix}" if suffix else "pcard/balanced",
|
|
55
|
+
"invariants": ["noetherian_conservation"],
|
|
56
|
+
})
|
|
57
|
+
return res
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def op_split(impl: dict[str, Any], target: MCard, ctx: dict[str, Any]) -> dict[str, Any]:
|
|
61
|
+
"""Execute the Baldwin Split (÷) operation.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
impl: Configuration dict for split (e.g. split_keys, branch_a_keys, branch_b_keys).
|
|
65
|
+
target: Target MCard to decompose.
|
|
66
|
+
ctx: Execution context.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Dict containing decomposed branches, manifests, and closure proofs.
|
|
70
|
+
"""
|
|
71
|
+
manifest = _parse_manifest(target)
|
|
72
|
+
split_keys = impl.get("split_keys", ctx.get("split_keys", []))
|
|
73
|
+
branch_a_keys = impl.get("branch_a_keys", ctx.get("branch_a_keys", []))
|
|
74
|
+
branch_b_keys = impl.get("branch_b_keys", ctx.get("branch_b_keys", []))
|
|
75
|
+
|
|
76
|
+
if branch_a_keys or branch_b_keys:
|
|
77
|
+
part_a = {k: manifest[k] for k in branch_a_keys if k in manifest}
|
|
78
|
+
part_b = {k: manifest[k] for k in branch_b_keys if k in manifest}
|
|
79
|
+
elif split_keys:
|
|
80
|
+
part_a = {k: manifest[k] for k in split_keys if k in manifest}
|
|
81
|
+
part_b = {k: v for k, v in manifest.items() if k not in split_keys}
|
|
82
|
+
else:
|
|
83
|
+
# Default orthogonal partition
|
|
84
|
+
keys = list(manifest.keys())
|
|
85
|
+
mid = max(1, len(keys) // 2)
|
|
86
|
+
part_a = {k: manifest[k] for k in keys[:mid]}
|
|
87
|
+
part_b = {k: manifest[k] for k in keys[mid:]}
|
|
88
|
+
|
|
89
|
+
manifest_a = ensure_triadic_closure(part_a, "branch_a")
|
|
90
|
+
manifest_b = ensure_triadic_closure(part_b, "branch_b")
|
|
91
|
+
|
|
92
|
+
yaml_a = yaml.dump(manifest_a, default_flow_style=False)
|
|
93
|
+
yaml_b = yaml.dump(manifest_b, default_flow_style=False)
|
|
94
|
+
|
|
95
|
+
mcard_a = MCard(yaml_a)
|
|
96
|
+
mcard_b = MCard(yaml_b)
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
"success": True,
|
|
100
|
+
"operator": "split",
|
|
101
|
+
"syntactic_closure": True,
|
|
102
|
+
"branch_a": {
|
|
103
|
+
"manifest": manifest_a,
|
|
104
|
+
"hash": mcard_a.hash,
|
|
105
|
+
"content": yaml_a,
|
|
106
|
+
},
|
|
107
|
+
"branch_b": {
|
|
108
|
+
"manifest": manifest_b,
|
|
109
|
+
"hash": mcard_b.hash,
|
|
110
|
+
"content": yaml_b,
|
|
111
|
+
},
|
|
112
|
+
"mcard_a": mcard_a,
|
|
113
|
+
"mcard_b": mcard_b,
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
__all__ = ["op_split", "ensure_triadic_closure"]
|