clm-kernel 0.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (333) hide show
  1. clm/__init__.py +230 -0
  2. clm/__main__.py +25 -0
  3. clm/cross_cutting/__init__.py +50 -0
  4. clm/cross_cutting/_test_runner.py +36 -0
  5. clm/cross_cutting/async_utils.py +25 -0
  6. clm/cross_cutting/cel_builder.py +123 -0
  7. clm/cross_cutting/cel_converter.py +697 -0
  8. clm/cross_cutting/cel_engine.py +252 -0
  9. clm/cross_cutting/cel_ops.py +72 -0
  10. clm/cross_cutting/cel_ops_db.py +157 -0
  11. clm/cross_cutting/cel_ops_files.py +99 -0
  12. clm/cross_cutting/cel_ops_state.py +71 -0
  13. clm/cross_cutting/cel_ops_strings.py +64 -0
  14. clm/cross_cutting/cel_resolver.py +63 -0
  15. clm/cross_cutting/cel_slices.py +56 -0
  16. clm/cross_cutting/cel_store.py +330 -0
  17. clm/cross_cutting/clm_logger.py +140 -0
  18. clm/cross_cutting/config/__init__.py +155 -0
  19. clm/cross_cutting/config/config_constants.py +392 -0
  20. clm/cross_cutting/config/env_parameters.py +90 -0
  21. clm/cross_cutting/config/logging.py +378 -0
  22. clm/cross_cutting/config/settings.py +504 -0
  23. clm/cross_cutting/context.py +5 -0
  24. clm/cross_cutting/domain_models.py +55 -0
  25. clm/cross_cutting/domain_types.py +61 -0
  26. clm/cross_cutting/effects.py +67 -0
  27. clm/cross_cutting/errors.py +153 -0
  28. clm/cross_cutting/governance.py +331 -0
  29. clm/cross_cutting/guards.py +41 -0
  30. clm/cross_cutting/mcard.py +321 -0
  31. clm/cross_cutting/native.py +185 -0
  32. clm/cross_cutting/observability.py +289 -0
  33. clm/cross_cutting/pcard.py +434 -0
  34. clm/cross_cutting/registry.py +71 -0
  35. clm/cross_cutting/resources.py +84 -0
  36. clm/cross_cutting/secrets.py +44 -0
  37. clm/cross_cutting/subprocess_utils.py +118 -0
  38. clm/cross_cutting/telemetry.py +164 -0
  39. clm/cross_cutting/testing/__init__.py +8 -0
  40. clm/cross_cutting/testing/comparator.py +187 -0
  41. clm/cross_cutting/testing/datasets.py +87 -0
  42. clm/cross_cutting/timeutil.py +20 -0
  43. clm/cross_cutting/types/new_type_check.py +0 -0
  44. clm/cross_cutting/utils/__init__.py +1 -0
  45. clm/cross_cutting/utils/url_safety.py +50 -0
  46. clm/cross_cutting/vcard.py +560 -0
  47. clm/cross_cutting/writer.py +118 -0
  48. clm/layer0/README.md +50 -0
  49. clm/layer0/__init__.py +223 -0
  50. clm/layer0/algebra.py +60 -0
  51. clm/layer0/algebra_baldwin.py +133 -0
  52. clm/layer0/algebra_baldwin_generators.py +36 -0
  53. clm/layer0/algebra_equivalence.py +262 -0
  54. clm/layer0/algebra_ingest.py +306 -0
  55. clm/layer0/algebra_manifest_io.py +156 -0
  56. clm/layer0/assembler.py +481 -0
  57. clm/layer0/cel/__init__.py +23 -0
  58. clm/layer0/cel_eval.py +491 -0
  59. clm/layer0/codec.py +106 -0
  60. clm/layer0/context.py +122 -0
  61. clm/layer0/db.py +160 -0
  62. clm/layer0/exceptions.py +73 -0
  63. clm/layer0/fibration.py +106 -0
  64. clm/layer0/gates.py +50 -0
  65. clm/layer0/hash.py +80 -0
  66. clm/layer0/loader.py +826 -0
  67. clm/layer0/math_utils.py +163 -0
  68. clm/layer0/mcard.py +96 -0
  69. clm/layer0/model/__init__.py +0 -0
  70. clm/layer0/model/action.py +223 -0
  71. clm/layer0/model/card.py +280 -0
  72. clm/layer0/model/card_triad.py +427 -0
  73. clm/layer0/model/compatibility.py +60 -0
  74. clm/layer0/model/dictionary.py +46 -0
  75. clm/layer0/model/dots.py +331 -0
  76. clm/layer0/model/event_producer.py +88 -0
  77. clm/layer0/model/g_time.py +89 -0
  78. clm/layer0/model/handle.py +163 -0
  79. clm/layer0/model/hash/__init__.py +6 -0
  80. clm/layer0/model/hash/algorithms/__init__.py +3 -0
  81. clm/layer0/model/hash/algorithms/custom_hash.py +7 -0
  82. clm/layer0/model/hash/algorithms/local_sha256.py +178 -0
  83. clm/layer0/model/hash/constants.py +53 -0
  84. clm/layer0/model/hash/enums.py +60 -0
  85. clm/layer0/model/hash/validator.py +307 -0
  86. clm/layer0/model/interpreter.py +275 -0
  87. clm/layer0/model/pagination.py +45 -0
  88. clm/layer0/model/pcard.py +716 -0
  89. clm/layer0/model/schema.py +44 -0
  90. clm/layer0/model/utils/__init__.py +1 -0
  91. clm/layer0/model/utils/content_analyzer.py +152 -0
  92. clm/layer0/model/validators/__init__.py +1 -0
  93. clm/layer0/model/validators/base_validator.py +44 -0
  94. clm/layer0/model/validators/binary_validator.py +66 -0
  95. clm/layer0/model/validators/text_validator.py +82 -0
  96. clm/layer0/model/validators/validation_registry.py +49 -0
  97. clm/layer0/model/vcard.py +823 -0
  98. clm/layer0/model/vcard_ext/__init__.py +39 -0
  99. clm/layer0/model/vcard_ext/core.py +60 -0
  100. clm/layer0/model/vcard_ext/network.py +23 -0
  101. clm/layer0/model/vcard_ext/observability.py +63 -0
  102. clm/layer0/model/vcard_ext/storage.py +60 -0
  103. clm/layer0/model/vcard_ext/vendors.py +211 -0
  104. clm/layer0/model/vcard_sandwich.py +119 -0
  105. clm/layer0/model/vcard_vocabulary.py +463 -0
  106. clm/layer0/model/workflow.py +274 -0
  107. clm/layer0/narrative.py +431 -0
  108. clm/layer0/ontology/collection.py +125 -0
  109. clm/layer0/parser.py +429 -0
  110. clm/layer0/relations.py +73 -0
  111. clm/layer0/rule_evaluator.py +251 -0
  112. clm/layer0/schema/__init__.py +38 -0
  113. clm/layer0/schema/ddl.py +97 -0
  114. clm/layer0/templates/abstract.yaml +99 -0
  115. clm/layer0/templates/balanced.yaml +253 -0
  116. clm/layer0/templates/concrete.yaml +172 -0
  117. clm/layer0/type_registry.py +223 -0
  118. clm/layer0/types.py +204 -0
  119. clm/layer0/utils.py +886 -0
  120. clm/layer0/verifier_core.py +262 -0
  121. clm/layer1/__init__.py +31 -0
  122. clm/layer1/action_dispatcher.py +275 -0
  123. clm/layer1/adapter_registry.py +233 -0
  124. clm/layer1/algebra_baldwin_generators.py +192 -0
  125. clm/layer1/arrow_compiler.py +525 -0
  126. clm/layer1/batch_processor.py +152 -0
  127. clm/layer1/cel/__init__.py +27 -0
  128. clm/layer1/cel/builder.py +173 -0
  129. clm/layer1/engine.py +759 -0
  130. clm/layer1/engine_async.py +89 -0
  131. clm/layer1/eval_callback.py +49 -0
  132. clm/layer1/gate_composer.py +229 -0
  133. clm/layer1/hoare_sandwich.py +298 -0
  134. clm/layer1/identity_guard.py +348 -0
  135. clm/layer1/matrix.py +138 -0
  136. clm/layer1/neural_runtime.py +221 -0
  137. clm/layer1/operations/__init__.py +123 -0
  138. clm/layer1/operations/augment.py +156 -0
  139. clm/layer1/operations/base.py +109 -0
  140. clm/layer1/operations/builtins.py +338 -0
  141. clm/layer1/operations/dispatch.py +144 -0
  142. clm/layer1/operations/exclude.py +107 -0
  143. clm/layer1/operations/handle.py +197 -0
  144. clm/layer1/operations/identity.py +139 -0
  145. clm/layer1/operations/invert.py +66 -0
  146. clm/layer1/operations/loader.py +116 -0
  147. clm/layer1/operations/manipulators/__init__.py +23 -0
  148. clm/layer1/operations/manipulators/document.py +211 -0
  149. clm/layer1/operations/manipulators/media.py +134 -0
  150. clm/layer1/operations/manipulators/structured.py +159 -0
  151. clm/layer1/operations/manipulators/tabular.py +334 -0
  152. clm/layer1/operations/port.py +65 -0
  153. clm/layer1/operations/services.py +529 -0
  154. clm/layer1/operations/split.py +117 -0
  155. clm/layer1/operations/substitute.py +75 -0
  156. clm/layer1/petri.py +1052 -0
  157. clm/layer1/pocketflow_orchestrator.py +408 -0
  158. clm/layer1/pocketflow_scheduler.py +519 -0
  159. clm/layer1/protocols.py +197 -0
  160. clm/layer1/runtime_adapter.py +718 -0
  161. clm/layer1/runtimes/__init__.py +71 -0
  162. clm/layer1/runtimes/_generated_assertions.py +125 -0
  163. clm/layer1/runtimes/_generated_types.py +42 -0
  164. clm/layer1/runtimes/base.py +265 -0
  165. clm/layer1/runtimes/binary.py +101 -0
  166. clm/layer1/runtimes/factory.py +203 -0
  167. clm/layer1/runtimes/javascript.py +321 -0
  168. clm/layer1/runtimes/javascript_runtime.js +33 -0
  169. clm/layer1/runtimes/js_scripts/isomorphism.mjs +22 -0
  170. clm/layer1/runtimes/js_scripts/loader.mjs +52 -0
  171. clm/layer1/runtimes/lambda_calc.py +126 -0
  172. clm/layer1/runtimes/python.py +288 -0
  173. clm/layer1/runtimes/python_runtime.py +41 -0
  174. clm/layer1/runtimes/script.py +114 -0
  175. clm/layer1/sandbox.py +334 -0
  176. clm/layer1/savepoint_guard.py +136 -0
  177. clm/layer1/sparse_net.py +122 -0
  178. clm/layer1/spec_tester.py +165 -0
  179. clm/layer1/store.py +295 -0
  180. clm/layer1/test_runner.py +498 -0
  181. clm/layer1/type_inspector.py +166 -0
  182. clm/layer1/verifier.py +772 -0
  183. clm/layer1/vm.py +91 -0
  184. clm/layer2/__init__.py +78 -0
  185. clm/layer2/card_collection.py +678 -0
  186. clm/layer2/classifier.py +479 -0
  187. clm/layer2/engine/__init__.py +1 -0
  188. clm/layer2/engine/abstract_sql_engine.py +320 -0
  189. clm/layer2/engine/base.py +193 -0
  190. clm/layer2/engine/duckdb_engine.py +576 -0
  191. clm/layer2/engine/sqlite_engine.py +518 -0
  192. clm/layer2/fiber/__init__.py +31 -0
  193. clm/layer2/fiber/context.py +89 -0
  194. clm/layer2/fiber/disposable.py +163 -0
  195. clm/layer2/fiber/fiber.py +125 -0
  196. clm/layer2/fiber/hoare.py +191 -0
  197. clm/layer2/file_io.py +376 -0
  198. clm/layer2/file_registrar.py +77 -0
  199. clm/layer2/ingest.py +202 -0
  200. clm/layer2/lineage.py +322 -0
  201. clm/layer2/mcard_fs.py +262 -0
  202. clm/layer2/merkle.py +11 -0
  203. clm/layer2/mime.py +244 -0
  204. clm/layer2/mime_detector.py +245 -0
  205. clm/layer2/private_collection.py +118 -0
  206. clm/layer2/shared_store.py +11 -0
  207. clm/layer2/storage/__init__.py +190 -0
  208. clm/layer2/storage/db_bitemporal.py +141 -0
  209. clm/layer2/storage/db_driver.py +331 -0
  210. clm/layer2/storage/db_hyperlinks.py +214 -0
  211. clm/layer2/storage/db_pool.py +139 -0
  212. clm/layer2/storage/db_repository.py +229 -0
  213. clm/layer2/storage/savepoint.py +78 -0
  214. clm/layer2/storage/sqlite_wal.py +74 -0
  215. clm/layer2/tridb.py +245 -0
  216. clm/layer2/vcard_receipt.py +71 -0
  217. clm/layer2/vfs.py +17 -0
  218. clm/layer3/__init__.py +141 -0
  219. clm/layer3/agency/__init__.py +19 -0
  220. clm/layer3/agency/config.py +220 -0
  221. clm/layer3/agency/providers/__init__.py +15 -0
  222. clm/layer3/agency/providers/base.py +91 -0
  223. clm/layer3/agency/providers/mlc_llm.py +169 -0
  224. clm/layer3/agency/providers/ollama.py +203 -0
  225. clm/layer3/agency/router.py +71 -0
  226. clm/layer3/agency/runtime.py +524 -0
  227. clm/layer3/gateway.py +353 -0
  228. clm/layer3/open_interpreter_adapter.py +159 -0
  229. clm/layer3/rag/__init__.py +70 -0
  230. clm/layer3/rag/cli.py +228 -0
  231. clm/layer3/rag/config.py +153 -0
  232. clm/layer3/rag/embeddings/__init__.py +16 -0
  233. clm/layer3/rag/embeddings/base.py +79 -0
  234. clm/layer3/rag/embeddings/ollama.py +211 -0
  235. clm/layer3/rag/embeddings/vision.py +290 -0
  236. clm/layer3/rag/engine.py +276 -0
  237. clm/layer3/rag/graph/__init__.py +22 -0
  238. clm/layer3/rag/graph/community.py +198 -0
  239. clm/layer3/rag/graph/engine.py +433 -0
  240. clm/layer3/rag/graph/extractor.py +255 -0
  241. clm/layer3/rag/graph/schema.py +115 -0
  242. clm/layer3/rag/graph/store.py +641 -0
  243. clm/layer3/rag/indexer.py +333 -0
  244. clm/layer3/rag/llm_providers.json +52 -0
  245. clm/layer3/rag/semantic_versioning.py +313 -0
  246. clm/layer3/rag/vector/__init__.py +28 -0
  247. clm/layer3/rag/vector/handle_vector_store.py +749 -0
  248. clm/layer3/rag/vector/schema.py +151 -0
  249. clm/layer3/rag/vector/store.py +632 -0
  250. clm/layer3/reticulum/__init__.py +55 -0
  251. clm/layer3/reticulum/bridge.py +74 -0
  252. clm/layer3/reticulum/daemon.py +88 -0
  253. clm/layer3/reticulum/discovery.py +164 -0
  254. clm/layer3/reticulum/identity.py +107 -0
  255. clm/layer3/reticulum/media.py +148 -0
  256. clm/layer3/reticulum/transport.py +133 -0
  257. clm/layer3/satori/__init__.py +209 -0
  258. clm/layer3/satori/alpha_conversion.py +168 -0
  259. clm/layer3/satori/beta_reduction.py +359 -0
  260. clm/layer3/satori/continuation.py +38 -0
  261. clm/layer3/satori/eta_conversion.py +205 -0
  262. clm/layer3/satori/free_variables.py +163 -0
  263. clm/layer3/satori/io_effects.py +370 -0
  264. clm/layer3/satori/lambda_runtime.py +726 -0
  265. clm/layer3/satori/lambda_term.py +230 -0
  266. clm/layer3/satori/sheaf_audit.py +261 -0
  267. clm/layer3/satori/speech_act.py +279 -0
  268. clm/layer4/README.md +9 -0
  269. clm/layer4/__init__.py +67 -0
  270. clm/layer4/_sovereign_executor.py +580 -0
  271. clm/layer4/action_commands.py +719 -0
  272. clm/layer4/api.py +236 -0
  273. clm/layer4/bootstrap/__init__.py +13 -0
  274. clm/layer4/bootstrap/bootstrapper.py +118 -0
  275. clm/layer4/bootstrap/cleanup.py +44 -0
  276. clm/layer4/bootstrap/clm_hooks.py +49 -0
  277. clm/layer4/bootstrap/genesis.py +77 -0
  278. clm/layer4/bootstrap/injection.py +125 -0
  279. clm/layer4/cli.py +549 -0
  280. clm/layer4/collection_manager.py +553 -0
  281. clm/layer4/commands/__init__.py +81 -0
  282. clm/layer4/commands/action_cmd.py +42 -0
  283. clm/layer4/commands/check_cmd.py +82 -0
  284. clm/layer4/commands/envelope.py +73 -0
  285. clm/layer4/commands/evaluate_cmd.py +224 -0
  286. clm/layer4/commands/mcard_cmd.py +24 -0
  287. clm/layer4/commands/petri_cmd.py +92 -0
  288. clm/layer4/commands/tridb_cmd.py +319 -0
  289. clm/layer4/commands/version_cmd.py +27 -0
  290. clm/layer4/diff_commands.py +167 -0
  291. clm/layer4/eoa_sponsor.py +232 -0
  292. clm/layer4/events.py +69 -0
  293. clm/layer4/flux_gateway.py +133 -0
  294. clm/layer4/gateway.py +91 -0
  295. clm/layer4/loader.py +307 -0
  296. clm/layer4/mcard_commands.py +166 -0
  297. clm/layer4/membrane.py +273 -0
  298. clm/layer4/navigation.py +50 -0
  299. clm/layer4/protocol/protocol_entry_python.py +108 -0
  300. clm/layer4/protocol/python_sync.py +126 -0
  301. clm/layer4/runner.py +150 -0
  302. clm/layer4/satori.py +262 -0
  303. clm/layer4/server/python_sync_client.py +150 -0
  304. clm/layer4/server/python_sync_server.py +139 -0
  305. clm/layer4/server/service_api.py +315 -0
  306. clm/layer4/server/start_servers.py +107 -0
  307. clm/layer4/server/stop_servers.py +24 -0
  308. clm/layer4/simulated_optimization_fix.py +0 -0
  309. clm/layer4/storage.py +463 -0
  310. clm/layer4/tridb_admin.py +268 -0
  311. clm/layer4/websocket/test_ws_client.py +104 -0
  312. clm/layer4/websocket/test_ws_server.py +138 -0
  313. clm/layer4/websocket/ws_server_local.py +160 -0
  314. clm/layer5/__init__.py +51 -0
  315. clm/layer5/async_batch_writer.py +133 -0
  316. clm/layer5/learning.py +276 -0
  317. clm/layer5/telemetry_hook.py +107 -0
  318. clm/layer5/type_lattice.py +238 -0
  319. clm/py.typed +0 -0
  320. clm/schemas/__init__.py +652 -0
  321. clm/schemas/abstract_face_schema.yaml +77 -0
  322. clm/schemas/cel_schema.yaml +416 -0
  323. clm/schemas/lifecycle/__init__.py +45 -0
  324. clm/schemas/lifecycle/models.py +123 -0
  325. clm/schemas/lifecycle/service.py +364 -0
  326. clm/schemas/mcard_schema.sql +192 -0
  327. clm/schemas/mcard_vector_schema.sql +283 -0
  328. clm/schemas/mlp-context-v1.json +91 -0
  329. clm_kernel-0.0.0.dist-info/METADATA +121 -0
  330. clm_kernel-0.0.0.dist-info/RECORD +333 -0
  331. clm_kernel-0.0.0.dist-info/WHEEL +4 -0
  332. clm_kernel-0.0.0.dist-info/entry_points.txt +3 -0
  333. clm_kernel-0.0.0.dist-info/licenses/LICENSE +21 -0
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