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
@@ -0,0 +1,678 @@
1
+ """Card collection module for MCard."""
2
+
3
+ import asyncio
4
+ import json
5
+ import logging
6
+ import os
7
+ import sqlite3
8
+ import sys
9
+ from collections.abc import Callable
10
+ from pathlib import Path
11
+ from typing import TYPE_CHECKING, Any, TypeVar
12
+
13
+ if TYPE_CHECKING:
14
+ from clm.layer0.narrative import Maybe
15
+
16
+ from clm.cross_cutting.config.settings import settings
17
+ from clm.layer0.model.card import MCard
18
+ from clm.layer0.model.event_producer import generate_collision_event
19
+ from clm.layer0.model.pagination import Page
20
+ from clm.layer2.engine.sqlite_engine import SQLiteConnection, SQLiteEngine
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+ # Initialize environment parameters (Removed - using settings singleton)
25
+
26
+ # Database paths are imported from config_constants
27
+
28
+ # Type variable for generic type hints
29
+ T = TypeVar("T")
30
+
31
+ # Type variable for MCard instances
32
+ MCardT = TypeVar("MCardT", bound="MCard")
33
+
34
+
35
+ class CardCollection:
36
+ """High-level interface for card collection operations.
37
+
38
+ DOTS Vocabulary Role: Carrier Category Car(S)
39
+ =============================================
40
+ CardCollection represents the **Carrier Category** in the Double Operadic Theory of Systems.
41
+ - Objects: Individual MCards (data artifacts).
42
+ - Morphisms: Hash references between MCards (content-addressable links).
43
+ - Composition: Morphisms compose via hash chains (Merkle-DAG structure).
44
+
45
+ The Action: Loose(I) ⊛ Car(S) → Car(S)
46
+ --------------------------------------
47
+ The **Action** is how interactions act on systems to produce new systems:
48
+ - CLM/PCard (Chart/Lens) defines the interaction pattern.
49
+ - CardCollection.add() produces a new MCard in the Carrier.
50
+ - The result is a new Object in Car(S).
51
+
52
+ CRDT Semantics (G-Set)
53
+ ---------------------
54
+ - Commutative: merge(A, B) = merge(B, A)
55
+ - Associative: merge(A, merge(B, C)) = merge(merge(A, B), C)
56
+ - Idempotent: merge(A, A) = A
57
+ - Strong Eventual Consistency: Replicas converge without coordination.
58
+
59
+ CRD-Only Operations
60
+ -------------------
61
+ - Create: add(), add_with_handle()
62
+ - Read: get(), get_by_handle(), get_page(), search_*()
63
+ - Delete: delete(), clear()
64
+ - NO UPDATE: MCard content is immutable. Updates create new versions.
65
+
66
+ This class provides a simple interface for working with MCard collections.
67
+ It handles database initialization automatically if no engine is provided.
68
+ """
69
+
70
+ @staticmethod
71
+ def print_all_cards(db_path: str | None = None, page_size: int = 10) -> None:
72
+ """Print all cards in the database in a formatted table.
73
+
74
+ Args:
75
+ db_path: Path to the database file. If None, uses the default from environment.
76
+ page_size: Number of items to process per page (default: 10).
77
+ """
78
+
79
+ try:
80
+ # Use the default collection which is already initialized with the correct path
81
+ collection = (
82
+ CardCollection() if not db_path else CardCollection(db_path=db_path)
83
+ )
84
+
85
+ logger.info("Fetching MCards from the database...")
86
+
87
+ # Get database path for display
88
+ db_path_str = "default"
89
+ if hasattr(collection, "engine") and hasattr(collection.engine, "db_path"):
90
+ db_path_str = collection.engine.db_path
91
+
92
+ # Print the header with database path
93
+ logger.info(f"\nDATABASE: {db_path_str}")
94
+ logger.info("=" * 100)
95
+ logger.info(
96
+ f"{'Hash':<10} | {'Content':<40} | {'Created':<17} | {'Type':<15}"
97
+ )
98
+ logger.info("-" * 100)
99
+
100
+ # Get all cards directly without pagination
101
+ all_cards = collection.get_all_mcards_raw()
102
+ total_cards = len(all_cards)
103
+
104
+ # Print all cards
105
+ for card in all_cards:
106
+ # Process each card for display using MCard.to_display_dict
107
+ processed = card.to_display_dict()
108
+ if processed:
109
+ # Shorten hash to 7 characters with ellipsis
110
+ short_hash = (
111
+ f"{processed['hash'][:7]}..."
112
+ if len(processed["hash"]) > 7
113
+ else processed["hash"]
114
+ )
115
+ # Shorten content preview to fit in 40 chars
116
+ preview = (
117
+ str(processed["content_preview"])[:37] + "..."
118
+ if len(str(processed["content_preview"])) > 40
119
+ else str(processed["content_preview"])
120
+ )
121
+ # Shorten created_at and content_type if needed
122
+ created = str(processed["created_at"])[:14]
123
+ ctype = processed["content_type"].split("/")[-1][
124
+ :12
125
+ ] # Just take the subtype and limit length
126
+ logger.info(
127
+ f"{short_hash:<10} | {preview:<40} | {created:<17} | {ctype:<15}"
128
+ )
129
+
130
+ # Print summary
131
+ logger.info(f"\n{'=' * 100}")
132
+ logger.info(f"Displaying all {total_cards} MCards in database")
133
+ logger.info(f"Database path: {db_path_str}")
134
+
135
+ if total_cards == 0:
136
+ logger.info("No MCards found in the database.")
137
+
138
+ except Exception as e:
139
+ logger.error(f"Error accessing the database: {e}", exc_info=True)
140
+ sys.exit(1)
141
+
142
+ def __init__(self, engine=None, engine_type: str = "sqlite", db_path: str = None):
143
+ """Initialize the card collection.
144
+
145
+ Args:
146
+ engine: Optional pre-configured database engine. If None, a new one will be created.
147
+ engine_type: Type of database engine to use ('sqlite' is the only supported option).
148
+ db_path: Path to the database file. If None, defaults to 'data/mcard.db'.
149
+
150
+ Raises:
151
+ ValueError: If the provided engine is not a valid database engine instance.
152
+ ValueError: If db_path is not a string when engine is None.
153
+ """
154
+ # Input validation for engine
155
+ if engine is not None:
156
+ if not hasattr(engine, "add") or not callable(engine.add):
157
+ raise ValueError(
158
+ "Invalid engine provided. Engine must implement the StorageEngine interface "
159
+ "with required methods (add, get, delete, etc.)"
160
+ )
161
+ self.engine = engine
162
+ logger.debug(
163
+ f"Using provided engine, db path: {getattr(engine, 'db_path', 'unknown')}"
164
+ )
165
+ return
166
+
167
+ # If no engine provided, initialize a new one
168
+ if db_path is not None and not isinstance(db_path, str | Path):
169
+ raise ValueError(
170
+ f"db_path must be a string or Path, got {type(db_path).__name__}"
171
+ )
172
+
173
+ # Set up database path using environment variable or default if none provided
174
+ if db_path is None:
175
+ logger.debug("No db_path provided, getting from settings")
176
+ db_path = settings.database.path
177
+ else:
178
+ logger.debug(f"Using provided db_path: {db_path}")
179
+
180
+ # Handle special :memory: database
181
+ if str(db_path) == ":memory:":
182
+ logger.debug("Using in-memory database")
183
+ abs_db_path = ":memory:"
184
+ else:
185
+ # Ensure the directory exists for file-based databases
186
+ abs_db_path = os.path.abspath(str(db_path))
187
+ db_dir = os.path.dirname(abs_db_path) or "."
188
+ logger.debug(f"Ensuring directory exists: {db_dir}")
189
+ try:
190
+ os.makedirs(db_dir, exist_ok=True)
191
+ except OSError as e:
192
+ logger.error(f"Failed to create directory {db_dir}: {e}", exc_info=True)
193
+ raise ValueError(f"Failed to create directory {db_dir}: {e}") from e
194
+
195
+ try:
196
+ if engine_type == "duckdb":
197
+ try:
198
+ from clm.layer2.engine.duckdb_engine import (
199
+ DuckDBConnection,
200
+ DuckDBEngine,
201
+ )
202
+ except ImportError as e:
203
+ raise ValueError(
204
+ "DuckDB is not installed. Install with: pip install mcard[duckdb]"
205
+ ) from e
206
+ logger.debug(f"Initializing DuckDB connection to: {abs_db_path}")
207
+ connection = DuckDBConnection(abs_db_path)
208
+ self.engine = DuckDBEngine(connection)
209
+ logger.info(f"Initialized DuckDB database at {abs_db_path}")
210
+ else:
211
+ logger.debug(f"Initializing SQLite connection to: {abs_db_path}")
212
+ connection = SQLiteConnection(abs_db_path)
213
+ self.engine = SQLiteEngine(connection)
214
+ logger.info(f"Initialized SQLite database at {abs_db_path}")
215
+ logger.debug(
216
+ f"Engine initialized with db_path: {getattr(self.engine, 'db_path', 'unknown')}"
217
+ )
218
+
219
+ except (OSError, sqlite3.Error) as e:
220
+ logger.error(
221
+ f"Failed to initialize database at {abs_db_path}: {e}", exc_info=True
222
+ )
223
+ raise ValueError(
224
+ f"Failed to initialize database at {abs_db_path}: {e}"
225
+ ) from e
226
+
227
+ def add(self, card: MCard) -> str:
228
+ """Add a card to the collection.
229
+
230
+ In a content-addressable scheme, we first check if there's an existing card
231
+ with the same hash. If found, we compare the content to determine if it's
232
+ a duplicate (same content) or a collision (different content).
233
+
234
+ Args:
235
+ card: The MCard to add
236
+
237
+ Returns:
238
+ str: The hash of the card
239
+
240
+ Raises:
241
+ ValueError: If card is None
242
+ """
243
+ logger.debug(f"Attempting to add card with content: {card.content}")
244
+ if card is None:
245
+ raise ValueError("Card cannot be None")
246
+
247
+ # Get the hash of the incoming card
248
+ hash_value = card.hash
249
+
250
+ # Check if a card with this hash already exists
251
+ existing_card = self.get(hash_value)
252
+ if existing_card:
253
+ logger.debug(f"Card with hash {hash_value} already exists")
254
+ # Compare content to determine if it's a duplicate or collision
255
+ if existing_card.content == card.content:
256
+ logger.debug(f"Duplicate card found with content: {card.content}")
257
+ # Duplicate detected - create duplicate event and add directly via engine
258
+ logger.info(
259
+ f"Duplicate detected for hash {hash_value}; creating duplicate event."
260
+ )
261
+ from clm.layer0.model.event_producer import generate_duplication_event
262
+
263
+ duplicate_event_content_str = generate_duplication_event(card)
264
+ duplicate_event_card = MCard(duplicate_event_content_str)
265
+ try:
266
+ self.engine.add(duplicate_event_card)
267
+ return hash_value
268
+ except (ValueError, sqlite3.Error):
269
+ # If event creation fails, return existing hash
270
+ logger.warning(
271
+ f"Failed to create duplicate event for {hash_value}; returning existing hash."
272
+ )
273
+ return hash_value
274
+ else:
275
+ logger.debug(
276
+ f"Collision detected for card with content: {card.content}"
277
+ )
278
+ # Create collision event card and store the new card with new hash function
279
+ collision_event_content_str = generate_collision_event(card)
280
+ contentDict = json.loads(collision_event_content_str)
281
+ # Different content = collision, upgrade hash function to stronger level
282
+ collision_content_card = MCard(
283
+ card.content, contentDict["upgraded_function"]
284
+ ) # Use the new hash function (upgraded_function)
285
+ collision_event_card = MCard(collision_event_content_str)
286
+ logger.debug(f"Collision event: {collision_event_content_str}")
287
+ # Add via engine directly to avoid recursive duplicate handling
288
+ try:
289
+ self.engine.add(collision_event_card)
290
+ except (ValueError, sqlite3.Error) as e:
291
+ logger.debug(
292
+ f"Collision event already exists or failed to add: {e}"
293
+ )
294
+ try:
295
+ self.engine.add(collision_content_card)
296
+ except (ValueError, sqlite3.Error) as e:
297
+ logger.debug(
298
+ f"Collision content already exists or failed to add: {e}"
299
+ )
300
+ logger.debug(
301
+ f"Added collision event card with hash: {collision_event_card.hash}"
302
+ )
303
+ return collision_event_card.hash
304
+
305
+ # No existing card with this hash or content, add the new card
306
+ self.engine.add(card)
307
+ logger.debug(f"Successfully added card with hash {hash_value}")
308
+ return hash_value
309
+
310
+ def get(self, hash_value: str) -> MCard | None:
311
+ """Retrieve a card by its hash"""
312
+ return self.engine.get(hash_value)
313
+
314
+ async def aadd(self, card: MCard) -> str:
315
+ """Async wrapper for add() using a thread pool.
316
+
317
+ This provides an awaitable interface without changing the underlying
318
+ synchronous storage engine. When a true async backend is available,
319
+ this method can be updated to call it directly.
320
+ """
321
+ return await asyncio.to_thread(self.add, card)
322
+
323
+ async def aget(self, hash_value: str) -> MCard | None:
324
+ """Async wrapper for get() using a thread pool.
325
+
326
+ Returns:
327
+ The MCard if found, else None.
328
+ """
329
+ return await asyncio.to_thread(self.get, hash_value)
330
+
331
+ def delete(self, hash_value: str) -> bool:
332
+ """Delete a card by its hash"""
333
+ return self.engine.delete(hash_value)
334
+
335
+ def get_page(self, page_number: int = 1, page_size: int = None) -> Page:
336
+ """Get a page of cards"""
337
+ if page_size is None:
338
+ page_size = settings.pagination.default_page_size
339
+ return self.engine.get_cards_by_page(
340
+ page_number=page_number, page_size=page_size
341
+ )
342
+
343
+ def search_by_string(
344
+ self, search_string: str, page_number: int = 1, page_size: int = None
345
+ ) -> Page:
346
+ """Search for cards containing the given string.
347
+
348
+ Args:
349
+ search_string: The string to search for in card content
350
+ page_number: The page number to return (1-based)
351
+ page_size: Number of items per page (defaults to env setting)
352
+
353
+ Returns:
354
+ Page: A page of matching cards
355
+ """
356
+ if page_size is None:
357
+ page_size = settings.pagination.default_page_size
358
+ # Delegate the search to the engine's search_by_string method
359
+ return self.engine.search_by_string(search_string, page_number, page_size)
360
+
361
+ def search_by_hash(
362
+ self, hash_value: str, page_number: int = 1, page_size: int = None
363
+ ) -> Page:
364
+ """Search for cards by exact hash value.
365
+
366
+ Uses a direct O(1) primary-key lookup instead of loading all cards.
367
+
368
+ Args:
369
+ hash_value: The hash value to search for
370
+ page_number: The page number to return (1-based)
371
+ page_size: Number of items per page (defaults to env setting)
372
+
373
+ Returns:
374
+ Page: A page of matching cards
375
+ """
376
+ if page_size is None:
377
+ page_size = settings.pagination.default_page_size
378
+ if page_number < 1:
379
+ raise ValueError("Page number must be greater than 0")
380
+ if page_size < 1:
381
+ raise ValueError("Page size must be greater than 0")
382
+
383
+ card = self.engine.get(hash_value)
384
+ items = [card] if card and page_number == 1 else []
385
+ total_items = 1 if card else 0
386
+
387
+ return Page.create(items, total_items, page_number, page_size)
388
+
389
+ def search_by_content(
390
+ self, search_string: str, page_number: int = 1, page_size: int = None
391
+ ) -> Page:
392
+ """Search for cards by content string
393
+
394
+ Args:
395
+ search_string: The string to search for in card content
396
+ page_number: The page number to return (1-based)
397
+ page_size: Number of items per page (defaults to env setting)
398
+
399
+ Returns:
400
+ Page: A page of matching cards
401
+ """
402
+ if page_size is None:
403
+ page_size = settings.pagination.default_page_size
404
+ return self.engine.search_by_content(search_string, page_number, page_size)
405
+
406
+ def clear(self) -> None:
407
+ """Remove all cards"""
408
+ self.engine.clear()
409
+
410
+ def count(self) -> int:
411
+ """Return total number of cards"""
412
+ return self.engine.count()
413
+
414
+ def get_all_mcards_raw(self) -> list[MCard]:
415
+ """Retrieve all MCard objects directly from the database without pagination or callbacks.
416
+
417
+ This method is more efficient than get_all_cards when you need all cards and don't need
418
+ pagination or processing callbacks.
419
+
420
+ Returns:
421
+ list: A list of MCard objects
422
+ """
423
+ page = self.engine.get_all_cards()
424
+ return page.items if page and hasattr(page, "items") else []
425
+
426
+ def get_all_cards(
427
+ self,
428
+ page_size: int = 10,
429
+ process_callback: Callable[[MCard], Any] | None = None,
430
+ ) -> tuple[list[MCard], int]:
431
+ """Retrieve all cards from the database with pagination support.
432
+
433
+ Args:
434
+ page_size: Number of items per page. Defaults to 10.
435
+ process_callback: Optional callback function to process each card.
436
+ Signature: (card) -> processed_card
437
+
438
+ Returns:
439
+ tuple: (list of all cards, total count of cards)
440
+ """
441
+ all_cards = []
442
+ page_number = 1
443
+ total_cards = 0
444
+
445
+ while True:
446
+ # Get a page of cards
447
+ page = self.get_mcards_at_page(page_number=page_number, page_size=page_size)
448
+
449
+ # If no items on this page, we're done
450
+ if not page or not page.items:
451
+ break
452
+
453
+ # Process cards if callback is provided
454
+ processed_cards = [
455
+ (
456
+ process_callback(card)
457
+ if process_callback and callable(process_callback)
458
+ else card
459
+ )
460
+ for card in page.items
461
+ if card is not None
462
+ ]
463
+ all_cards.extend(processed_cards)
464
+
465
+ # Update total cards count
466
+ total_cards += len(page.items)
467
+
468
+ # If there are no more pages, we're done
469
+ if not page.has_next:
470
+ break
471
+
472
+ # Move to the next page
473
+ page_number += 1
474
+
475
+ return all_cards, total_cards
476
+
477
+ def get_mcards_at_page(self, page_number: int = 1, page_size: int = 10) -> Page:
478
+ """Get a single page of MCards.
479
+
480
+ Args:
481
+ page_number: Page number to retrieve (1-based)
482
+ page_size: Number of items per page
483
+
484
+ Returns:
485
+ Page: A Page object containing the MCards for the requested page
486
+
487
+ Raises:
488
+ ValueError: If page_number < 1 or page_size < 1
489
+ """
490
+ return self.engine.get_cards_by_page(
491
+ page_number=page_number, page_size=page_size
492
+ )
493
+
494
+ # ========== Handle Operations ==========
495
+
496
+ def add_with_handle(self, card: MCard, handle: str) -> str:
497
+ """Add a card and register a handle for it.
498
+
499
+ Args:
500
+ card: The MCard to add.
501
+ handle: The handle string to register.
502
+
503
+ Returns:
504
+ The hash of the added card.
505
+
506
+ Raises:
507
+ HandleValidationError: If the handle string is invalid.
508
+ ValueError: If the handle already exists.
509
+ """
510
+ hash_value = self.add(card)
511
+ self.engine.register_handle(handle, hash_value)
512
+ return hash_value
513
+
514
+ def update_handle(self, handle: str, new_card: MCard) -> str:
515
+ """Add a new card and update the handle to point to it.
516
+
517
+ Args:
518
+ handle: The handle string to update.
519
+ new_card: The new MCard to add.
520
+
521
+ Returns:
522
+ The hash of the new card.
523
+
524
+ Raises:
525
+ HandleValidationError: If the handle string is invalid.
526
+ ValueError: If the handle does not exist.
527
+ """
528
+ hash_value = self.add(new_card)
529
+ self.engine.update_handle(handle, hash_value)
530
+ return hash_value
531
+
532
+ def get_by_handle(self, handle: str) -> MCard | None:
533
+ """Get the card currently pointed to by a handle.
534
+
535
+ Args:
536
+ handle: The handle string to resolve.
537
+
538
+ Returns:
539
+ The MCard, or None if the handle does not exist.
540
+ """
541
+ return self.engine.get_by_handle(handle)
542
+
543
+ def resolve_handle(self, handle: str) -> str | None:
544
+ """Resolve a handle to its current hash.
545
+
546
+ Args:
547
+ handle: The handle string to resolve.
548
+
549
+ Returns:
550
+ The current hash, or None if the handle does not exist.
551
+ """
552
+ return self.engine.resolve_handle(handle)
553
+
554
+ def get_handle_history(self, handle: str) -> list:
555
+ """Get the version history for a handle.
556
+
557
+ Args:
558
+ handle: The handle string.
559
+
560
+ Returns:
561
+ A list of dicts with 'previous_hash' and 'changed_at' keys.
562
+ """
563
+ return self.engine.get_handle_history(handle)
564
+
565
+ def prune_handle_history(
566
+ self, handle: str, older_than: str | None = None, delete_all: bool = False
567
+ ) -> int:
568
+ """Prune version history for a handle.
569
+
570
+ Args:
571
+ handle: The handle string.
572
+ older_than: Optional timestamp string. Delete older entries.
573
+ delete_all: If True, delete all history.
574
+
575
+ Returns:
576
+ Number of deleted entries.
577
+ """
578
+ return self.engine.prune_handle_history(
579
+ handle, older_than=older_than, delete_all=delete_all
580
+ )
581
+
582
+ # =========================================================================
583
+ # Monadic Handle Methods (using Maybe monad from clm_template.py)
584
+ # =========================================================================
585
+
586
+ def get_by_handle_m(self, handle: str) -> "Maybe[MCard]":
587
+ """Monadic version of get_by_handle.
588
+
589
+ Returns Maybe.just(card) on success, Maybe.nothing() on failure.
590
+ Enables functional composition with .bind() chaining.
591
+
592
+ Example:
593
+ collection.get_by_handle_m("my_doc").bind(
594
+ lambda card: Maybe.just(card.get_content(as_text=True))
595
+ )
596
+
597
+ Args:
598
+ handle: The handle string to resolve.
599
+
600
+ Returns:
601
+ Maybe[MCard]: Just(card) if found, Nothing otherwise.
602
+ """
603
+ from clm.layer0.narrative import Maybe
604
+
605
+ card = self.get_by_handle(handle)
606
+ return Maybe.just(card) if card else Maybe.nothing()
607
+
608
+ def resolve_handle_m(self, handle: str) -> "Maybe[str]":
609
+ """Monadic version of resolve_handle.
610
+
611
+ Returns Maybe.just(hash) on success, Maybe.nothing() on failure.
612
+
613
+ Args:
614
+ handle: The handle string to resolve.
615
+
616
+ Returns:
617
+ Maybe[str]: Just(hash) if found, Nothing otherwise.
618
+ """
619
+ from clm.layer0.narrative import Maybe
620
+
621
+ hash_val = self.resolve_handle(handle)
622
+ return Maybe.just(hash_val) if hash_val else Maybe.nothing()
623
+
624
+ def get_m(self, hash_value: str) -> "Maybe[MCard]":
625
+ """Monadic version of get.
626
+
627
+ Returns Maybe.just(card) on success, Maybe.nothing() on failure.
628
+
629
+ Args:
630
+ hash_value: The hash of the card to retrieve.
631
+
632
+ Returns:
633
+ Maybe[MCard]: Just(card) if found, Nothing otherwise.
634
+ """
635
+ from clm.layer0.narrative import Maybe
636
+
637
+ card = self.get(hash_value)
638
+ return Maybe.just(card) if card else Maybe.nothing()
639
+
640
+ def resolve_and_get_m(self, handle: str) -> "Maybe[MCard]":
641
+ """Resolve a handle and get the card in one monadic operation.
642
+
643
+ This is equivalent to:
644
+ resolve_handle_m(handle).bind(lambda h: get_m(h))
645
+
646
+ Args:
647
+ handle: The handle string to resolve.
648
+
649
+ Returns:
650
+ Maybe[MCard]: Just(card) if resolved and found, Nothing otherwise.
651
+ """
652
+ return self.resolve_handle_m(handle).bind(lambda h: self.get_m(h))
653
+
654
+ _default_collection = None
655
+
656
+
657
+ def get_default_collection():
658
+ """Lazily get or create the default CardCollection."""
659
+ global _default_collection
660
+ if _default_collection is None:
661
+ from clm.cross_cutting.config.config_constants import DEFAULT_DB_PATH
662
+ _default_collection = CardCollection(db_path=DEFAULT_DB_PATH)
663
+ return _default_collection
664
+
665
+
666
+ # Register CardCollection as the concrete store factory for Layer 1 protocols (INV-383-01)
667
+ try:
668
+ from clm.layer1.protocols import register_store_factory
669
+
670
+ def _collection_factory(db_path: str | None = None) -> CardCollection:
671
+ from clm.cross_cutting.config.config_constants import DEFAULT_DB_PATH
672
+ if db_path is None or db_path == str(DEFAULT_DB_PATH):
673
+ return get_default_collection()
674
+ return CardCollection(db_path=db_path)
675
+
676
+ register_store_factory(_collection_factory)
677
+ except ImportError:
678
+ pass