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/layer2/file_io.py ADDED
@@ -0,0 +1,376 @@
1
+ """
2
+ File I/O operations for MCard.
3
+
4
+ This module handles safe file reading, streaming, and directory traversal.
5
+ It is designed to be robust against large files, binary content, and pathological text.
6
+ """
7
+
8
+ import hashlib
9
+ import logging
10
+ import threading
11
+ import time
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ from clm.cross_cutting.config.settings import settings
16
+ from clm.layer0.model.interpreter import ContentTypeInterpreter
17
+ from clm.layer0.model.utils.content_analyzer import (
18
+ _PATHOLOGICAL_NO_BREAK_MIN as READ_CHUNK_SIZE,
19
+ )
20
+ from clm.layer0.model.utils.content_analyzer import (
21
+ _PROBLEMATIC_MAX_KNOWN_TYPE as KNOWN_TYPE_SIZE_LIMIT,
22
+ )
23
+ from clm.layer0.model.utils.content_analyzer import (
24
+ _PROBLEMATIC_MIN_MULTI_POSITION as MULTI_POSITION_THRESHOLD,
25
+ )
26
+ from clm.layer0.model.utils.content_analyzer import (
27
+ _PROBLEMATIC_SAMPLE_SIZE as BINARY_CHECK_SAMPLE_SIZE,
28
+ )
29
+ from clm.layer0.model.utils.content_analyzer import ContentAnalyzer
30
+
31
+ # File I/O specific constants
32
+ MAX_FILE_SIZE = settings.file_processing.max_file_size # Configurable via environment
33
+ LARGE_FILE_THRESHOLD = 50 * 1024 # 50KB — above this, read in chunks
34
+ CONTENT_DETECTION_SAMPLE_SIZE = 1024 * 1024 # 1MB cap for content-type detection
35
+
36
+ logger = logging.getLogger(__name__)
37
+
38
+
39
+ def soft_wrap_long_lines(text: str, max_line_length: int = 1000) -> str:
40
+ """Insert newlines into very long single lines to prevent pathological processing.
41
+ This is a safe text normalization step for minified or long-line content.
42
+ """
43
+ if max_line_length <= 0:
44
+ return text
45
+ out_lines: list[str] = []
46
+ for line in text.splitlines() or [text]:
47
+ if len(line) <= max_line_length:
48
+ out_lines.append(line)
49
+ continue
50
+ # Chunk the long line
51
+ for i in range(0, len(line), max_line_length):
52
+ out_lines.append(line[i : i + max_line_length])
53
+ # Preserve trailing newline if present in input
54
+ result = "\n".join(out_lines)
55
+ return result
56
+
57
+
58
+ def stream_read_normalized_text(
59
+ file_path: Path, *, byte_cap: int, wrap_width: int
60
+ ) -> dict[str, Any]:
61
+ """Stream-read bytes up to byte_cap, decode with replacement, and insert soft wraps on the fly.
62
+ Returns dict with keys: text, original_size, original_sha256_prefix.
63
+ """
64
+ sha = hashlib.sha256()
65
+ total_size = 0
66
+ produced_chars: list[str] = []
67
+ current_len = 0
68
+ with open(file_path, "rb") as f:
69
+ remaining = byte_cap
70
+ while remaining > 0:
71
+ chunk = f.read(min(READ_CHUNK_SIZE, remaining))
72
+ if not chunk:
73
+ break
74
+ sha.update(chunk)
75
+ total_size += len(chunk)
76
+ remaining -= len(chunk)
77
+ try:
78
+ s = chunk.decode("utf-8", errors="replace")
79
+ except UnicodeDecodeError:
80
+ s = chunk.decode("latin-1", errors="replace")
81
+ for ch in s:
82
+ if ch == "\r":
83
+ # Normalize CR to nothing; let CRLF become just LF via the subsequent '\n'
84
+ continue
85
+ produced_chars.append(ch)
86
+ if ch == "\n":
87
+ current_len = 0
88
+ else:
89
+ current_len += 1
90
+ if wrap_width > 0 and current_len >= wrap_width:
91
+ produced_chars.append("\n")
92
+ current_len = 0
93
+ return {
94
+ "text": "".join(produced_chars),
95
+ "original_size": total_size,
96
+ "original_sha256_prefix": sha.hexdigest()[:16],
97
+ }
98
+
99
+
100
+ def read_file_safely(
101
+ file_path: str | Path,
102
+ allow_pathological: bool = False,
103
+ max_bytes: int | None = None,
104
+ ) -> bytes:
105
+ """
106
+ Read file content with timeout protection and size limits.
107
+
108
+ Args:
109
+ file_path: Path to the file to read
110
+ allow_pathological: If True, bypass long-line/pathological content checks
111
+ max_bytes: If set, cap the number of bytes read to this value
112
+
113
+ Returns:
114
+ File content as bytes
115
+
116
+ Raises:
117
+ TimeoutError: If reading takes too long
118
+ IOError: If file cannot be read
119
+ """
120
+ file_path = Path(file_path)
121
+
122
+ # Check file size first
123
+ try:
124
+ file_size = file_path.stat().st_size
125
+ except OSError as e:
126
+ raise OSError(f"Cannot access file {file_path}: {e}") from e
127
+
128
+ if file_size > MAX_FILE_SIZE:
129
+ raise OSError(f"File too large: {file_size} bytes (max {MAX_FILE_SIZE})")
130
+
131
+ # Timeout configurable with a small bias for larger files
132
+ base_timeout = settings.file_processing.read_timeout_secs
133
+ timeout_seconds = (
134
+ 5.0 if file_size > LARGE_FILE_THRESHOLD and base_timeout >= 5 else base_timeout
135
+ )
136
+
137
+ result = [None]
138
+ exception = [None]
139
+
140
+ def read_with_timeout():
141
+ try:
142
+ with open(file_path, "rb") as file:
143
+ # For potentially problematic files, read in chunks with progress checking
144
+ if (
145
+ file_size > LARGE_FILE_THRESHOLD or max_bytes is not None
146
+ ): # Files > 50KB or when capping
147
+ content = bytearray()
148
+ chunk_size = READ_CHUNK_SIZE
149
+ bytes_read = 0
150
+ start_time = time.time()
151
+
152
+ to_read = max_bytes if max_bytes is not None else file_size
153
+
154
+ while bytes_read < to_read:
155
+ if time.time() - start_time > timeout_seconds:
156
+ raise TimeoutError(
157
+ f"Reading timeout exceeded for {file_path}"
158
+ )
159
+
160
+ remaining = to_read - bytes_read
161
+ chunk = file.read(min(chunk_size, remaining))
162
+ if not chunk:
163
+ break
164
+ content.extend(chunk)
165
+ bytes_read += len(chunk)
166
+
167
+ # Check for pathological content patterns during reading unless allowed
168
+ if (
169
+ (not allow_pathological)
170
+ and len(content) > 32768
171
+ and b"\n" not in content
172
+ and b"\r" not in content
173
+ ):
174
+ raise OSError(
175
+ f"File appears to be a single massive line: {file_path}"
176
+ )
177
+
178
+ result[0] = bytes(content)
179
+ else:
180
+ result[0] = file.read()
181
+ except Exception as e:
182
+ exception[0] = e
183
+
184
+ # Use threading for timeout since signal doesn't work well on all platforms
185
+ thread = threading.Thread(target=read_with_timeout)
186
+ thread.daemon = True
187
+ thread.start()
188
+ thread.join(timeout=timeout_seconds)
189
+
190
+ if thread.is_alive():
191
+ # Thread is still running, reading is taking too long
192
+ logger.warning(f"File reading timed out after {timeout_seconds}s: {file_path}")
193
+ raise TimeoutError(f"File reading timed out for {file_path}")
194
+
195
+ if exception[0]:
196
+ logger.error(f"Error reading file {file_path}: {exception[0]}")
197
+ raise exception[0]
198
+
199
+ if result[0] is None:
200
+ raise OSError(f"Failed to read file: {file_path}")
201
+
202
+ return result[0]
203
+
204
+
205
+ def is_problematic_file(file_path: Path) -> bool:
206
+ """
207
+ Check if a file is likely to cause processing issues.
208
+ """
209
+ try:
210
+ file_size = file_path.stat().st_size
211
+
212
+ # Skip empty files
213
+ if file_size == 0:
214
+ return False
215
+
216
+ # If it's a known type that can have long lines, be more permissive
217
+ file_extension = file_path.suffix.lower()
218
+ is_known_type = ContentAnalyzer.is_known_long_line_extension(file_extension)
219
+
220
+ # For known types, still have size limits to prevent pathological cases
221
+ if (
222
+ is_known_type and file_size > KNOWN_TYPE_SIZE_LIMIT
223
+ ): # 1MB limit for known types
224
+ logger.warning(
225
+ f"Skipping large file of known type: {file_path} ({file_size} bytes)"
226
+ )
227
+ return True
228
+
229
+ with open(file_path, "rb") as f:
230
+ # Sample from beginning, middle, and end for large files
231
+ sample_size = min(BINARY_CHECK_SAMPLE_SIZE, file_size) # 32KB sample
232
+ samples = []
233
+
234
+ if (
235
+ file_size > MULTI_POSITION_THRESHOLD
236
+ ): # For files > 100KB, check multiple positions
237
+ positions = [0, file_size // 2, max(0, file_size - sample_size)]
238
+ else:
239
+ positions = [0]
240
+
241
+ for pos in positions:
242
+ f.seek(pos)
243
+ sample = f.read(sample_size)
244
+ if sample:
245
+ samples.append(sample)
246
+
247
+ for sample in samples:
248
+ # Check for binary content that's not structured
249
+ if ContentAnalyzer.is_unstructured_binary(sample):
250
+ logger.warning(f"Skipping unstructured binary file: {file_path}")
251
+ return True
252
+
253
+ # Check for pathological line patterns
254
+ if ContentAnalyzer.has_pathological_lines(sample, is_known_type):
255
+ logger.warning(
256
+ f"Skipping file with pathological line structure: {file_path}"
257
+ )
258
+ return True
259
+
260
+ return False
261
+
262
+ except OSError as e:
263
+ logger.warning(f"Error checking file {file_path}: {e}")
264
+ return True # Skip files we can't check
265
+
266
+
267
+ def list_files(directory: str | Path, recursive: bool = False) -> list[Path]:
268
+ """
269
+ Load all files from the specified directory.
270
+ Explicitly skips hidden files and directories (starting with .).
271
+ """
272
+ import os
273
+
274
+ dir_path = Path(directory) if isinstance(directory, str) else directory
275
+ if not dir_path.exists():
276
+ raise FileNotFoundError(f"Directory '{dir_path}' does not exist.")
277
+
278
+ safe_files = []
279
+
280
+ if recursive:
281
+ for root, dirs, filenames in os.walk(str(dir_path)):
282
+ # Modify dirs in-place to skip hidden directories and common massive dependencies
283
+ dirs[:] = [
284
+ d
285
+ for d in dirs
286
+ if not d.startswith(".")
287
+ and d
288
+ not in {
289
+ "node_modules",
290
+ "__pycache__",
291
+ "venv",
292
+ "env",
293
+ "build",
294
+ "dist",
295
+ "target",
296
+ "mcard-studio-worktree",
297
+ }
298
+ ]
299
+
300
+ for filename in filenames:
301
+ if filename.startswith("."):
302
+ continue
303
+
304
+ file_path = Path(root) / filename
305
+ if not is_problematic_file(file_path):
306
+ safe_files.append(file_path)
307
+ else:
308
+ # Non-recursive: just list top level
309
+ # os.walk matches this if we break, but simpler usually:
310
+ for item in dir_path.iterdir():
311
+ if item.name.startswith("."):
312
+ continue
313
+ if item.is_file():
314
+ if not is_problematic_file(item):
315
+ safe_files.append(item)
316
+
317
+ logger.info(f"Found {len(safe_files)} safe files in {dir_path}")
318
+ return safe_files
319
+
320
+
321
+ def process_file_content(
322
+ file_path: str | Path,
323
+ *,
324
+ force_binary: bool = False,
325
+ allow_pathological: bool = False,
326
+ max_bytes: int | None = None,
327
+ ) -> dict[str, Any]:
328
+ """
329
+ Process a file and return its metadata and content.
330
+ """
331
+ # Read file content as bytes
332
+ content = read_file_safely(
333
+ file_path, allow_pathological=allow_pathological, max_bytes=max_bytes
334
+ )
335
+
336
+ # Analyze the content to get MIME type and other metadata
337
+ # For very large files or files with extremely long lines, use a sample
338
+ content_sample = content
339
+ if (
340
+ len(content) > CONTENT_DETECTION_SAMPLE_SIZE
341
+ ): # 1MB limit for content type detection
342
+ content_sample = content[:CONTENT_DETECTION_SAMPLE_SIZE]
343
+ logger.info(f"Using content sample for large file: {file_path}")
344
+
345
+ mime_type, extension = ContentTypeInterpreter.detect_content_type(content_sample)
346
+ is_binary = ContentTypeInterpreter.is_binary_content(content_sample)
347
+
348
+ # Force binary mode if requested
349
+ if force_binary:
350
+ is_binary = True
351
+ # If detection thought it's text, override to a safe default
352
+ if not mime_type or mime_type.startswith("text/"):
353
+ mime_type = "application/octet-stream"
354
+
355
+ # For text files, try to decode the content
356
+ if not is_binary and mime_type.startswith("text/"):
357
+ try:
358
+ # Decode the content as UTF-8 for text files
359
+ content = content.decode("utf-8")
360
+ except UnicodeDecodeError:
361
+ # If UTF-8 fails, try with error replacement
362
+ try:
363
+ content = content.decode("utf-8", errors="replace")
364
+ except UnicodeDecodeError as e:
365
+ logger.warning(
366
+ f"Failed to decode content for {file_path} as UTF-8: {e}"
367
+ )
368
+
369
+ return {
370
+ "content": content,
371
+ "filename": Path(file_path).name,
372
+ "mime_type": mime_type,
373
+ "extension": extension,
374
+ "is_binary": is_binary,
375
+ "size": len(content), # Use original content size, not sample
376
+ }
@@ -0,0 +1,77 @@
1
+ """CLM File Registrar — File-to-MCard registration in the Tri-Database.
2
+
3
+ Registers CLM manifest files as MCards, including AST normalization,
4
+ classification, and dialect metadata handles.
5
+ """
6
+
7
+ import json
8
+ import os
9
+
10
+ from clm.cross_cutting.clm_logger import get_logger
11
+ from clm.layer0.utils import clm_hash_string, resolve_clm_path
12
+ from clm.layer1.verifier import get_load_manifest
13
+ from clm.layer2.storage import CLMDB, CLMRepository
14
+
15
+ from .classifier import classify_clm, detect_dialect
16
+
17
+ _logger = get_logger("clm.layer2.file_registrar")
18
+
19
+
20
+ def register_file_mcard_py(clm_root: str, clm_file: str) -> None:
21
+ """
22
+ Registers a CLM manifest file as an MCard in the Tri-Database.
23
+
24
+ Args:
25
+ clm_root (str): The root directory of the CLM workspace.
26
+ clm_file (str): The CLM file to register.
27
+ """
28
+ load_normalized_manifest_fn = get_load_manifest()
29
+ clm_file = resolve_clm_path(clm_root, clm_file)
30
+ db = CLMDB(clm_root)
31
+ repo = CLMRepository(db, clm_root)
32
+ rel_path = os.path.relpath(clm_file, clm_root)
33
+ rel_path = rel_path.replace('\\', '/')
34
+ handle_name = f"file:{rel_path}"
35
+
36
+ with open(clm_file, encoding="utf-8") as f:
37
+ content = f.read()
38
+ norm_content = content.replace("\r\n", "\n").replace("\r", "\n").rstrip("\n")
39
+ content_hash = clm_hash_string(norm_content)
40
+
41
+ repo.register_handle(handle_name, norm_content)
42
+
43
+ json_ast = ""
44
+ try:
45
+ parsed = load_normalized_manifest_fn(clm_root, clm_file)
46
+ json_ast = json.dumps(parsed)
47
+ except Exception as e:
48
+ _logger.warning(f"[register_file_mcard_py] Warning: manifest normalization failed: {e}")
49
+
50
+ if json_ast:
51
+ classification = "Number"
52
+ try:
53
+ parsed_data = json.loads(json_ast)
54
+ classification = classify_clm(parsed_data)
55
+ except Exception as e:
56
+ _logger.warning(f"[register_file_mcard_py] Warning: classification failed: {e}")
57
+
58
+ dialect = "UNKNOWN"
59
+ if classification == "Function":
60
+ try:
61
+ parsed_data = json.loads(json_ast)
62
+ dialect = detect_dialect(parsed_data, classification)
63
+ except Exception as e:
64
+ _logger.warning(f"[register_file_mcard_py] Warning: dialect detection failed: {e}")
65
+
66
+ for key, val in [("ast", json_ast), ("classification", classification), ("dialect", dialect)]:
67
+ h_name = f"{key}:{content_hash}"
68
+ repo.register_handle(h_name, val)
69
+
70
+
71
+ # Register file registrar hook with Layer 1 protocols (Dependency Inversion)
72
+ try:
73
+ from clm.layer1.protocols import register_file_registrar_hook
74
+
75
+ register_file_registrar_hook(register_file_mcard_py)
76
+ except ImportError:
77
+ pass
clm/layer2/ingest.py ADDED
@@ -0,0 +1,202 @@
1
+ """Universal MCard Ingestion Membrane (Sprint 392 / DEL-392-02).
2
+
3
+ Provides universal ingestion of local files and raw byte streams into the
4
+ content-addressed MCard virtual filesystem (mcard://), with automatic MIME detection,
5
+ structural metadata extraction, and Zero-FS in-memory storage support.
6
+
7
+ Acceptance Criteria:
8
+ - AC-392-02-01: Ingesting any supported file returns valid MCard with deterministic mcard://<blake3_hash>.
9
+ - AC-392-02-02: Automatic MIME detection without relying solely on file extensions.
10
+ - AC-392-02-03: Structural metadata stored in card metadata.
11
+ - AC-392-02-04: Zero-FS support (in-memory SQLite storage when CLM_ZERO_FS=true).
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import base64
17
+ import os
18
+ import sqlite3
19
+ from pathlib import Path
20
+
21
+ from clm.cross_cutting.domain_models import MCard
22
+ from clm.layer2.mcard_fs import MCardFileSystem
23
+ from clm.layer2.mime_detector import detect_mime_and_format, extract_structural_metadata
24
+
25
+ _SHARED_MEMORY_CONN: sqlite3.Connection | None = None
26
+ _DEFAULT_FS: MCardFileSystem | None = None
27
+
28
+
29
+ def get_default_fs() -> MCardFileSystem:
30
+ """Obtain the default MCardFileSystem instance for the current process.
31
+
32
+ Under CLM_ZERO_FS=true or default test mode, uses an in-memory SQLite instance.
33
+ """
34
+ global _SHARED_MEMORY_CONN, _DEFAULT_FS
35
+ if _DEFAULT_FS is not None:
36
+ return _DEFAULT_FS
37
+
38
+ # Check Zero-FS flag
39
+ zero_fs = os.environ.get("CLM_ZERO_FS", "true").lower() in ("true", "1", "yes")
40
+
41
+ if zero_fs:
42
+ if _SHARED_MEMORY_CONN is None:
43
+ _SHARED_MEMORY_CONN = sqlite3.connect(":memory:", check_same_thread=False)
44
+ _DEFAULT_FS = MCardFileSystem(conn=_SHARED_MEMORY_CONN)
45
+ else:
46
+ clm_root = os.environ.get("CLM_ROOT") or os.getcwd()
47
+ db_path = Path(clm_root) / "computable_knowledge" / "content_memory.db"
48
+ db_path.parent.mkdir(parents=True, exist_ok=True)
49
+ conn = sqlite3.connect(str(db_path), check_same_thread=False)
50
+ _DEFAULT_FS = MCardFileSystem(conn=conn)
51
+
52
+ return _DEFAULT_FS
53
+
54
+
55
+ def reset_default_fs() -> None:
56
+ """Reset shared default filesystem (primarily for test isolation)."""
57
+ global _SHARED_MEMORY_CONN, _DEFAULT_FS
58
+ if _SHARED_MEMORY_CONN is not None:
59
+ try:
60
+ _SHARED_MEMORY_CONN.close()
61
+ except Exception:
62
+ pass
63
+ _SHARED_MEMORY_CONN = None
64
+ _DEFAULT_FS = None
65
+
66
+
67
+ def ingest_bytes(
68
+ data: bytes,
69
+ format: str | None = None,
70
+ handle: str | None = None,
71
+ author: str = "did:clm:user",
72
+ fs: MCardFileSystem | None = None,
73
+ filename: str | None = None,
74
+ ) -> MCard:
75
+ """Ingest raw bytes into the MCard Virtual File System.
76
+
77
+ Args:
78
+ data: Raw byte payload.
79
+ format: Optional explicit format override. If None, sniffed automatically.
80
+ handle: Optional human-readable handle (e.g. 'customers.csv' or 'mcard://my_dataset').
81
+ author: DID string of the authoring entity.
82
+ fs: Optional MCardFileSystem instance. If None, uses default fs.
83
+ filename: Optional source filename to assist MIME sniffing.
84
+
85
+ Returns:
86
+ MCard instance registered in CAS.
87
+ """
88
+ target_fs = fs or get_default_fs()
89
+
90
+ # 1. Detect MIME and format
91
+ sniffed_mime, sniffed_format = detect_mime_and_format(data, filename=filename)
92
+ chosen_format = format or sniffed_format
93
+
94
+ # 2. Extract structural metadata
95
+ metadata = extract_structural_metadata(data, sniffed_mime, chosen_format)
96
+ if filename:
97
+ metadata["source_filename"] = os.path.basename(filename)
98
+
99
+ # 3. Format payload for canonical storage
100
+ is_textual = chosen_format in ("csv", "json", "yaml", "markdown", "text")
101
+ if is_textual:
102
+ try:
103
+ payload = data.decode("utf-8")
104
+ metadata["encoding"] = "utf-8"
105
+ except UnicodeDecodeError:
106
+ payload = base64.b64encode(data).decode("ascii")
107
+ metadata["encoding"] = "base64"
108
+ else:
109
+ payload = base64.b64encode(data).decode("ascii")
110
+ metadata["encoding"] = "base64"
111
+
112
+ # 4. Compute deterministic BLAKE3 hash of content
113
+ from clm.layer0.model.card_triad import compute_canonical_hash
114
+ digest = compute_canonical_hash(payload)
115
+
116
+ # 5. Resolve canonical URI
117
+ if handle:
118
+ clean_handle = handle.removeprefix("mcard://").removeprefix("mcard:").strip("/")
119
+ uri = f"mcard:{clean_handle}"
120
+ else:
121
+ uri = f"mcard:{digest}"
122
+
123
+ # 6. Store in filesystem
124
+ mcard = target_fs.write_file(
125
+ uri=uri,
126
+ payload=payload,
127
+ author=author,
128
+ metadata=metadata,
129
+ )
130
+ return mcard
131
+
132
+
133
+ def ingest_file(
134
+ path: str | Path,
135
+ handle: str | None = None,
136
+ author: str = "did:clm:user",
137
+ format: str | None = None,
138
+ fs: MCardFileSystem | None = None,
139
+ ) -> MCard:
140
+ """Ingest a local filesystem file into the MCard Virtual File System.
141
+
142
+ Args:
143
+ path: Path to the local file.
144
+ handle: Optional handle name. If None, defaults to file's basename or hash.
145
+ author: DID string of the ingesting agent.
146
+ format: Optional format override.
147
+ fs: Optional MCardFileSystem instance.
148
+
149
+ Returns:
150
+ MCard instance registered in CAS.
151
+ """
152
+ file_path = Path(path).resolve()
153
+ if not file_path.is_file():
154
+ raise FileNotFoundError(f"File not found for ingestion: {path}")
155
+
156
+ data = file_path.read_bytes()
157
+ default_handle = handle or file_path.name
158
+
159
+ return ingest_bytes(
160
+ data=data,
161
+ format=format,
162
+ handle=default_handle,
163
+ author=author,
164
+ fs=fs,
165
+ filename=str(file_path),
166
+ )
167
+
168
+
169
+ def export_mcard(
170
+ uri: str,
171
+ output_path: str | Path,
172
+ fs: MCardFileSystem | None = None,
173
+ ) -> Path:
174
+ """Export an MCard from CAS back to the local host filesystem.
175
+
176
+ Args:
177
+ uri: Handle or URI of the MCard (e.g. 'mcard://<hash>' or 'mcard:<handle>').
178
+ output_path: Local filesystem destination path.
179
+ fs: Optional MCardFileSystem instance.
180
+
181
+ Returns:
182
+ Path of written file.
183
+ """
184
+ target_fs = fs or get_default_fs()
185
+ mcard = target_fs.read_file(uri)
186
+
187
+ out_p = Path(output_path).resolve()
188
+ out_p.parent.mkdir(parents=True, exist_ok=True)
189
+
190
+ encoding = mcard.metadata.get("encoding", "utf-8")
191
+ if encoding == "base64" and isinstance(mcard.payload, str):
192
+ raw_bytes = base64.b64decode(mcard.payload)
193
+ out_p.write_bytes(raw_bytes)
194
+ elif isinstance(mcard.payload, str):
195
+ out_p.write_text(mcard.payload, encoding="utf-8")
196
+ elif isinstance(mcard.payload, bytes):
197
+ out_p.write_bytes(mcard.payload)
198
+ else:
199
+ import json
200
+ out_p.write_text(json.dumps(mcard.payload, indent=2), encoding="utf-8")
201
+
202
+ return out_p