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,518 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import sqlite3
|
|
3
|
+
|
|
4
|
+
from clm.cross_cutting.config.settings import settings
|
|
5
|
+
from clm.layer0.model.card import MCard
|
|
6
|
+
from clm.layer0.model.pagination import Page
|
|
7
|
+
from clm.layer2.engine.abstract_sql_engine import AbstractSqlEngine
|
|
8
|
+
from clm.layer2.engine.base import DatabaseConnection, resolve_db_path
|
|
9
|
+
|
|
10
|
+
# DEFAULT_PAGE_SIZE is used in function signatures, so we fetch it once or define a local constant
|
|
11
|
+
# However, usually we can access settings.pagination.default_page_size if it's constant.
|
|
12
|
+
# For function signatures, we can't use dynamic properties easily if they change.
|
|
13
|
+
# But settings are generally static after init.
|
|
14
|
+
# To avoid "argument default is mutable" or weirdness, we can grab the value.
|
|
15
|
+
DEFAULT_PAGE_SIZE = settings.pagination.default_page_size
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SQLiteConnection(DatabaseConnection):
|
|
21
|
+
"""Database connection for SQLite.
|
|
22
|
+
|
|
23
|
+
The schema is loaded exclusively from the canonical SQL files via the
|
|
24
|
+
``MCardSchema`` singleton. No hardcoded CREATE TABLE statements should
|
|
25
|
+
appear in this file.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
# =====================================================================
|
|
29
|
+
# Single Source of Truth: Schema files
|
|
30
|
+
# =====================================================================
|
|
31
|
+
# The SQLite engine does NOT define its own schema. It loads all
|
|
32
|
+
# table / index definitions from the canonical SQL files via the
|
|
33
|
+
# MCardSchema singleton.
|
|
34
|
+
#
|
|
35
|
+
# Canonical source files:
|
|
36
|
+
# schema/mcard_schema.sql (REQUIRED -- core tables)
|
|
37
|
+
# schema/mcard_vector_schema.sql (optional -- vector/graph tables)
|
|
38
|
+
#
|
|
39
|
+
# If a new table is added to either file, this engine picks it up
|
|
40
|
+
# automatically. No hardcoded CREATE TABLE statements should ever
|
|
41
|
+
# appear in this file.
|
|
42
|
+
# =====================================================================
|
|
43
|
+
|
|
44
|
+
SCHEMA_SOURCE_FILES = (
|
|
45
|
+
"schema/mcard_schema.sql", # Required -- core tables
|
|
46
|
+
"schema/mcard_vector_schema.sql", # Optional -- vector/graph tables
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def __init__(self, db_path: str):
|
|
50
|
+
self.db_path = resolve_db_path(db_path)
|
|
51
|
+
self.conn: sqlite3.Connection | None = None
|
|
52
|
+
|
|
53
|
+
def connect(self):
|
|
54
|
+
"""Open the SQLite connection and initialise schema if the DB is empty.
|
|
55
|
+
|
|
56
|
+
All schema definitions are loaded from the canonical SQL files via
|
|
57
|
+
``MCardSchema.init_all_tables(conn)``. No CREATE TABLE statements
|
|
58
|
+
are hardcoded in this method.
|
|
59
|
+
|
|
60
|
+
Raises:
|
|
61
|
+
FileNotFoundError: if the required core schema SQL file is missing.
|
|
62
|
+
"""
|
|
63
|
+
logger.debug(f"Connecting to database at {self.db_path}")
|
|
64
|
+
try:
|
|
65
|
+
self.conn = sqlite3.connect(self.db_path, check_same_thread=False)
|
|
66
|
+
# Set busy_timeout to match TypeScript SqliteNodeEngine parity
|
|
67
|
+
from clm.cross_cutting.config.config_constants import SQLITE_BUSY_TIMEOUT_MS
|
|
68
|
+
|
|
69
|
+
self.conn.execute(f"PRAGMA busy_timeout = {SQLITE_BUSY_TIMEOUT_MS}")
|
|
70
|
+
if self.db_path != ":memory:" and not self.db_path.startswith(
|
|
71
|
+
"file::memory:"
|
|
72
|
+
):
|
|
73
|
+
self.conn.execute("PRAGMA journal_mode = WAL")
|
|
74
|
+
self.conn.execute("PRAGMA synchronous = NORMAL")
|
|
75
|
+
else:
|
|
76
|
+
self.conn.execute("PRAGMA journal_mode = DELETE")
|
|
77
|
+
logger.debug(f"Connection established to {self.db_path}")
|
|
78
|
+
|
|
79
|
+
# Check if the database is empty and initialize schema if necessary
|
|
80
|
+
cursor = self.conn.cursor()
|
|
81
|
+
cursor.execute(
|
|
82
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='card'"
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
if not cursor.fetchone():
|
|
86
|
+
logger.debug(f"Creating new database schema at {self.db_path}")
|
|
87
|
+
|
|
88
|
+
from clm.schemas import MCardSchema
|
|
89
|
+
|
|
90
|
+
logger.debug("SQLite schema loading from canonical source")
|
|
91
|
+
|
|
92
|
+
schema = MCardSchema.get_instance()
|
|
93
|
+
|
|
94
|
+
# Initialize ALL tables from canonical SQL files
|
|
95
|
+
count = schema.init_all_tables(self.conn)
|
|
96
|
+
logger.debug(
|
|
97
|
+
f"Database schema created successfully "
|
|
98
|
+
f"({count} statements executed from canonical SQL files)"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
else:
|
|
102
|
+
logger.debug(f"Using existing database at {self.db_path}")
|
|
103
|
+
|
|
104
|
+
except sqlite3.Error as e:
|
|
105
|
+
logger.error(
|
|
106
|
+
f"Database error connecting to {self.db_path}: {e}", exc_info=True
|
|
107
|
+
)
|
|
108
|
+
raise
|
|
109
|
+
|
|
110
|
+
def disconnect(self):
|
|
111
|
+
if self.conn:
|
|
112
|
+
self.conn.close()
|
|
113
|
+
self.conn = None
|
|
114
|
+
|
|
115
|
+
def commit(self):
|
|
116
|
+
if self.conn:
|
|
117
|
+
self.conn.commit()
|
|
118
|
+
|
|
119
|
+
def rollback(self):
|
|
120
|
+
if self.conn:
|
|
121
|
+
self.conn.rollback()
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class SQLiteEngine(AbstractSqlEngine):
|
|
125
|
+
def __init__(self, connection: SQLiteConnection):
|
|
126
|
+
self.connection = connection
|
|
127
|
+
self.connection.connect()
|
|
128
|
+
|
|
129
|
+
def __del__(self):
|
|
130
|
+
self.connection.disconnect()
|
|
131
|
+
|
|
132
|
+
def __enter__(self):
|
|
133
|
+
return self
|
|
134
|
+
|
|
135
|
+
def __exit__(self, exc_type, exc_val, _exc_tb):
|
|
136
|
+
self.connection.disconnect()
|
|
137
|
+
return False
|
|
138
|
+
|
|
139
|
+
# =====================================================================
|
|
140
|
+
# AbstractSqlEngine primitives
|
|
141
|
+
# =====================================================================
|
|
142
|
+
|
|
143
|
+
def _query_rows(self, sql: str, params: tuple = ()) -> list[dict]:
|
|
144
|
+
cursor = self.connection.conn.cursor()
|
|
145
|
+
cursor.execute(sql, params)
|
|
146
|
+
columns = [desc[0] for desc in cursor.description] if cursor.description else []
|
|
147
|
+
return [dict(zip(columns, row, strict=False)) for row in cursor.fetchall()]
|
|
148
|
+
|
|
149
|
+
def _exec_sql(self, sql: str, params: tuple = ()) -> int:
|
|
150
|
+
import time
|
|
151
|
+
|
|
152
|
+
max_retries = 5
|
|
153
|
+
base_delay = 0.01
|
|
154
|
+
for attempt in range(max_retries):
|
|
155
|
+
try:
|
|
156
|
+
cursor = self.connection.conn.cursor()
|
|
157
|
+
cursor.execute(sql, params)
|
|
158
|
+
self.connection.commit()
|
|
159
|
+
return cursor.rowcount
|
|
160
|
+
except sqlite3.OperationalError as e:
|
|
161
|
+
if (
|
|
162
|
+
"locked" in str(e).lower() or "busy" in str(e).lower()
|
|
163
|
+
) and attempt < max_retries - 1:
|
|
164
|
+
time.sleep(base_delay * (2**attempt))
|
|
165
|
+
continue
|
|
166
|
+
raise
|
|
167
|
+
|
|
168
|
+
def _ensure_handle_tables(self) -> None:
|
|
169
|
+
"""Ensure the handle_registry and handle_history tables exist.
|
|
170
|
+
|
|
171
|
+
Uses the MCardSchema singleton to load table definitions from
|
|
172
|
+
the canonical SQL files.
|
|
173
|
+
"""
|
|
174
|
+
from clm.schemas import MCardSchema
|
|
175
|
+
|
|
176
|
+
schema = MCardSchema.get_instance()
|
|
177
|
+
cursor = self.connection.conn.cursor()
|
|
178
|
+
cursor.execute(schema.get_table("handle_registry"))
|
|
179
|
+
cursor.execute(schema.get_table("handle_history"))
|
|
180
|
+
cursor.execute(schema.get_index("idx_handle_current_hash"))
|
|
181
|
+
self.connection.commit()
|
|
182
|
+
logger.debug("Handle tables ensured.")
|
|
183
|
+
|
|
184
|
+
def _ensure_tombstone_table(self) -> None:
|
|
185
|
+
"""Ensure the card_tombstone table exists."""
|
|
186
|
+
cursor = self.connection.conn.cursor()
|
|
187
|
+
cursor.execute(
|
|
188
|
+
"""
|
|
189
|
+
CREATE TABLE IF NOT EXISTS card_tombstone (
|
|
190
|
+
hash TEXT PRIMARY KEY,
|
|
191
|
+
tombstoned_at TEXT NOT NULL,
|
|
192
|
+
reason TEXT DEFAULT 'DELETED',
|
|
193
|
+
FOREIGN KEY (hash) REFERENCES card(hash)
|
|
194
|
+
)
|
|
195
|
+
"""
|
|
196
|
+
)
|
|
197
|
+
self.connection.commit()
|
|
198
|
+
|
|
199
|
+
# =====================================================================
|
|
200
|
+
# Card Operations (engine-specific)
|
|
201
|
+
# =====================================================================
|
|
202
|
+
|
|
203
|
+
def add(self, card: MCard) -> str:
|
|
204
|
+
import time
|
|
205
|
+
|
|
206
|
+
hash_value = str(card.hash)
|
|
207
|
+
self._ensure_tombstone_table()
|
|
208
|
+
cursor = self.connection.conn.cursor()
|
|
209
|
+
|
|
210
|
+
# If previously tombstoned, untombstone to restore availability
|
|
211
|
+
cursor.execute("SELECT 1 FROM card_tombstone WHERE hash = ?", (hash_value,))
|
|
212
|
+
if cursor.fetchone():
|
|
213
|
+
cursor.execute("DELETE FROM card_tombstone WHERE hash = ?", (hash_value,))
|
|
214
|
+
self.connection.commit()
|
|
215
|
+
logger.debug(f"Re-activated previously tombstoned card {hash_value}")
|
|
216
|
+
return hash_value
|
|
217
|
+
|
|
218
|
+
max_retries = 5
|
|
219
|
+
base_delay = 0.01
|
|
220
|
+
for attempt in range(max_retries):
|
|
221
|
+
try:
|
|
222
|
+
# Ensure content is bytes
|
|
223
|
+
content_bytes = (
|
|
224
|
+
card.content
|
|
225
|
+
if isinstance(card.content, bytes)
|
|
226
|
+
else card.content.encode("utf-8")
|
|
227
|
+
)
|
|
228
|
+
cursor.execute(
|
|
229
|
+
"INSERT INTO card (hash, content, g_time) VALUES (?, ?, ?)",
|
|
230
|
+
(hash_value, content_bytes, str(card.g_time)),
|
|
231
|
+
)
|
|
232
|
+
self.connection.commit()
|
|
233
|
+
logger.debug(f"Added card with hash {hash_value}")
|
|
234
|
+
return hash_value
|
|
235
|
+
except sqlite3.OperationalError as e:
|
|
236
|
+
if (
|
|
237
|
+
"locked" in str(e).lower() or "busy" in str(e).lower()
|
|
238
|
+
) and attempt < max_retries - 1:
|
|
239
|
+
time.sleep(base_delay * (2**attempt))
|
|
240
|
+
continue
|
|
241
|
+
raise
|
|
242
|
+
except sqlite3.IntegrityError as e:
|
|
243
|
+
raise ValueError(
|
|
244
|
+
f"Card with hash {hash_value} and {str(card.g_time)} already exists, \n Content: {card.content[:20]}"
|
|
245
|
+
) from e
|
|
246
|
+
|
|
247
|
+
def get(self, hash_value: str) -> MCard | None:
|
|
248
|
+
self._ensure_tombstone_table()
|
|
249
|
+
cursor = self.connection.conn.cursor()
|
|
250
|
+
cursor.execute(
|
|
251
|
+
"""
|
|
252
|
+
SELECT c.content, c.g_time, c.hash
|
|
253
|
+
FROM card c
|
|
254
|
+
LEFT JOIN card_tombstone t ON c.hash = t.hash
|
|
255
|
+
WHERE c.hash = ? AND t.hash IS NULL
|
|
256
|
+
""",
|
|
257
|
+
(str(hash_value),),
|
|
258
|
+
)
|
|
259
|
+
row = cursor.fetchone()
|
|
260
|
+
|
|
261
|
+
if not row:
|
|
262
|
+
return None
|
|
263
|
+
|
|
264
|
+
content, g_time, hash_val = row
|
|
265
|
+
return self._row_to_card(content, hash_val, g_time)
|
|
266
|
+
|
|
267
|
+
def delete(self, hash_value: str) -> bool:
|
|
268
|
+
"""Tombstones a card to preserve G-Set CRDT immortality (INV-REF-03).
|
|
269
|
+
|
|
270
|
+
No row is deleted from the `card` CAS table. Instead, a tombstone is recorded
|
|
271
|
+
in `card_tombstone`, and associated handles are dereferenced with a TOMBSTONE
|
|
272
|
+
audit entry written to `handle_history`.
|
|
273
|
+
"""
|
|
274
|
+
from datetime import datetime, timezone
|
|
275
|
+
|
|
276
|
+
cursor = self.connection.conn.cursor()
|
|
277
|
+
cursor.execute("SELECT 1 FROM card WHERE hash = ?", (str(hash_value),))
|
|
278
|
+
if not cursor.fetchone():
|
|
279
|
+
return False
|
|
280
|
+
|
|
281
|
+
self._ensure_tombstone_table()
|
|
282
|
+
cursor.execute(
|
|
283
|
+
"SELECT 1 FROM card_tombstone WHERE hash = ?", (str(hash_value),)
|
|
284
|
+
)
|
|
285
|
+
if cursor.fetchone():
|
|
286
|
+
return False
|
|
287
|
+
|
|
288
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
289
|
+
cursor.execute(
|
|
290
|
+
"INSERT OR IGNORE INTO card_tombstone (hash, tombstoned_at, reason) VALUES (?, ?, 'DELETED')",
|
|
291
|
+
(str(hash_value), now),
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
self._ensure_handle_tables()
|
|
295
|
+
cursor.execute(
|
|
296
|
+
"SELECT handle FROM handle_registry WHERE current_hash = ?",
|
|
297
|
+
(str(hash_value),),
|
|
298
|
+
)
|
|
299
|
+
handles = [row[0] for row in cursor.fetchall()]
|
|
300
|
+
for handle in handles:
|
|
301
|
+
cursor.execute(
|
|
302
|
+
"INSERT INTO handle_history (handle, previous_hash, changed_at, change_type) VALUES (?, ?, ?, 'TOMBSTONE')",
|
|
303
|
+
(handle, str(hash_value), now),
|
|
304
|
+
)
|
|
305
|
+
cursor.execute("DELETE FROM handle_registry WHERE handle = ?", (handle,))
|
|
306
|
+
|
|
307
|
+
self.connection.commit()
|
|
308
|
+
return True
|
|
309
|
+
|
|
310
|
+
def search_by_string(
|
|
311
|
+
self,
|
|
312
|
+
search_string: str,
|
|
313
|
+
page_number: int = 1,
|
|
314
|
+
page_size: int = DEFAULT_PAGE_SIZE,
|
|
315
|
+
) -> Page:
|
|
316
|
+
"""Search for cards by string in content, hash, or g_time"""
|
|
317
|
+
if page_number < 1:
|
|
318
|
+
raise ValueError("Page number must be >= 1")
|
|
319
|
+
if page_size < 1:
|
|
320
|
+
raise ValueError("Page size must be >= 1")
|
|
321
|
+
|
|
322
|
+
offset = (page_number - 1) * page_size
|
|
323
|
+
self._ensure_tombstone_table()
|
|
324
|
+
cursor = self.connection.conn.cursor()
|
|
325
|
+
|
|
326
|
+
# Get total count of matching items
|
|
327
|
+
cursor.execute(
|
|
328
|
+
"""
|
|
329
|
+
SELECT COUNT(*) FROM card c
|
|
330
|
+
LEFT JOIN card_tombstone t ON c.hash = t.hash
|
|
331
|
+
WHERE t.hash IS NULL AND (c.hash LIKE ? OR c.g_time LIKE ? OR CAST(c.content AS TEXT) LIKE ?)
|
|
332
|
+
""",
|
|
333
|
+
(f"%{search_string}%", f"%{search_string}%", f"%{search_string}%"),
|
|
334
|
+
)
|
|
335
|
+
total_items = cursor.fetchone()[0]
|
|
336
|
+
|
|
337
|
+
# Get the actual items for the current page
|
|
338
|
+
cursor.execute(
|
|
339
|
+
"""
|
|
340
|
+
SELECT c.content, c.g_time, c.hash FROM card c
|
|
341
|
+
LEFT JOIN card_tombstone t ON c.hash = t.hash
|
|
342
|
+
WHERE t.hash IS NULL AND (c.hash LIKE ? OR c.g_time LIKE ? OR CAST(c.content AS TEXT) LIKE ?)
|
|
343
|
+
ORDER BY c.g_time DESC LIMIT ? OFFSET ?
|
|
344
|
+
""",
|
|
345
|
+
(
|
|
346
|
+
f"%{search_string}%",
|
|
347
|
+
f"%{search_string}%",
|
|
348
|
+
f"%{search_string}%",
|
|
349
|
+
page_size,
|
|
350
|
+
offset,
|
|
351
|
+
),
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
items = [
|
|
355
|
+
self._row_to_card(content, hash_val, g_time)
|
|
356
|
+
for content, g_time, hash_val in cursor.fetchall()
|
|
357
|
+
]
|
|
358
|
+
|
|
359
|
+
return Page.create(items, total_items, page_number, page_size)
|
|
360
|
+
|
|
361
|
+
def search_by_content(
|
|
362
|
+
self,
|
|
363
|
+
search_string: str | bytes,
|
|
364
|
+
page_number: int = 1,
|
|
365
|
+
page_size: int = DEFAULT_PAGE_SIZE,
|
|
366
|
+
) -> Page:
|
|
367
|
+
"""Search for cards by string or binary pattern in content.
|
|
368
|
+
|
|
369
|
+
Args:
|
|
370
|
+
search_string: The string or binary pattern to search for
|
|
371
|
+
page_number: The page number (1-based)
|
|
372
|
+
page_size: Number of items per page
|
|
373
|
+
|
|
374
|
+
Returns:
|
|
375
|
+
A Page object containing the matching cards
|
|
376
|
+
"""
|
|
377
|
+
if page_number < 1:
|
|
378
|
+
raise ValueError("Page number must be >= 1")
|
|
379
|
+
if page_size < 1:
|
|
380
|
+
raise ValueError("Page size must be >= 1")
|
|
381
|
+
|
|
382
|
+
if not search_string:
|
|
383
|
+
raise ValueError("Search string cannot be empty")
|
|
384
|
+
|
|
385
|
+
offset = (page_number - 1) * page_size
|
|
386
|
+
self._ensure_tombstone_table()
|
|
387
|
+
cursor = self.connection.conn.cursor()
|
|
388
|
+
|
|
389
|
+
# Convert search string to bytes if it's a string
|
|
390
|
+
if isinstance(search_string, str):
|
|
391
|
+
search_bytes = search_string.encode("utf-8")
|
|
392
|
+
else:
|
|
393
|
+
search_bytes = search_string
|
|
394
|
+
|
|
395
|
+
# For binary search, we need to use the INSTR function to find the pattern
|
|
396
|
+
cursor.execute(
|
|
397
|
+
"""
|
|
398
|
+
SELECT COUNT(*) FROM card c
|
|
399
|
+
LEFT JOIN card_tombstone t ON c.hash = t.hash
|
|
400
|
+
WHERE t.hash IS NULL AND INSTR(c.content, ?) > 0
|
|
401
|
+
""",
|
|
402
|
+
(search_bytes,),
|
|
403
|
+
)
|
|
404
|
+
total_items = cursor.fetchone()[0]
|
|
405
|
+
|
|
406
|
+
# Get the actual items for the current page
|
|
407
|
+
cursor.execute(
|
|
408
|
+
"""
|
|
409
|
+
SELECT c.content, c.g_time, c.hash FROM card c
|
|
410
|
+
LEFT JOIN card_tombstone t ON c.hash = t.hash
|
|
411
|
+
WHERE t.hash IS NULL AND INSTR(c.content, ?) > 0
|
|
412
|
+
ORDER BY c.g_time DESC
|
|
413
|
+
LIMIT ? OFFSET ?
|
|
414
|
+
""",
|
|
415
|
+
(search_bytes, page_size, offset),
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
items = [
|
|
419
|
+
self._row_to_card(content, hash_val, g_time)
|
|
420
|
+
for content, g_time, hash_val in cursor.fetchall()
|
|
421
|
+
]
|
|
422
|
+
|
|
423
|
+
return Page.create(items, total_items, page_number, page_size)
|
|
424
|
+
|
|
425
|
+
def count(self) -> int:
|
|
426
|
+
self._ensure_tombstone_table()
|
|
427
|
+
cursor = self.connection.conn.cursor()
|
|
428
|
+
cursor.execute(
|
|
429
|
+
"""
|
|
430
|
+
SELECT COUNT(*) FROM card c
|
|
431
|
+
LEFT JOIN card_tombstone t ON c.hash = t.hash
|
|
432
|
+
WHERE t.hash IS NULL
|
|
433
|
+
"""
|
|
434
|
+
)
|
|
435
|
+
return cursor.fetchone()[0]
|
|
436
|
+
|
|
437
|
+
def get_cards_by_page(
|
|
438
|
+
self, page_number: int = 1, page_size: int = DEFAULT_PAGE_SIZE
|
|
439
|
+
) -> Page:
|
|
440
|
+
"""Get a page of cards from the database.
|
|
441
|
+
|
|
442
|
+
Args:
|
|
443
|
+
page_number: The page number (1-based).
|
|
444
|
+
page_size: Number of items per page.
|
|
445
|
+
|
|
446
|
+
Returns:
|
|
447
|
+
A Page object containing the requested cards.
|
|
448
|
+
|
|
449
|
+
Raises:
|
|
450
|
+
ValueError: If page_number < 1 or page_size < 1
|
|
451
|
+
"""
|
|
452
|
+
if page_number < 1:
|
|
453
|
+
raise ValueError("Page number must be >= 1")
|
|
454
|
+
if page_size < 1:
|
|
455
|
+
raise ValueError("Page size must be >= 1")
|
|
456
|
+
|
|
457
|
+
self._ensure_tombstone_table()
|
|
458
|
+
cursor = self.connection.conn.cursor()
|
|
459
|
+
cursor.execute(
|
|
460
|
+
"""
|
|
461
|
+
SELECT COUNT(*) FROM card c
|
|
462
|
+
LEFT JOIN card_tombstone t ON c.hash = t.hash
|
|
463
|
+
WHERE t.hash IS NULL
|
|
464
|
+
"""
|
|
465
|
+
)
|
|
466
|
+
total_items = cursor.fetchone()[0]
|
|
467
|
+
|
|
468
|
+
offset = (page_number - 1) * page_size
|
|
469
|
+
cursor.execute(
|
|
470
|
+
"""
|
|
471
|
+
SELECT c.content, c.g_time, c.hash FROM card c
|
|
472
|
+
LEFT JOIN card_tombstone t ON c.hash = t.hash
|
|
473
|
+
WHERE t.hash IS NULL
|
|
474
|
+
ORDER BY c.g_time DESC LIMIT ? OFFSET ?
|
|
475
|
+
""",
|
|
476
|
+
(page_size, offset),
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
items = [
|
|
480
|
+
self._row_to_card(content, hash_val, g_time)
|
|
481
|
+
for content, g_time, hash_val in cursor.fetchall()
|
|
482
|
+
]
|
|
483
|
+
|
|
484
|
+
return Page.create(items, total_items, page_number, page_size)
|
|
485
|
+
|
|
486
|
+
# =====================================================================
|
|
487
|
+
# Handle Space Mapping Operations (bi-directional mapping parity)
|
|
488
|
+
# =====================================================================
|
|
489
|
+
|
|
490
|
+
def set_handle(self, handle: str, target_hash: str, timestamp: str) -> None:
|
|
491
|
+
"""Physically map a namespace Handle strictly against the immutable Content Hash."""
|
|
492
|
+
cursor = self.connection.conn.cursor()
|
|
493
|
+
cursor.execute(
|
|
494
|
+
"""
|
|
495
|
+
INSERT INTO handle_registry (handle, current_hash, created_at, updated_at)
|
|
496
|
+
VALUES (?, ?, ?, ?)
|
|
497
|
+
ON CONFLICT(handle) DO UPDATE SET
|
|
498
|
+
current_hash=excluded.current_hash,
|
|
499
|
+
updated_at=excluded.updated_at
|
|
500
|
+
""",
|
|
501
|
+
(handle, target_hash, timestamp, timestamp),
|
|
502
|
+
)
|
|
503
|
+
self.connection.commit()
|
|
504
|
+
|
|
505
|
+
def get_handle_dump(self) -> list[tuple[str, bytes]]:
|
|
506
|
+
"""Recursively returns ALL valid mappings between Handles and physical Target MCards."""
|
|
507
|
+
self._ensure_tombstone_table()
|
|
508
|
+
cursor = self.connection.conn.cursor()
|
|
509
|
+
cursor.execute(
|
|
510
|
+
"""
|
|
511
|
+
SELECT h.handle, c.content
|
|
512
|
+
FROM handle_registry h
|
|
513
|
+
INNER JOIN card c ON h.current_hash = c.hash
|
|
514
|
+
LEFT JOIN card_tombstone t ON c.hash = t.hash
|
|
515
|
+
WHERE t.hash IS NULL
|
|
516
|
+
"""
|
|
517
|
+
)
|
|
518
|
+
return [(row[0], row[1]) for row in cursor.fetchall()]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Layer 2 Cordis Fiber Substrate (Sprint 389, INV-389-01..04).
|
|
2
|
+
|
|
3
|
+
Normative Reference:
|
|
4
|
+
docs/spec/20-architecture/card-triad-architecture.md § Cordis Fiber Spatiotemporal Execution Substrate
|
|
5
|
+
docs/spec/40-behavior/hoare_triple_composition_protocol.md § 1 (Hoare Invariant Gating)
|
|
6
|
+
|
|
7
|
+
Invariants:
|
|
8
|
+
INV-389-01: 5-State Fiber Lifecycle (Pending ➔ Loading ➔ Active ➔ Unloading ➔ Disposed).
|
|
9
|
+
INV-389-02: Reversible LIFO Effect Teardown Stack (DisposableList).
|
|
10
|
+
INV-389-03: Sub-millisecond (<1.0ms) Transactional Rollback (SavepointGuard, TriDbSavepointGuard).
|
|
11
|
+
INV-389-04: Lexical Context Hierarchy & Service Injection (Context, @Inject, ctx.isolate).
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from .context import Context
|
|
16
|
+
from .disposable import DisposableList, SavepointGuard, TriDbSavepointGuard
|
|
17
|
+
from .fiber import Fiber, FiberLifecycleError, FiberState
|
|
18
|
+
from .hoare import HoareExecutionReport, HoareFiberExecutor, HoareTriple
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"Context",
|
|
22
|
+
"DisposableList",
|
|
23
|
+
"SavepointGuard",
|
|
24
|
+
"TriDbSavepointGuard",
|
|
25
|
+
"Fiber",
|
|
26
|
+
"FiberState",
|
|
27
|
+
"FiberLifecycleError",
|
|
28
|
+
"HoareTriple",
|
|
29
|
+
"HoareExecutionReport",
|
|
30
|
+
"HoareFiberExecutor",
|
|
31
|
+
]
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Cordis Hierarchical Context Tree (DEL-389-01, INV-389-04).
|
|
2
|
+
|
|
3
|
+
Implements lexical scoping, dependency injection (@Inject), and spatiotemporal
|
|
4
|
+
isolation (ctx.isolate) composed over the existing clm.layer0.context.CLMContext.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from clm.layer0.context import CLMContext
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Context:
|
|
14
|
+
"""Hierarchical Cordis Service Context.
|
|
15
|
+
|
|
16
|
+
Enforces INV-389-04 (Spatiotemporal Lexical Isolation):
|
|
17
|
+
Mutations and service registrations performed in child contexts (created via
|
|
18
|
+
`ctx.isolate()`) never leak into sibling or parent contexts.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
parent: Context | None = None,
|
|
24
|
+
services: dict[str, Any] | None = None,
|
|
25
|
+
clm_ctx: CLMContext | None = None,
|
|
26
|
+
) -> None:
|
|
27
|
+
self.parent = parent
|
|
28
|
+
self._services: dict[str, Any] = dict(services or {})
|
|
29
|
+
self.clm_ctx = clm_ctx or (parent.clm_ctx.isolate() if parent and parent.clm_ctx else CLMContext.create_default())
|
|
30
|
+
|
|
31
|
+
def provide(self, name: str, service: Any) -> None:
|
|
32
|
+
"""Register a service in this lexical context."""
|
|
33
|
+
self._services[name] = service
|
|
34
|
+
|
|
35
|
+
def get(self, name: str, default: Any = None) -> Any:
|
|
36
|
+
"""Resolve a service by name, walking up the ancestor hierarchy if needed."""
|
|
37
|
+
if name in self._services:
|
|
38
|
+
return self._services[name]
|
|
39
|
+
if self.parent is not None:
|
|
40
|
+
return self.parent.get(name, default)
|
|
41
|
+
return default
|
|
42
|
+
|
|
43
|
+
def has(self, name: str) -> bool:
|
|
44
|
+
"""Check if a service is available in this context or any ancestor."""
|
|
45
|
+
if name in self._services:
|
|
46
|
+
return True
|
|
47
|
+
if self.parent is not None:
|
|
48
|
+
return self.parent.has(name)
|
|
49
|
+
return False
|
|
50
|
+
|
|
51
|
+
def inject(self, *service_names: str) -> dict[str, Any]:
|
|
52
|
+
"""Resolve multiple declared @Inject services, raising KeyError if missing."""
|
|
53
|
+
resolved = {}
|
|
54
|
+
for s in service_names:
|
|
55
|
+
if not self.has(s):
|
|
56
|
+
raise KeyError(f"Context injection failed: service '{s}' not available in context")
|
|
57
|
+
resolved[s] = self.get(s)
|
|
58
|
+
return resolved
|
|
59
|
+
|
|
60
|
+
def isolate(
|
|
61
|
+
self,
|
|
62
|
+
services: dict[str, Any] | None = None,
|
|
63
|
+
**clm_overrides: Any,
|
|
64
|
+
) -> Context:
|
|
65
|
+
"""Create an isolated child context.
|
|
66
|
+
|
|
67
|
+
Mutations in the child context NEVER affect the parent (INV-389-04).
|
|
68
|
+
"""
|
|
69
|
+
child_clm = self.clm_ctx.isolate(**clm_overrides) if self.clm_ctx else None
|
|
70
|
+
return Context(
|
|
71
|
+
parent=self,
|
|
72
|
+
services=services or {},
|
|
73
|
+
clm_ctx=child_clm,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def __getitem__(self, key: str) -> Any:
|
|
77
|
+
val = self.get(key)
|
|
78
|
+
if val is None and not self.has(key):
|
|
79
|
+
raise KeyError(f"Service '{key}' not found in context")
|
|
80
|
+
return val
|
|
81
|
+
|
|
82
|
+
def __setitem__(self, key: str, value: Any) -> None:
|
|
83
|
+
self.provide(key, value)
|
|
84
|
+
|
|
85
|
+
def __contains__(self, key: str) -> bool:
|
|
86
|
+
return self.has(key)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
__all__ = ["Context"]
|