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,320 @@
1
+ """
2
+ AbstractSqlEngine — Base class for SQL-backed StorageEngine implementations.
3
+
4
+ Provides concrete implementations of all handle operations using two abstract
5
+ primitives that each SQL engine must implement:
6
+
7
+ _query_rows(sql, params) — Run a SELECT and return list of dicts
8
+ _exec_sql(sql, params) — Run a write statement and return rows-changed
9
+
10
+ SQL uses ``?`` as the canonical placeholder. Each engine is responsible
11
+ for translating to its native syntax (e.g. DuckDB's ``$1, $2, ...``).
12
+
13
+ Card operations (add, get, delete, get_page, search, etc.) remain abstract
14
+ because they involve engine-specific cursor patterns and exception types.
15
+ """
16
+
17
+ import logging
18
+ from abc import abstractmethod
19
+ from datetime import datetime, timezone
20
+
21
+ from clm.layer0.model.card import MCard, MCardFromData
22
+ from clm.layer0.model.pagination import Page
23
+ from clm.layer2.engine.base import StorageEngine
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ class AbstractSqlEngine(StorageEngine):
29
+ """Intermediate base class providing concrete handle operations.
30
+
31
+ Subclasses must implement ``_query_rows``, ``_exec_sql``, and
32
+ ``_ensure_handle_tables``. All handle methods are written once here
33
+ and inherited by both SQLiteEngine and DuckDBEngine.
34
+ """
35
+
36
+ # =====================================================================
37
+ # Abstract primitives — each SQL engine implements these
38
+ # =====================================================================
39
+
40
+ @abstractmethod
41
+ def _query_rows(self, sql: str, params: tuple = ()) -> list[dict]:
42
+ """Run a SELECT query and return a list of row dicts.
43
+
44
+ Each dict maps column name -> value. SQL uses ``?`` placeholders.
45
+ """
46
+ ...
47
+
48
+ @abstractmethod
49
+ def _exec_sql(self, sql: str, params: tuple = ()) -> int:
50
+ """Run a write statement (INSERT/UPDATE/DELETE) and return rows-changed.
51
+
52
+ SQL uses ``?`` placeholders.
53
+ """
54
+ ...
55
+
56
+ @abstractmethod
57
+ def _ensure_handle_tables(self) -> None:
58
+ """Ensure handle_registry and handle_history tables exist."""
59
+ ...
60
+
61
+ @abstractmethod
62
+ def _ensure_tombstone_table(self) -> None:
63
+ """Ensure card_tombstone table exists."""
64
+ ...
65
+
66
+ # =====================================================================
67
+ # Shared: _row_to_card (identical in both engines)
68
+ # =====================================================================
69
+
70
+ @staticmethod
71
+ def _row_to_card(content, hash_value: str, g_time: str) -> MCard:
72
+ """Convert a database row into an MCard instance."""
73
+ if not isinstance(content, bytes):
74
+ logger.warning(
75
+ f"Content from database is not bytes but {type(content)}: "
76
+ f"{str(content)[:20]}..."
77
+ )
78
+ content_bytes = (
79
+ content.encode("utf-8") if isinstance(content, str) else bytes(content)
80
+ )
81
+ else:
82
+ content_bytes = content
83
+ return MCardFromData(content_bytes, hash_value, g_time)
84
+
85
+ # =====================================================================
86
+ # Shared: get_all_cards, get_page delegate, clear
87
+ # =====================================================================
88
+
89
+ def get_all_cards(self) -> Page:
90
+ """Get all non-tombstoned cards in a single page."""
91
+ self._ensure_tombstone_table()
92
+ rows = self._query_rows(
93
+ """
94
+ SELECT c.content, c.hash, c.g_time
95
+ FROM card c
96
+ LEFT JOIN card_tombstone t ON c.hash = t.hash
97
+ WHERE t.hash IS NULL
98
+ """
99
+ )
100
+ items = [self._row_to_card(r["content"], r["hash"], r["g_time"]) for r in rows]
101
+ return Page.create(
102
+ items=items,
103
+ total_items=len(items),
104
+ page_number=1,
105
+ page_size=len(items) if items else 1,
106
+ )
107
+
108
+ def get_page(self, page_number: int = 1, page_size: int = 10) -> Page:
109
+ """Delegate to get_cards_by_page."""
110
+ return self.get_cards_by_page(page_number, page_size)
111
+
112
+ def clear(self) -> None:
113
+ """Tombstones all cards to preserve G-Set CRDT immortality (INV-REF-03).
114
+
115
+ Hard deletion (`DELETE FROM card`) is strictly forbidden by INV-REF-03.
116
+ Callers of clear() will tombstone all cards while retaining raw CAS records intact.
117
+ """
118
+ import warnings
119
+
120
+ warnings.warn(
121
+ "Hard deletion violates G-Set CRDT invariants. Tombstoning all cards in card_tombstone.",
122
+ DeprecationWarning,
123
+ stacklevel=2,
124
+ )
125
+ self._ensure_tombstone_table()
126
+ now = datetime.now(timezone.utc).isoformat()
127
+ self._exec_sql(
128
+ "INSERT OR IGNORE INTO card_tombstone (hash, tombstoned_at, reason) SELECT hash, ?, 'CLEARED' FROM card",
129
+ (now,),
130
+ )
131
+ self._ensure_handle_tables()
132
+ self._exec_sql("DELETE FROM handle_registry")
133
+
134
+ # =====================================================================
135
+ # Concrete: Handle Operations
136
+ # =====================================================================
137
+
138
+ def ensure_handle_tables(self) -> None:
139
+ self._ensure_handle_tables()
140
+
141
+ def register_handle(self, handle: str, hash_value: str) -> bool:
142
+ from clm.layer0.model.handle import validate_handle
143
+
144
+ self._ensure_handle_tables()
145
+ validated = validate_handle(handle)
146
+ now = datetime.now(timezone.utc).isoformat()
147
+
148
+ # Check for duplicate
149
+ existing = self._query_rows(
150
+ "SELECT handle FROM handle_registry WHERE handle = ?",
151
+ (validated,),
152
+ )
153
+ if existing:
154
+ raise ValueError(f"Handle '{validated}' already exists.")
155
+
156
+ self._exec_sql(
157
+ "INSERT INTO handle_registry (handle, current_hash, created_at, updated_at) "
158
+ "VALUES (?, ?, ?, ?)",
159
+ (validated, hash_value, now, now),
160
+ )
161
+ logger.info(f"Registered handle '{validated}' -> {hash_value[:8]}...")
162
+ return True
163
+
164
+ def resolve_handle(self, handle: str) -> str | None:
165
+ from clm.layer0.model.handle import HandleValidationError, validate_handle
166
+
167
+ self._ensure_handle_tables()
168
+ try:
169
+ validated = validate_handle(handle)
170
+ except HandleValidationError:
171
+ # Invalid handle format - cannot exist in registry
172
+ return None
173
+ rows = self._query_rows(
174
+ "SELECT current_hash FROM handle_registry WHERE handle = ?",
175
+ (validated,),
176
+ )
177
+ return rows[0]["current_hash"] if rows else None
178
+
179
+ def get_by_handle(self, handle: str) -> MCard | None:
180
+ hash_value = self.resolve_handle(handle)
181
+ if hash_value:
182
+ return self.get(hash_value)
183
+ return None
184
+
185
+ def update_handle(self, handle: str, new_hash: str) -> str:
186
+ from clm.layer0.model.handle import validate_handle
187
+
188
+ self._ensure_handle_tables()
189
+ validated = validate_handle(handle)
190
+ now = datetime.now(timezone.utc).isoformat()
191
+
192
+ rows = self._query_rows(
193
+ "SELECT current_hash FROM handle_registry WHERE handle = ?",
194
+ (validated,),
195
+ )
196
+ if not rows:
197
+ raise ValueError(f"Handle '{validated}' not found.")
198
+
199
+ previous_hash = rows[0]["current_hash"]
200
+
201
+ # Record history
202
+ self._exec_sql(
203
+ "INSERT INTO handle_history (handle, previous_hash, changed_at) "
204
+ "VALUES (?, ?, ?)",
205
+ (validated, previous_hash, now),
206
+ )
207
+
208
+ # Update handle
209
+ self._exec_sql(
210
+ "UPDATE handle_registry SET current_hash = ?, updated_at = ? WHERE handle = ?",
211
+ (new_hash, now, validated),
212
+ )
213
+
214
+ logger.info(
215
+ f"Updated handle '{validated}': {previous_hash[:8]}... -> {new_hash[:8]}..."
216
+ )
217
+ return previous_hash
218
+
219
+ def get_handle_history(self, handle: str) -> list[dict[str, str]]:
220
+ from clm.layer0.model.handle import validate_handle
221
+
222
+ self._ensure_handle_tables()
223
+ validated = validate_handle(handle)
224
+ rows = self._query_rows(
225
+ "SELECT previous_hash, changed_at FROM handle_history "
226
+ "WHERE handle = ? ORDER BY id DESC",
227
+ (validated,),
228
+ )
229
+ return [
230
+ {"previous_hash": r["previous_hash"], "changed_at": r["changed_at"]}
231
+ for r in rows
232
+ ]
233
+
234
+ def prune_handle_history(
235
+ self, handle: str, older_than: str | None = None, delete_all: bool = False
236
+ ) -> int:
237
+ from clm.layer0.model.handle import validate_handle
238
+
239
+ self._ensure_handle_tables()
240
+ validated = validate_handle(handle)
241
+
242
+ if delete_all:
243
+ return self._exec_sql(
244
+ "DELETE FROM handle_history WHERE handle = ?", (validated,)
245
+ )
246
+ elif older_than:
247
+ return self._exec_sql(
248
+ "DELETE FROM handle_history WHERE handle = ? AND changed_at < ?",
249
+ (validated, older_than),
250
+ )
251
+ return 0
252
+
253
+ # =====================================================================
254
+ # Concrete: Handle Management Operations
255
+ # =====================================================================
256
+
257
+ def get_all_handles(self) -> list[dict[str, str]]:
258
+ self._ensure_handle_tables()
259
+ rows = self._query_rows("SELECT handle, current_hash FROM handle_registry")
260
+ return [{"handle": r["handle"], "hash": r["current_hash"]} for r in rows]
261
+
262
+ def remove_handle(self, handle: str) -> None:
263
+ from clm.layer0.model.handle import validate_handle
264
+
265
+ self._ensure_handle_tables()
266
+ validated = validate_handle(handle)
267
+ self._exec_sql("DELETE FROM handle_history WHERE handle = ?", (validated,))
268
+ self._exec_sql("DELETE FROM handle_registry WHERE handle = ?", (validated,))
269
+ logger.info(f"Removed handle '{validated}' and its history.")
270
+
271
+ def rename_handle(self, old_handle: str, new_handle: str) -> None:
272
+ from clm.layer0.model.handle import validate_handle
273
+
274
+ self._ensure_handle_tables()
275
+ old_validated = validate_handle(old_handle)
276
+ new_validated = validate_handle(new_handle)
277
+
278
+ if old_validated == new_validated:
279
+ return
280
+
281
+ # Check old exists
282
+ old_rows = self._query_rows(
283
+ "SELECT handle, current_hash, created_at, updated_at "
284
+ "FROM handle_registry WHERE handle = ?",
285
+ (old_validated,),
286
+ )
287
+ if not old_rows:
288
+ raise ValueError(f"Handle '{old_validated}' not found.")
289
+
290
+ # Check new doesn't exist
291
+ new_rows = self._query_rows(
292
+ "SELECT handle FROM handle_registry WHERE handle = ?",
293
+ (new_validated,),
294
+ )
295
+ if new_rows:
296
+ raise ValueError(f"Handle '{new_validated}' already exists.")
297
+
298
+ # FK-safe rename: INSERT new → UPDATE history → DELETE old.
299
+ # handle_history.handle FK-references handle_registry.handle, so we
300
+ # cannot simply UPDATE either table first without violating the constraint.
301
+ old_row = old_rows[0]
302
+ self._exec_sql(
303
+ "INSERT INTO handle_registry (handle, current_hash, created_at, updated_at) "
304
+ "VALUES (?, ?, ?, ?)",
305
+ (
306
+ new_validated,
307
+ old_row["current_hash"],
308
+ old_row["created_at"],
309
+ old_row["updated_at"],
310
+ ),
311
+ )
312
+ self._exec_sql(
313
+ "UPDATE handle_history SET handle = ? WHERE handle = ?",
314
+ (new_validated, old_validated),
315
+ )
316
+ self._exec_sql(
317
+ "DELETE FROM handle_registry WHERE handle = ?",
318
+ (old_validated,),
319
+ )
320
+ logger.info(f"Renamed handle '{old_validated}' -> '{new_validated}'.")
@@ -0,0 +1,193 @@
1
+ import logging
2
+ from abc import ABC, abstractmethod
3
+ from pathlib import Path
4
+ from typing import Protocol
5
+
6
+ from clm.layer0.model.card import MCard
7
+ from clm.layer0.model.pagination import Page
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ def resolve_db_path(db_path: str) -> str:
13
+ """Resolve a database path to an absolute path, creating parent dirs.
14
+
15
+ Shared by SQLiteConnection and DuckDBConnection to avoid duplicating
16
+ path resolution logic.
17
+
18
+ Args:
19
+ db_path: Path string, or ':memory:' for in-memory database.
20
+
21
+ Returns:
22
+ The resolved absolute path, or ':memory:' unchanged.
23
+
24
+ Raises:
25
+ PermissionError: If the parent directory cannot be created.
26
+ OSError: On other filesystem errors.
27
+ """
28
+ if db_path == ":memory:":
29
+ return ":memory:"
30
+
31
+ path = Path(db_path)
32
+ if not path.is_absolute():
33
+ # Resolve relative to the project root (mcard/engine/base.py -> root)
34
+ # base.py is in mcard/engine/, so root is 2 levels up.
35
+ root = Path(__file__).resolve().parents[2]
36
+ path = (root / db_path).resolve()
37
+ else:
38
+ path = path.resolve()
39
+
40
+ try:
41
+ path.parent.mkdir(parents=True, exist_ok=True)
42
+ except PermissionError as e:
43
+ logger.error(f"Permission error: {e}", exc_info=True)
44
+ logger.error(f"Unable to access or create database at {path}")
45
+ raise
46
+ except OSError as e:
47
+ logger.error(f"Unexpected error setting up database: {e}", exc_info=True)
48
+ raise
49
+
50
+ return str(path)
51
+
52
+
53
+ class HandleHistoryEntry:
54
+ """Represents a single entry in handle version history."""
55
+
56
+ __slots__ = ("previous_hash", "changed_at")
57
+
58
+ def __init__(self, previous_hash: str, changed_at: str) -> None:
59
+ self.previous_hash = previous_hash
60
+ self.changed_at = changed_at
61
+
62
+ def to_dict(self) -> dict:
63
+ return {"previous_hash": self.previous_hash, "changed_at": self.changed_at}
64
+
65
+
66
+ class DatabaseConnection(Protocol):
67
+ """Protocol for database connections"""
68
+
69
+ def connect(self) -> None: ...
70
+ def disconnect(self) -> None: ...
71
+ def commit(self) -> None: ...
72
+ def rollback(self) -> None: ...
73
+
74
+
75
+ class StorageEngine(ABC):
76
+ """Abstract base class for storage engines.
77
+
78
+ Mirrors the TypeScript StorageEngine interface in mcard-js to ensure
79
+ cross-language parity. All card *and* handle operations are declared
80
+ here so that every concrete engine is guaranteed to support them.
81
+ """
82
+
83
+ # ========== Card Operations ==========
84
+
85
+ @abstractmethod
86
+ def add(self, card: MCard) -> str:
87
+ """Add a card and return its hash."""
88
+ pass
89
+
90
+ @abstractmethod
91
+ def get(self, hash_value: str) -> MCard | None:
92
+ """Retrieve a card by its hash."""
93
+ pass
94
+
95
+ @abstractmethod
96
+ def delete(self, hash_value: str) -> bool:
97
+ """Delete a card by its hash."""
98
+ pass
99
+
100
+ @abstractmethod
101
+ def get_page(self, page_number: int, page_size: int) -> Page:
102
+ """Get a page of cards."""
103
+ pass
104
+
105
+ @abstractmethod
106
+ def get_all_cards(self) -> Page:
107
+ """Get all cards in a single page."""
108
+ pass
109
+
110
+ @abstractmethod
111
+ def get_cards_by_page(self, page_number: int, page_size: int) -> Page:
112
+ """Get a page of cards (canonical pagination method)."""
113
+ pass
114
+
115
+ @abstractmethod
116
+ def search_by_string(
117
+ self, search_string: str, page_number: int, page_size: int
118
+ ) -> Page:
119
+ """Search cards by string in content, hash, or g_time."""
120
+ pass
121
+
122
+ @abstractmethod
123
+ def search_by_content(
124
+ self, search_string: str, page_number: int, page_size: int
125
+ ) -> Page:
126
+ """Search cards by content pattern."""
127
+ pass
128
+
129
+ @abstractmethod
130
+ def clear(self) -> None:
131
+ """Remove all cards."""
132
+ pass
133
+
134
+ @abstractmethod
135
+ def count(self) -> int:
136
+ """Return total number of cards."""
137
+ pass
138
+
139
+ # ========== Handle Operations ==========
140
+
141
+ @abstractmethod
142
+ def ensure_handle_tables(self) -> None:
143
+ """Ensure handle-related tables exist."""
144
+ pass
145
+
146
+ @abstractmethod
147
+ def register_handle(self, handle: str, hash_value: str) -> bool:
148
+ """Register a new handle pointing to a hash."""
149
+ pass
150
+
151
+ @abstractmethod
152
+ def resolve_handle(self, handle: str) -> str | None:
153
+ """Resolve a handle to its current hash."""
154
+ pass
155
+
156
+ @abstractmethod
157
+ def get_by_handle(self, handle: str) -> MCard | None:
158
+ """Get the MCard currently pointed to by a handle."""
159
+ pass
160
+
161
+ @abstractmethod
162
+ def update_handle(self, handle: str, new_hash: str) -> str:
163
+ """Update handle to point to new_hash; return previous hash."""
164
+ pass
165
+
166
+ @abstractmethod
167
+ def get_handle_history(self, handle: str) -> list:
168
+ """Get version history for a handle."""
169
+ pass
170
+
171
+ @abstractmethod
172
+ def prune_handle_history(
173
+ self, handle: str, older_than: str | None = None, delete_all: bool = False
174
+ ) -> int:
175
+ """Prune version history for a handle."""
176
+ pass
177
+
178
+ # ========== Handle Management Operations ==========
179
+
180
+ @abstractmethod
181
+ def get_all_handles(self) -> list:
182
+ """List all registered handles with their current hashes."""
183
+ pass
184
+
185
+ @abstractmethod
186
+ def remove_handle(self, handle: str) -> None:
187
+ """Remove a handle and all its history. Does NOT delete the card."""
188
+ pass
189
+
190
+ @abstractmethod
191
+ def rename_handle(self, old_handle: str, new_handle: str) -> None:
192
+ """Rename a handle atomically."""
193
+ pass