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
clm/layer4/loader.py
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
"""
|
|
2
|
+
File loading and storage operations for MCard.
|
|
3
|
+
|
|
4
|
+
This module handles the high-level logic of processing files and storing them
|
|
5
|
+
into a CardCollection. It uses mcard.file_io for low-level file operations.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from clm.cross_cutting.config.settings import settings
|
|
13
|
+
from clm.layer0.model.card import MCard
|
|
14
|
+
from clm.layer0.model.handle import HandleValidationError
|
|
15
|
+
from clm.layer2 import file_io
|
|
16
|
+
from clm.layer2.card_collection import CardCollection
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def process_and_store_file(
|
|
22
|
+
file_path: str | Path,
|
|
23
|
+
collection: CardCollection,
|
|
24
|
+
*,
|
|
25
|
+
root_path: Path | None = None,
|
|
26
|
+
allow_problematic: bool = False,
|
|
27
|
+
max_bytes_on_problem: int | None = None,
|
|
28
|
+
metadata_only: bool = False,
|
|
29
|
+
) -> dict[str, Any] | None:
|
|
30
|
+
"""Process a file, create an MCard, and store it in the collection."""
|
|
31
|
+
# Convert to Path object for consistency
|
|
32
|
+
file_path = Path(file_path) if isinstance(file_path, str) else file_path
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
# Check if the file is problematic before processing
|
|
36
|
+
if file_io.is_problematic_file(file_path):
|
|
37
|
+
if not allow_problematic:
|
|
38
|
+
logger.warning(f"Skipping problematic file: {file_path}")
|
|
39
|
+
return None
|
|
40
|
+
# Resolve environment-configured parameters
|
|
41
|
+
if max_bytes_on_problem is None:
|
|
42
|
+
max_bytes_on_problem = settings.file_processing.max_problem_text_bytes
|
|
43
|
+
|
|
44
|
+
# Extensions known to naturally contain very long lines
|
|
45
|
+
known_long_exts = (
|
|
46
|
+
".json",
|
|
47
|
+
".jsonl",
|
|
48
|
+
".csv",
|
|
49
|
+
".xml",
|
|
50
|
+
".min.js",
|
|
51
|
+
".min.css",
|
|
52
|
+
".svg",
|
|
53
|
+
)
|
|
54
|
+
is_known_type = file_path.name.lower().endswith(known_long_exts)
|
|
55
|
+
wrap_width = (
|
|
56
|
+
settings.file_processing.wrap_width_known
|
|
57
|
+
if is_known_type
|
|
58
|
+
else settings.file_processing.wrap_width_default
|
|
59
|
+
)
|
|
60
|
+
# Process as safe text via streaming normalization
|
|
61
|
+
logger.warning(
|
|
62
|
+
f"Problematic file detected, processing as safe text with soft-wrap (cap {max_bytes_on_problem} bytes, wrap {wrap_width}): {file_path}"
|
|
63
|
+
)
|
|
64
|
+
try:
|
|
65
|
+
streamed = file_io.stream_read_normalized_text(
|
|
66
|
+
file_path, byte_cap=max_bytes_on_problem, wrap_width=wrap_width
|
|
67
|
+
)
|
|
68
|
+
text = streamed["text"]
|
|
69
|
+
file_info = {
|
|
70
|
+
"content": text,
|
|
71
|
+
"filename": Path(file_path).name,
|
|
72
|
+
"mime_type": "text/plain",
|
|
73
|
+
"extension": Path(file_path).suffix.lower(),
|
|
74
|
+
"is_binary": False,
|
|
75
|
+
"size": len(text),
|
|
76
|
+
"original_size": streamed["original_size"],
|
|
77
|
+
"original_sha256_prefix": streamed["original_sha256_prefix"],
|
|
78
|
+
"normalized": True,
|
|
79
|
+
"wrap_width": wrap_width,
|
|
80
|
+
}
|
|
81
|
+
except Exception as _e:
|
|
82
|
+
# Last-resort fallback: capped binary
|
|
83
|
+
logger.warning(
|
|
84
|
+
f"Safe text processing failed, falling back to capped binary ({max_bytes_on_problem} bytes): {file_path}",
|
|
85
|
+
exc_info=True,
|
|
86
|
+
)
|
|
87
|
+
file_info = file_io.process_file_content(
|
|
88
|
+
file_path,
|
|
89
|
+
force_binary=True,
|
|
90
|
+
allow_pathological=True,
|
|
91
|
+
max_bytes=max_bytes_on_problem,
|
|
92
|
+
)
|
|
93
|
+
else:
|
|
94
|
+
logger.info(f"PROCESSING FILE: {file_path}")
|
|
95
|
+
# Process the file
|
|
96
|
+
logger.info(f"Reading file content: {file_path}")
|
|
97
|
+
file_info = file_io.process_file_content(file_path)
|
|
98
|
+
if not file_info:
|
|
99
|
+
logger.warning(f"No file info returned for: {file_path}")
|
|
100
|
+
return None
|
|
101
|
+
logger.info(
|
|
102
|
+
f"File processed - Type: {file_info.get('mime_type')}, Size: {file_info.get('size')} bytes"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
mcard = None
|
|
106
|
+
# Optionally skip storing content for problematic files if metadata_only is requested
|
|
107
|
+
if metadata_only and file_io.is_problematic_file(file_path):
|
|
108
|
+
pass
|
|
109
|
+
else:
|
|
110
|
+
# Check for empty content (e.g., empty __init__.py files)
|
|
111
|
+
content = file_info.get("content")
|
|
112
|
+
if not content or (isinstance(content, bytes | str) and len(content) == 0):
|
|
113
|
+
logger.debug(
|
|
114
|
+
f"Skipping empty file: {file_path} (empty files cannot be stored as MCards)"
|
|
115
|
+
)
|
|
116
|
+
return {
|
|
117
|
+
"hash": None,
|
|
118
|
+
"content_type": file_info.get("mime_type"),
|
|
119
|
+
"is_binary": file_info.get("is_binary"),
|
|
120
|
+
"filename": file_info.get("filename"),
|
|
121
|
+
"size": 0,
|
|
122
|
+
"file_path": str(file_path),
|
|
123
|
+
"skipped": True,
|
|
124
|
+
"skip_reason": "empty_file",
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
# Create MCard with just the content bytes
|
|
128
|
+
try:
|
|
129
|
+
logger.info(f"Creating MCard for: {file_path}")
|
|
130
|
+
mcard = MCard(content=content)
|
|
131
|
+
logger.info(f"Created MCard with hash: {mcard.get_hash()}")
|
|
132
|
+
except ValueError as e:
|
|
133
|
+
if "empty" in str(e).lower():
|
|
134
|
+
logger.debug(f"Skipping file with empty content: {file_path}")
|
|
135
|
+
return None
|
|
136
|
+
logger.error(
|
|
137
|
+
f"Failed to create MCard for {file_path}: {e}", exc_info=True
|
|
138
|
+
)
|
|
139
|
+
return None
|
|
140
|
+
except Exception as _e:
|
|
141
|
+
logger.error(f"Failed to create MCard for {file_path}", exc_info=True)
|
|
142
|
+
return None
|
|
143
|
+
# Add to collection and Handle Registration
|
|
144
|
+
try:
|
|
145
|
+
logger.info(f"Adding MCard to collection for: {file_path}")
|
|
146
|
+
added_hash = collection.add(mcard)
|
|
147
|
+
|
|
148
|
+
# --- Handle Registration Logic ---
|
|
149
|
+
# 1. Try filename handle
|
|
150
|
+
handle = file_path.name
|
|
151
|
+
try:
|
|
152
|
+
collection.engine.register_handle(handle, added_hash)
|
|
153
|
+
logger.info(f"Registered handle '{handle}' for {file_path.name}")
|
|
154
|
+
except HandleValidationError:
|
|
155
|
+
# 1b. Filename is invalid as handle (e.g., starts with number)
|
|
156
|
+
# Generate alternative handle by prepending 'doc_'
|
|
157
|
+
alt_handle = f"doc_{handle}"
|
|
158
|
+
try:
|
|
159
|
+
collection.engine.register_handle(alt_handle, added_hash)
|
|
160
|
+
logger.info(
|
|
161
|
+
f"Registered alternative handle '{alt_handle}' for {file_path.name}"
|
|
162
|
+
)
|
|
163
|
+
except ValueError:
|
|
164
|
+
# Alternative handle also taken - skip handle registration
|
|
165
|
+
logger.debug(
|
|
166
|
+
f"Could not register handle for '{file_path.name}'. "
|
|
167
|
+
f"MCard stored with hash {added_hash[:8]}..."
|
|
168
|
+
)
|
|
169
|
+
except ValueError:
|
|
170
|
+
# 2. Name taken: Try relative path handle
|
|
171
|
+
if root_path:
|
|
172
|
+
try:
|
|
173
|
+
rel_path = str(file_path.relative_to(root_path))
|
|
174
|
+
collection.engine.register_handle(rel_path, added_hash)
|
|
175
|
+
logger.info(
|
|
176
|
+
f"Registered handle '{rel_path}' (fallback) for {file_path.name}"
|
|
177
|
+
)
|
|
178
|
+
except ValueError:
|
|
179
|
+
# Both name and path already registered - content still stored by hash
|
|
180
|
+
logger.debug(
|
|
181
|
+
f"Handle name '{handle}' already in use (common for files like README.md, LICENSE). "
|
|
182
|
+
f"MCard stored successfully with hash {added_hash[:8]}... (accessible by hash, not by handle)"
|
|
183
|
+
)
|
|
184
|
+
else:
|
|
185
|
+
logger.debug(
|
|
186
|
+
f"Handle name '{handle}' already in use. "
|
|
187
|
+
f"MCard stored successfully with hash {added_hash[:8]}... (accessible by hash, not by handle)"
|
|
188
|
+
)
|
|
189
|
+
# ---------------------------------
|
|
190
|
+
|
|
191
|
+
logger.info(f"Successfully added MCard to collection for: {file_path}")
|
|
192
|
+
except Exception as _e:
|
|
193
|
+
logger.error(
|
|
194
|
+
f"Failed to add MCard to collection for {file_path}",
|
|
195
|
+
exc_info=True,
|
|
196
|
+
)
|
|
197
|
+
return None
|
|
198
|
+
|
|
199
|
+
# Prepare and return processing info
|
|
200
|
+
result = {
|
|
201
|
+
"hash": mcard.get_hash() if mcard else None,
|
|
202
|
+
"content_type": file_info.get("mime_type"),
|
|
203
|
+
"is_binary": file_info.get("is_binary"),
|
|
204
|
+
"filename": file_info.get("filename"),
|
|
205
|
+
"size": file_info.get("size"),
|
|
206
|
+
"file_path": str(file_path),
|
|
207
|
+
}
|
|
208
|
+
# Surface original bytes metadata if present
|
|
209
|
+
if "original_size" in file_info:
|
|
210
|
+
result["original_size"] = file_info["original_size"]
|
|
211
|
+
if "original_sha256_prefix" in file_info:
|
|
212
|
+
result["original_sha256_prefix"] = file_info["original_sha256_prefix"]
|
|
213
|
+
if metadata_only and file_io.is_problematic_file(file_path):
|
|
214
|
+
result["metadata_only"] = True
|
|
215
|
+
|
|
216
|
+
logger.info(f"COMPLETED processing file: {file_path}")
|
|
217
|
+
return result
|
|
218
|
+
|
|
219
|
+
except (TimeoutError, ValueError) as e:
|
|
220
|
+
logger.warning(f"Skipping problematic file {file_path}: {e}")
|
|
221
|
+
return None
|
|
222
|
+
except Exception as _e:
|
|
223
|
+
logger.error(f"Error processing {file_path}", exc_info=True)
|
|
224
|
+
return None
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def load_file_to_collection(
|
|
228
|
+
path: str | Path,
|
|
229
|
+
collection: CardCollection,
|
|
230
|
+
recursive: bool = False,
|
|
231
|
+
include_problematic: bool = False,
|
|
232
|
+
max_bytes_on_problem: int = 2 * 1024 * 1024,
|
|
233
|
+
metadata_only: bool = False,
|
|
234
|
+
) -> dict[str, Any]:
|
|
235
|
+
"""
|
|
236
|
+
Load a file or directory of files into the specified collection.
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
Dict containing 'metrics' and 'results'.
|
|
240
|
+
"""
|
|
241
|
+
path = Path(path).resolve() if isinstance(path, str) else path.resolve()
|
|
242
|
+
results = []
|
|
243
|
+
|
|
244
|
+
# Identify root for relative path calculation
|
|
245
|
+
root_path = path if path.is_dir() else path.parent
|
|
246
|
+
|
|
247
|
+
file_paths = []
|
|
248
|
+
if path.is_file():
|
|
249
|
+
file_paths = [path]
|
|
250
|
+
elif path.is_dir():
|
|
251
|
+
file_paths = file_io.list_files(path, recursive=recursive)
|
|
252
|
+
else:
|
|
253
|
+
raise FileNotFoundError(f"Path '{path}' does not exist or is not accessible")
|
|
254
|
+
|
|
255
|
+
# Metrics Calculation
|
|
256
|
+
unique_dirs = set()
|
|
257
|
+
max_depth = 0
|
|
258
|
+
|
|
259
|
+
for fp in file_paths:
|
|
260
|
+
try:
|
|
261
|
+
rel = fp.relative_to(root_path)
|
|
262
|
+
unique_dirs.add(fp.parent)
|
|
263
|
+
# Depth is number of parts in relative path (including filename? No, just dirs usually)
|
|
264
|
+
# "levels of directories" -> usually depth of hierarchy.
|
|
265
|
+
# path parts: 'a/b/c.txt' -> ('a', 'b', 'c.txt') len=3. Depth=2 (folders).
|
|
266
|
+
# If at root: 'c.txt' -> ('c.txt') len=1. Depth=0.
|
|
267
|
+
current_depth = len(rel.parts) - 1
|
|
268
|
+
if current_depth > max_depth:
|
|
269
|
+
max_depth = current_depth
|
|
270
|
+
except ValueError:
|
|
271
|
+
pass
|
|
272
|
+
|
|
273
|
+
metrics = {
|
|
274
|
+
"files_count": len(file_paths),
|
|
275
|
+
"directories_count": (
|
|
276
|
+
len(unique_dirs) if path.is_dir() else 0
|
|
277
|
+
), # Only count subdirs inside? Or all involved?
|
|
278
|
+
# unique_dirs includes root's subdirs. If path is file, dirs is just parent (which is outside scope usually).
|
|
279
|
+
# Let's count dirs traversed.
|
|
280
|
+
"directory_levels": max_depth,
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
logger.info(f"About to process {len(file_paths)} files")
|
|
284
|
+
for i, file_path in enumerate(file_paths):
|
|
285
|
+
logger.info(f"Processing file {i + 1}/{len(file_paths)}: {file_path}")
|
|
286
|
+
result = process_and_store_file(
|
|
287
|
+
file_path,
|
|
288
|
+
collection,
|
|
289
|
+
root_path=root_path,
|
|
290
|
+
allow_problematic=include_problematic,
|
|
291
|
+
max_bytes_on_problem=max_bytes_on_problem,
|
|
292
|
+
metadata_only=metadata_only,
|
|
293
|
+
)
|
|
294
|
+
if result:
|
|
295
|
+
results.append(result)
|
|
296
|
+
logger.info(f"Completed file {i + 1}/{len(file_paths)}: {file_path}")
|
|
297
|
+
|
|
298
|
+
return {"metrics": metrics, "results": results}
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
# Register file loader with Layer 1 protocols (Dependency Inversion)
|
|
302
|
+
try:
|
|
303
|
+
from clm.layer1.protocols import register_file_loader
|
|
304
|
+
|
|
305
|
+
register_file_loader(load_file_to_collection)
|
|
306
|
+
except ImportError:
|
|
307
|
+
pass
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""CLI Subcommands for MCard Ingestion, Inspection, and Export (Sprint 392 / DEL-392-03).
|
|
2
|
+
|
|
3
|
+
Provides user-facing command-line operations:
|
|
4
|
+
- clm mcard ingest <path> [--handle <name>] [--author <did>]
|
|
5
|
+
- clm mcard inspect <uri> [--json]
|
|
6
|
+
- clm mcard export <uri> <output_path>
|
|
7
|
+
|
|
8
|
+
Acceptance Criteria:
|
|
9
|
+
- AC-392-03-01: clm mcard ingest exits 0 and prints canonical URI to stdout.
|
|
10
|
+
- AC-392-03-02: clm mcard inspect displays formatted YAML/JSON metadata and payload snippet.
|
|
11
|
+
- AC-392-03-03: clm mcard export round-trips binary/text content bit-for-bit.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import json
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
import yaml
|
|
21
|
+
|
|
22
|
+
from clm.cross_cutting.clm_logger import get_logger
|
|
23
|
+
from clm.layer2.ingest import export_mcard, get_default_fs, ingest_file
|
|
24
|
+
from clm.layer2.mcard_fs import MCardFileSystem
|
|
25
|
+
|
|
26
|
+
_logger = get_logger("clm.layer4.mcard_commands")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def mcard_ingest_cmd(
|
|
30
|
+
path: str,
|
|
31
|
+
handle: str | None = None,
|
|
32
|
+
author: str = "did:clm:user",
|
|
33
|
+
fs: MCardFileSystem | None = None,
|
|
34
|
+
) -> int:
|
|
35
|
+
"""Ingest local file into CAS and print canonical mcard:// URI."""
|
|
36
|
+
try:
|
|
37
|
+
mcard = ingest_file(path=path, handle=handle, author=author, fs=fs)
|
|
38
|
+
# Format canonical user URI
|
|
39
|
+
uri_str = mcard.uri
|
|
40
|
+
if uri_str.startswith("mcard:"):
|
|
41
|
+
rest = uri_str[len("mcard:"):].lstrip("/")
|
|
42
|
+
canonical_print = f"mcard://{rest}"
|
|
43
|
+
else:
|
|
44
|
+
canonical_print = uri_str
|
|
45
|
+
print(canonical_print)
|
|
46
|
+
return 0
|
|
47
|
+
except Exception as e:
|
|
48
|
+
_logger.error(f"Error ingesting MCard from '{path}': {e}")
|
|
49
|
+
return 1
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def mcard_inspect_cmd(
|
|
53
|
+
uri: str,
|
|
54
|
+
as_json: bool = False,
|
|
55
|
+
fs: MCardFileSystem | None = None,
|
|
56
|
+
) -> int:
|
|
57
|
+
"""Inspect metadata and payload snippet of content-addressed MCard."""
|
|
58
|
+
try:
|
|
59
|
+
target_fs = fs or get_default_fs()
|
|
60
|
+
clean_uri = uri
|
|
61
|
+
if clean_uri.startswith("mcard://"):
|
|
62
|
+
clean_uri = "mcard:" + clean_uri[len("mcard://"):]
|
|
63
|
+
mcard = target_fs.read_file(clean_uri)
|
|
64
|
+
|
|
65
|
+
# Prepare inspect report
|
|
66
|
+
summary: dict[str, Any] = {
|
|
67
|
+
"uri": uri,
|
|
68
|
+
"hash": str(mcard.hash),
|
|
69
|
+
"author": str(mcard.author),
|
|
70
|
+
"sequence": mcard.sequence,
|
|
71
|
+
"metadata": mcard.metadata,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# Payload snippet
|
|
75
|
+
if isinstance(mcard.payload, str):
|
|
76
|
+
if len(mcard.payload) > 500:
|
|
77
|
+
summary["payload_snippet"] = mcard.payload[:500] + "... [truncated]"
|
|
78
|
+
else:
|
|
79
|
+
summary["payload_snippet"] = mcard.payload
|
|
80
|
+
elif isinstance(mcard.payload, bytes):
|
|
81
|
+
summary["payload_snippet"] = f"<bytes length={len(mcard.payload)}>"
|
|
82
|
+
else:
|
|
83
|
+
summary["payload_snippet"] = mcard.payload
|
|
84
|
+
|
|
85
|
+
if as_json:
|
|
86
|
+
print(json.dumps(summary, indent=2, default=str))
|
|
87
|
+
else:
|
|
88
|
+
print(yaml.dump(summary, sort_keys=False))
|
|
89
|
+
return 0
|
|
90
|
+
except Exception as e:
|
|
91
|
+
_logger.error(f"Error inspecting MCard '{uri}': {e}")
|
|
92
|
+
return 1
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def mcard_export_cmd(
|
|
96
|
+
uri: str,
|
|
97
|
+
output_path: str,
|
|
98
|
+
fs: MCardFileSystem | None = None,
|
|
99
|
+
) -> int:
|
|
100
|
+
"""Export an MCard from CAS back to the local filesystem."""
|
|
101
|
+
try:
|
|
102
|
+
clean_uri = uri
|
|
103
|
+
if clean_uri.startswith("mcard://"):
|
|
104
|
+
clean_uri = "mcard:" + clean_uri[len("mcard://"):]
|
|
105
|
+
out_p = export_mcard(clean_uri, output_path, fs=fs)
|
|
106
|
+
print(f"Exported {uri} -> {out_p}")
|
|
107
|
+
return 0
|
|
108
|
+
except Exception as e:
|
|
109
|
+
_logger.error(f"Error exporting MCard '{uri}' to '{output_path}': {e}")
|
|
110
|
+
return 1
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def handle_mcard_cli(args: list[str], fs: MCardFileSystem | None = None) -> int:
|
|
114
|
+
"""CLI subparser dispatcher for 'clm mcard <subcommand>'."""
|
|
115
|
+
parser = argparse.ArgumentParser(
|
|
116
|
+
prog="clm mcard",
|
|
117
|
+
description="Universal MCard Ingestion and Inspection CLI",
|
|
118
|
+
)
|
|
119
|
+
subparsers = parser.add_subparsers(dest="subcommand", help="MCard operation")
|
|
120
|
+
|
|
121
|
+
# ingest
|
|
122
|
+
ingest_p = subparsers.add_parser("ingest", help="Ingest a file into universal MCard CAS")
|
|
123
|
+
ingest_p.add_argument("path", help="Path to local file")
|
|
124
|
+
ingest_p.add_argument("--handle", "-H", default=None, help="Optional handle name")
|
|
125
|
+
ingest_p.add_argument("--author", "-a", default="did:clm:user", help="Author DID")
|
|
126
|
+
|
|
127
|
+
# inspect
|
|
128
|
+
inspect_p = subparsers.add_parser("inspect", help="Inspect MCard metadata and content")
|
|
129
|
+
inspect_p.add_argument("uri", help="Canonical MCard URI (mcard://...)")
|
|
130
|
+
inspect_p.add_argument("--json", action="store_true", help="Output as JSON instead of YAML")
|
|
131
|
+
|
|
132
|
+
# export
|
|
133
|
+
export_p = subparsers.add_parser("export", help="Export an MCard from CAS to disk")
|
|
134
|
+
export_p.add_argument("uri", help="Canonical MCard URI (mcard://...)")
|
|
135
|
+
export_p.add_argument("output_path", help="Local destination file path")
|
|
136
|
+
|
|
137
|
+
# diff (DEL-396-01)
|
|
138
|
+
diff_p = subparsers.add_parser("diff", help="Compute semantic diff between two MCards")
|
|
139
|
+
diff_p.add_argument("uri1", help="First MCard URI")
|
|
140
|
+
diff_p.add_argument("uri2", help="Second MCard URI")
|
|
141
|
+
diff_p.add_argument("--json", action="store_true", help="Output diff as JSON")
|
|
142
|
+
|
|
143
|
+
# lineage (DEL-396-01)
|
|
144
|
+
lineage_p = subparsers.add_parser("lineage", help="Display causal provenance lineage of an MCard")
|
|
145
|
+
lineage_p.add_argument("uri", help="Canonical MCard URI")
|
|
146
|
+
lineage_p.add_argument("--mermaid", action="store_true", help="Output as Mermaid flowchart markdown")
|
|
147
|
+
lineage_p.add_argument("--dot", action="store_true", help="Output as Graphviz DOT")
|
|
148
|
+
lineage_p.add_argument("--json", action="store_true", help="Output as JSON graph")
|
|
149
|
+
|
|
150
|
+
parsed = parser.parse_args(args)
|
|
151
|
+
|
|
152
|
+
if parsed.subcommand == "ingest":
|
|
153
|
+
return mcard_ingest_cmd(parsed.path, handle=parsed.handle, author=parsed.author, fs=fs)
|
|
154
|
+
elif parsed.subcommand == "inspect":
|
|
155
|
+
return mcard_inspect_cmd(parsed.uri, as_json=parsed.json, fs=fs)
|
|
156
|
+
elif parsed.subcommand == "export":
|
|
157
|
+
return mcard_export_cmd(parsed.uri, parsed.output_path, fs=fs)
|
|
158
|
+
elif parsed.subcommand == "diff":
|
|
159
|
+
from clm.layer4.diff_commands import mcard_diff_cmd
|
|
160
|
+
return mcard_diff_cmd(parsed.uri1, parsed.uri2, as_json=parsed.json, fs=fs)
|
|
161
|
+
elif parsed.subcommand == "lineage":
|
|
162
|
+
from clm.layer4.diff_commands import mcard_lineage_cmd
|
|
163
|
+
return mcard_lineage_cmd(parsed.uri, as_mermaid=parsed.mermaid, as_dot=parsed.dot, as_json=parsed.json)
|
|
164
|
+
else:
|
|
165
|
+
parser.print_help()
|
|
166
|
+
return 1
|