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,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
|