clm-kernel 0.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- clm/__init__.py +230 -0
- clm/__main__.py +25 -0
- clm/cross_cutting/__init__.py +50 -0
- clm/cross_cutting/_test_runner.py +36 -0
- clm/cross_cutting/async_utils.py +25 -0
- clm/cross_cutting/cel_builder.py +123 -0
- clm/cross_cutting/cel_converter.py +697 -0
- clm/cross_cutting/cel_engine.py +252 -0
- clm/cross_cutting/cel_ops.py +72 -0
- clm/cross_cutting/cel_ops_db.py +157 -0
- clm/cross_cutting/cel_ops_files.py +99 -0
- clm/cross_cutting/cel_ops_state.py +71 -0
- clm/cross_cutting/cel_ops_strings.py +64 -0
- clm/cross_cutting/cel_resolver.py +63 -0
- clm/cross_cutting/cel_slices.py +56 -0
- clm/cross_cutting/cel_store.py +330 -0
- clm/cross_cutting/clm_logger.py +140 -0
- clm/cross_cutting/config/__init__.py +155 -0
- clm/cross_cutting/config/config_constants.py +392 -0
- clm/cross_cutting/config/env_parameters.py +90 -0
- clm/cross_cutting/config/logging.py +378 -0
- clm/cross_cutting/config/settings.py +504 -0
- clm/cross_cutting/context.py +5 -0
- clm/cross_cutting/domain_models.py +55 -0
- clm/cross_cutting/domain_types.py +61 -0
- clm/cross_cutting/effects.py +67 -0
- clm/cross_cutting/errors.py +153 -0
- clm/cross_cutting/governance.py +331 -0
- clm/cross_cutting/guards.py +41 -0
- clm/cross_cutting/mcard.py +321 -0
- clm/cross_cutting/native.py +185 -0
- clm/cross_cutting/observability.py +289 -0
- clm/cross_cutting/pcard.py +434 -0
- clm/cross_cutting/registry.py +71 -0
- clm/cross_cutting/resources.py +84 -0
- clm/cross_cutting/secrets.py +44 -0
- clm/cross_cutting/subprocess_utils.py +118 -0
- clm/cross_cutting/telemetry.py +164 -0
- clm/cross_cutting/testing/__init__.py +8 -0
- clm/cross_cutting/testing/comparator.py +187 -0
- clm/cross_cutting/testing/datasets.py +87 -0
- clm/cross_cutting/timeutil.py +20 -0
- clm/cross_cutting/types/new_type_check.py +0 -0
- clm/cross_cutting/utils/__init__.py +1 -0
- clm/cross_cutting/utils/url_safety.py +50 -0
- clm/cross_cutting/vcard.py +560 -0
- clm/cross_cutting/writer.py +118 -0
- clm/layer0/README.md +50 -0
- clm/layer0/__init__.py +223 -0
- clm/layer0/algebra.py +60 -0
- clm/layer0/algebra_baldwin.py +133 -0
- clm/layer0/algebra_baldwin_generators.py +36 -0
- clm/layer0/algebra_equivalence.py +262 -0
- clm/layer0/algebra_ingest.py +306 -0
- clm/layer0/algebra_manifest_io.py +156 -0
- clm/layer0/assembler.py +481 -0
- clm/layer0/cel/__init__.py +23 -0
- clm/layer0/cel_eval.py +491 -0
- clm/layer0/codec.py +106 -0
- clm/layer0/context.py +122 -0
- clm/layer0/db.py +160 -0
- clm/layer0/exceptions.py +73 -0
- clm/layer0/fibration.py +106 -0
- clm/layer0/gates.py +50 -0
- clm/layer0/hash.py +80 -0
- clm/layer0/loader.py +826 -0
- clm/layer0/math_utils.py +163 -0
- clm/layer0/mcard.py +96 -0
- clm/layer0/model/__init__.py +0 -0
- clm/layer0/model/action.py +223 -0
- clm/layer0/model/card.py +280 -0
- clm/layer0/model/card_triad.py +427 -0
- clm/layer0/model/compatibility.py +60 -0
- clm/layer0/model/dictionary.py +46 -0
- clm/layer0/model/dots.py +331 -0
- clm/layer0/model/event_producer.py +88 -0
- clm/layer0/model/g_time.py +89 -0
- clm/layer0/model/handle.py +163 -0
- clm/layer0/model/hash/__init__.py +6 -0
- clm/layer0/model/hash/algorithms/__init__.py +3 -0
- clm/layer0/model/hash/algorithms/custom_hash.py +7 -0
- clm/layer0/model/hash/algorithms/local_sha256.py +178 -0
- clm/layer0/model/hash/constants.py +53 -0
- clm/layer0/model/hash/enums.py +60 -0
- clm/layer0/model/hash/validator.py +307 -0
- clm/layer0/model/interpreter.py +275 -0
- clm/layer0/model/pagination.py +45 -0
- clm/layer0/model/pcard.py +716 -0
- clm/layer0/model/schema.py +44 -0
- clm/layer0/model/utils/__init__.py +1 -0
- clm/layer0/model/utils/content_analyzer.py +152 -0
- clm/layer0/model/validators/__init__.py +1 -0
- clm/layer0/model/validators/base_validator.py +44 -0
- clm/layer0/model/validators/binary_validator.py +66 -0
- clm/layer0/model/validators/text_validator.py +82 -0
- clm/layer0/model/validators/validation_registry.py +49 -0
- clm/layer0/model/vcard.py +823 -0
- clm/layer0/model/vcard_ext/__init__.py +39 -0
- clm/layer0/model/vcard_ext/core.py +60 -0
- clm/layer0/model/vcard_ext/network.py +23 -0
- clm/layer0/model/vcard_ext/observability.py +63 -0
- clm/layer0/model/vcard_ext/storage.py +60 -0
- clm/layer0/model/vcard_ext/vendors.py +211 -0
- clm/layer0/model/vcard_sandwich.py +119 -0
- clm/layer0/model/vcard_vocabulary.py +463 -0
- clm/layer0/model/workflow.py +274 -0
- clm/layer0/narrative.py +431 -0
- clm/layer0/ontology/collection.py +125 -0
- clm/layer0/parser.py +429 -0
- clm/layer0/relations.py +73 -0
- clm/layer0/rule_evaluator.py +251 -0
- clm/layer0/schema/__init__.py +38 -0
- clm/layer0/schema/ddl.py +97 -0
- clm/layer0/templates/abstract.yaml +99 -0
- clm/layer0/templates/balanced.yaml +253 -0
- clm/layer0/templates/concrete.yaml +172 -0
- clm/layer0/type_registry.py +223 -0
- clm/layer0/types.py +204 -0
- clm/layer0/utils.py +886 -0
- clm/layer0/verifier_core.py +262 -0
- clm/layer1/__init__.py +31 -0
- clm/layer1/action_dispatcher.py +275 -0
- clm/layer1/adapter_registry.py +233 -0
- clm/layer1/algebra_baldwin_generators.py +192 -0
- clm/layer1/arrow_compiler.py +525 -0
- clm/layer1/batch_processor.py +152 -0
- clm/layer1/cel/__init__.py +27 -0
- clm/layer1/cel/builder.py +173 -0
- clm/layer1/engine.py +759 -0
- clm/layer1/engine_async.py +89 -0
- clm/layer1/eval_callback.py +49 -0
- clm/layer1/gate_composer.py +229 -0
- clm/layer1/hoare_sandwich.py +298 -0
- clm/layer1/identity_guard.py +348 -0
- clm/layer1/matrix.py +138 -0
- clm/layer1/neural_runtime.py +221 -0
- clm/layer1/operations/__init__.py +123 -0
- clm/layer1/operations/augment.py +156 -0
- clm/layer1/operations/base.py +109 -0
- clm/layer1/operations/builtins.py +338 -0
- clm/layer1/operations/dispatch.py +144 -0
- clm/layer1/operations/exclude.py +107 -0
- clm/layer1/operations/handle.py +197 -0
- clm/layer1/operations/identity.py +139 -0
- clm/layer1/operations/invert.py +66 -0
- clm/layer1/operations/loader.py +116 -0
- clm/layer1/operations/manipulators/__init__.py +23 -0
- clm/layer1/operations/manipulators/document.py +211 -0
- clm/layer1/operations/manipulators/media.py +134 -0
- clm/layer1/operations/manipulators/structured.py +159 -0
- clm/layer1/operations/manipulators/tabular.py +334 -0
- clm/layer1/operations/port.py +65 -0
- clm/layer1/operations/services.py +529 -0
- clm/layer1/operations/split.py +117 -0
- clm/layer1/operations/substitute.py +75 -0
- clm/layer1/petri.py +1052 -0
- clm/layer1/pocketflow_orchestrator.py +408 -0
- clm/layer1/pocketflow_scheduler.py +519 -0
- clm/layer1/protocols.py +197 -0
- clm/layer1/runtime_adapter.py +718 -0
- clm/layer1/runtimes/__init__.py +71 -0
- clm/layer1/runtimes/_generated_assertions.py +125 -0
- clm/layer1/runtimes/_generated_types.py +42 -0
- clm/layer1/runtimes/base.py +265 -0
- clm/layer1/runtimes/binary.py +101 -0
- clm/layer1/runtimes/factory.py +203 -0
- clm/layer1/runtimes/javascript.py +321 -0
- clm/layer1/runtimes/javascript_runtime.js +33 -0
- clm/layer1/runtimes/js_scripts/isomorphism.mjs +22 -0
- clm/layer1/runtimes/js_scripts/loader.mjs +52 -0
- clm/layer1/runtimes/lambda_calc.py +126 -0
- clm/layer1/runtimes/python.py +288 -0
- clm/layer1/runtimes/python_runtime.py +41 -0
- clm/layer1/runtimes/script.py +114 -0
- clm/layer1/sandbox.py +334 -0
- clm/layer1/savepoint_guard.py +136 -0
- clm/layer1/sparse_net.py +122 -0
- clm/layer1/spec_tester.py +165 -0
- clm/layer1/store.py +295 -0
- clm/layer1/test_runner.py +498 -0
- clm/layer1/type_inspector.py +166 -0
- clm/layer1/verifier.py +772 -0
- clm/layer1/vm.py +91 -0
- clm/layer2/__init__.py +78 -0
- clm/layer2/card_collection.py +678 -0
- clm/layer2/classifier.py +479 -0
- clm/layer2/engine/__init__.py +1 -0
- clm/layer2/engine/abstract_sql_engine.py +320 -0
- clm/layer2/engine/base.py +193 -0
- clm/layer2/engine/duckdb_engine.py +576 -0
- clm/layer2/engine/sqlite_engine.py +518 -0
- clm/layer2/fiber/__init__.py +31 -0
- clm/layer2/fiber/context.py +89 -0
- clm/layer2/fiber/disposable.py +163 -0
- clm/layer2/fiber/fiber.py +125 -0
- clm/layer2/fiber/hoare.py +191 -0
- clm/layer2/file_io.py +376 -0
- clm/layer2/file_registrar.py +77 -0
- clm/layer2/ingest.py +202 -0
- clm/layer2/lineage.py +322 -0
- clm/layer2/mcard_fs.py +262 -0
- clm/layer2/merkle.py +11 -0
- clm/layer2/mime.py +244 -0
- clm/layer2/mime_detector.py +245 -0
- clm/layer2/private_collection.py +118 -0
- clm/layer2/shared_store.py +11 -0
- clm/layer2/storage/__init__.py +190 -0
- clm/layer2/storage/db_bitemporal.py +141 -0
- clm/layer2/storage/db_driver.py +331 -0
- clm/layer2/storage/db_hyperlinks.py +214 -0
- clm/layer2/storage/db_pool.py +139 -0
- clm/layer2/storage/db_repository.py +229 -0
- clm/layer2/storage/savepoint.py +78 -0
- clm/layer2/storage/sqlite_wal.py +74 -0
- clm/layer2/tridb.py +245 -0
- clm/layer2/vcard_receipt.py +71 -0
- clm/layer2/vfs.py +17 -0
- clm/layer3/__init__.py +141 -0
- clm/layer3/agency/__init__.py +19 -0
- clm/layer3/agency/config.py +220 -0
- clm/layer3/agency/providers/__init__.py +15 -0
- clm/layer3/agency/providers/base.py +91 -0
- clm/layer3/agency/providers/mlc_llm.py +169 -0
- clm/layer3/agency/providers/ollama.py +203 -0
- clm/layer3/agency/router.py +71 -0
- clm/layer3/agency/runtime.py +524 -0
- clm/layer3/gateway.py +353 -0
- clm/layer3/open_interpreter_adapter.py +159 -0
- clm/layer3/rag/__init__.py +70 -0
- clm/layer3/rag/cli.py +228 -0
- clm/layer3/rag/config.py +153 -0
- clm/layer3/rag/embeddings/__init__.py +16 -0
- clm/layer3/rag/embeddings/base.py +79 -0
- clm/layer3/rag/embeddings/ollama.py +211 -0
- clm/layer3/rag/embeddings/vision.py +290 -0
- clm/layer3/rag/engine.py +276 -0
- clm/layer3/rag/graph/__init__.py +22 -0
- clm/layer3/rag/graph/community.py +198 -0
- clm/layer3/rag/graph/engine.py +433 -0
- clm/layer3/rag/graph/extractor.py +255 -0
- clm/layer3/rag/graph/schema.py +115 -0
- clm/layer3/rag/graph/store.py +641 -0
- clm/layer3/rag/indexer.py +333 -0
- clm/layer3/rag/llm_providers.json +52 -0
- clm/layer3/rag/semantic_versioning.py +313 -0
- clm/layer3/rag/vector/__init__.py +28 -0
- clm/layer3/rag/vector/handle_vector_store.py +749 -0
- clm/layer3/rag/vector/schema.py +151 -0
- clm/layer3/rag/vector/store.py +632 -0
- clm/layer3/reticulum/__init__.py +55 -0
- clm/layer3/reticulum/bridge.py +74 -0
- clm/layer3/reticulum/daemon.py +88 -0
- clm/layer3/reticulum/discovery.py +164 -0
- clm/layer3/reticulum/identity.py +107 -0
- clm/layer3/reticulum/media.py +148 -0
- clm/layer3/reticulum/transport.py +133 -0
- clm/layer3/satori/__init__.py +209 -0
- clm/layer3/satori/alpha_conversion.py +168 -0
- clm/layer3/satori/beta_reduction.py +359 -0
- clm/layer3/satori/continuation.py +38 -0
- clm/layer3/satori/eta_conversion.py +205 -0
- clm/layer3/satori/free_variables.py +163 -0
- clm/layer3/satori/io_effects.py +370 -0
- clm/layer3/satori/lambda_runtime.py +726 -0
- clm/layer3/satori/lambda_term.py +230 -0
- clm/layer3/satori/sheaf_audit.py +261 -0
- clm/layer3/satori/speech_act.py +279 -0
- clm/layer4/README.md +9 -0
- clm/layer4/__init__.py +67 -0
- clm/layer4/_sovereign_executor.py +580 -0
- clm/layer4/action_commands.py +719 -0
- clm/layer4/api.py +236 -0
- clm/layer4/bootstrap/__init__.py +13 -0
- clm/layer4/bootstrap/bootstrapper.py +118 -0
- clm/layer4/bootstrap/cleanup.py +44 -0
- clm/layer4/bootstrap/clm_hooks.py +49 -0
- clm/layer4/bootstrap/genesis.py +77 -0
- clm/layer4/bootstrap/injection.py +125 -0
- clm/layer4/cli.py +549 -0
- clm/layer4/collection_manager.py +553 -0
- clm/layer4/commands/__init__.py +81 -0
- clm/layer4/commands/action_cmd.py +42 -0
- clm/layer4/commands/check_cmd.py +82 -0
- clm/layer4/commands/envelope.py +73 -0
- clm/layer4/commands/evaluate_cmd.py +224 -0
- clm/layer4/commands/mcard_cmd.py +24 -0
- clm/layer4/commands/petri_cmd.py +92 -0
- clm/layer4/commands/tridb_cmd.py +319 -0
- clm/layer4/commands/version_cmd.py +27 -0
- clm/layer4/diff_commands.py +167 -0
- clm/layer4/eoa_sponsor.py +232 -0
- clm/layer4/events.py +69 -0
- clm/layer4/flux_gateway.py +133 -0
- clm/layer4/gateway.py +91 -0
- clm/layer4/loader.py +307 -0
- clm/layer4/mcard_commands.py +166 -0
- clm/layer4/membrane.py +273 -0
- clm/layer4/navigation.py +50 -0
- clm/layer4/protocol/protocol_entry_python.py +108 -0
- clm/layer4/protocol/python_sync.py +126 -0
- clm/layer4/runner.py +150 -0
- clm/layer4/satori.py +262 -0
- clm/layer4/server/python_sync_client.py +150 -0
- clm/layer4/server/python_sync_server.py +139 -0
- clm/layer4/server/service_api.py +315 -0
- clm/layer4/server/start_servers.py +107 -0
- clm/layer4/server/stop_servers.py +24 -0
- clm/layer4/simulated_optimization_fix.py +0 -0
- clm/layer4/storage.py +463 -0
- clm/layer4/tridb_admin.py +268 -0
- clm/layer4/websocket/test_ws_client.py +104 -0
- clm/layer4/websocket/test_ws_server.py +138 -0
- clm/layer4/websocket/ws_server_local.py +160 -0
- clm/layer5/__init__.py +51 -0
- clm/layer5/async_batch_writer.py +133 -0
- clm/layer5/learning.py +276 -0
- clm/layer5/telemetry_hook.py +107 -0
- clm/layer5/type_lattice.py +238 -0
- clm/py.typed +0 -0
- clm/schemas/__init__.py +652 -0
- clm/schemas/abstract_face_schema.yaml +77 -0
- clm/schemas/cel_schema.yaml +416 -0
- clm/schemas/lifecycle/__init__.py +45 -0
- clm/schemas/lifecycle/models.py +123 -0
- clm/schemas/lifecycle/service.py +364 -0
- clm/schemas/mcard_schema.sql +192 -0
- clm/schemas/mcard_vector_schema.sql +283 -0
- clm/schemas/mlp-context-v1.json +91 -0
- clm_kernel-0.0.0.dist-info/METADATA +121 -0
- clm_kernel-0.0.0.dist-info/RECORD +333 -0
- clm_kernel-0.0.0.dist-info/WHEEL +4 -0
- clm_kernel-0.0.0.dist-info/entry_points.txt +3 -0
- clm_kernel-0.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -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
|