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
clm/layer2/lineage.py
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
"""Merkle Lineage Graph Engine (Sprint 395 / DEL-395-03).
|
|
2
|
+
|
|
3
|
+
Tracks causal ancestry and Merkle provenance across multi-step Action workflows:
|
|
4
|
+
(Parent MCards, Action Hash, VCard Receipt) ──> Output MCard
|
|
5
|
+
|
|
6
|
+
Guarantees:
|
|
7
|
+
- AC-395-03-01: Every output MCard embeds immutable Merkle hashes of ancestor cards and actions.
|
|
8
|
+
- AC-395-03-02: get_mcard_lineage() reconstructs complete provenance tree in <5ms.
|
|
9
|
+
- AC-395-03-03: Visual export produces valid Mermaid flowchart and Graphviz DOT markdown.
|
|
10
|
+
|
|
11
|
+
Layer 2 module (can import Layer 0, Layer 1, Layer 2).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import datetime
|
|
17
|
+
import time
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from clm.cross_cutting.domain_models import MCard
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class LineageNode:
|
|
26
|
+
"""A node in the Merkle lineage graph."""
|
|
27
|
+
|
|
28
|
+
id: str
|
|
29
|
+
type: str # "mcard", "action", "receipt"
|
|
30
|
+
label: str
|
|
31
|
+
hash: str
|
|
32
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class LineageEdge:
|
|
37
|
+
"""A causal directed edge in the lineage graph."""
|
|
38
|
+
|
|
39
|
+
source: str
|
|
40
|
+
target: str
|
|
41
|
+
relation: str = "derives" # "derives", "transformed_by", "certified_by"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class LineageDAG:
|
|
46
|
+
"""Reconstructed directed acyclic graph of MCard provenance."""
|
|
47
|
+
|
|
48
|
+
target_uri: str
|
|
49
|
+
nodes: dict[str, LineageNode] = field(default_factory=dict)
|
|
50
|
+
edges: list[LineageEdge] = field(default_factory=list)
|
|
51
|
+
reconstruction_time_ms: float = 0.0
|
|
52
|
+
|
|
53
|
+
def add_node(self, node: LineageNode) -> None:
|
|
54
|
+
self.nodes[node.id] = node
|
|
55
|
+
|
|
56
|
+
def add_edge(self, edge: LineageEdge) -> None:
|
|
57
|
+
self.edges.append(edge)
|
|
58
|
+
|
|
59
|
+
def get_ancestors(self, uri: str) -> list[str]:
|
|
60
|
+
"""Compute all ancestor URIs that contributed to the given node."""
|
|
61
|
+
parents_map: dict[str, list[str]] = {}
|
|
62
|
+
for e in self.edges:
|
|
63
|
+
parents_map.setdefault(e.target, []).append(e.source)
|
|
64
|
+
|
|
65
|
+
visited: set[str] = set()
|
|
66
|
+
queue = list(parents_map.get(uri, []))
|
|
67
|
+
while queue:
|
|
68
|
+
curr = queue.pop(0)
|
|
69
|
+
if curr not in visited:
|
|
70
|
+
visited.add(curr)
|
|
71
|
+
queue.extend(parents_map.get(curr, []))
|
|
72
|
+
return sorted(list(visited))
|
|
73
|
+
|
|
74
|
+
def get_root_inputs(self) -> list[str]:
|
|
75
|
+
"""Return all root inputs (nodes with no incoming causal edges)."""
|
|
76
|
+
has_incoming = {e.target for e in self.edges}
|
|
77
|
+
return [nid for nid, node in self.nodes.items() if nid not in has_incoming and node.type == "mcard"]
|
|
78
|
+
|
|
79
|
+
def to_mermaid(self) -> str:
|
|
80
|
+
"""Export lineage DAG to valid Mermaid flowchart markdown."""
|
|
81
|
+
lines = ["flowchart TD"]
|
|
82
|
+
|
|
83
|
+
# Define node styles
|
|
84
|
+
for nid, node in self.nodes.items():
|
|
85
|
+
safe_id = nid.replace(":", "_").replace("/", "_").replace(".", "_").replace("-", "_")
|
|
86
|
+
short_hash = node.hash[:8] if node.hash else ""
|
|
87
|
+
if node.type == "action":
|
|
88
|
+
label = f"⚡ Action: {node.label}\\n[{short_hash}]"
|
|
89
|
+
lines.append(f' {safe_id}["{label}"]:::actionClass')
|
|
90
|
+
elif node.type == "receipt":
|
|
91
|
+
label = f"📜 Receipt: {node.label}\\n[{short_hash}]"
|
|
92
|
+
lines.append(f' {safe_id}["{label}"]:::receiptClass')
|
|
93
|
+
else:
|
|
94
|
+
label = f"📦 {node.label}\\n[{short_hash}]"
|
|
95
|
+
lines.append(f' {safe_id}["{label}"]:::mcardClass')
|
|
96
|
+
|
|
97
|
+
# Define edges
|
|
98
|
+
for e in self.edges:
|
|
99
|
+
src = e.source.replace(":", "_").replace("/", "_").replace(".", "_").replace("-", "_")
|
|
100
|
+
tgt = e.target.replace(":", "_").replace("/", "_").replace(".", "_").replace("-", "_")
|
|
101
|
+
if e.relation == "transformed_by":
|
|
102
|
+
lines.append(f" {src} -->|transformed_by| {tgt}")
|
|
103
|
+
elif e.relation == "certified_by":
|
|
104
|
+
lines.append(f" {tgt} -.->|certified_by| {src}")
|
|
105
|
+
else:
|
|
106
|
+
lines.append(f" {src} -->|derives| {tgt}")
|
|
107
|
+
|
|
108
|
+
# Styles
|
|
109
|
+
lines.append(" classDef mcardClass fill:#e3f2fd,stroke:#1565c0,stroke-width:2px;")
|
|
110
|
+
lines.append(" classDef actionClass fill:#fff3e0,stroke:#e65100,stroke-width:2px;")
|
|
111
|
+
lines.append(" classDef receiptClass fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px;")
|
|
112
|
+
|
|
113
|
+
return "\n".join(lines)
|
|
114
|
+
|
|
115
|
+
def to_dot(self) -> str:
|
|
116
|
+
"""Export lineage DAG to Graphviz DOT format."""
|
|
117
|
+
lines = ['digraph LineageDAG {', ' rankdir=TB;', ' node [shape=box, style=rounded, fontname="Helvetica"];']
|
|
118
|
+
|
|
119
|
+
for nid, node in self.nodes.items():
|
|
120
|
+
short_hash = node.hash[:8] if node.hash else ""
|
|
121
|
+
lines.append(f' "{nid}" [label="{node.label}\\n({short_hash})"];')
|
|
122
|
+
|
|
123
|
+
for e in self.edges:
|
|
124
|
+
lines.append(f' "{e.source}" -> "{e.target}" [label="{e.relation}"];')
|
|
125
|
+
|
|
126
|
+
lines.append("}")
|
|
127
|
+
return "\n".join(lines)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@dataclass
|
|
131
|
+
class LineageRecord:
|
|
132
|
+
"""Immutable causal record of a single step derivation."""
|
|
133
|
+
|
|
134
|
+
output_uri: str
|
|
135
|
+
output_hash: str
|
|
136
|
+
parent_uris: list[str]
|
|
137
|
+
parent_hashes: list[str]
|
|
138
|
+
action_name: str
|
|
139
|
+
action_hash: str
|
|
140
|
+
receipt_uri: str | None
|
|
141
|
+
receipt_hash: str | None
|
|
142
|
+
step_id: str
|
|
143
|
+
timestamp: str = field(default_factory=lambda: datetime.datetime.now(datetime.timezone.utc).isoformat())
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class LineageRegistry:
|
|
147
|
+
"""In-memory and persistent causal registry for MCard provenance."""
|
|
148
|
+
|
|
149
|
+
def __init__(self):
|
|
150
|
+
self._records: dict[str, LineageRecord] = {} # output_uri -> record
|
|
151
|
+
self._hash_to_uri: dict[str, str] = {} # output_hash -> uri
|
|
152
|
+
self._card_meta: dict[str, dict[str, Any]] = {}
|
|
153
|
+
|
|
154
|
+
def record_step_lineage(
|
|
155
|
+
self,
|
|
156
|
+
step_id: str,
|
|
157
|
+
action_name: str,
|
|
158
|
+
action_hash: str,
|
|
159
|
+
parent_cards: list[MCard] | dict[str, MCard],
|
|
160
|
+
output_cards: list[MCard] | dict[str, MCard],
|
|
161
|
+
receipt: MCard | None = None,
|
|
162
|
+
) -> list[LineageRecord]:
|
|
163
|
+
"""Record causal derivation edges for all outputs produced in a step."""
|
|
164
|
+
now = datetime.datetime.now(datetime.timezone.utc).isoformat()
|
|
165
|
+
|
|
166
|
+
parents = list(parent_cards.values()) if isinstance(parent_cards, dict) else list(parent_cards)
|
|
167
|
+
outputs = list(output_cards.values()) if isinstance(output_cards, dict) else list(output_cards)
|
|
168
|
+
|
|
169
|
+
def _extract_hash(card: Any) -> str:
|
|
170
|
+
if card is None:
|
|
171
|
+
return ""
|
|
172
|
+
h = getattr(card, "hash", None) or getattr(card, "content_hash", None)
|
|
173
|
+
return str(h) if h else ""
|
|
174
|
+
|
|
175
|
+
parent_uris = [p.uri for p in parents if p and getattr(p, "uri", None)]
|
|
176
|
+
parent_hashes = [_extract_hash(p) for p in parents if p]
|
|
177
|
+
|
|
178
|
+
receipt_uri = receipt.uri if receipt and getattr(receipt, "uri", None) else None
|
|
179
|
+
receipt_hash = _extract_hash(receipt) if receipt else None
|
|
180
|
+
|
|
181
|
+
records: list[LineageRecord] = []
|
|
182
|
+
for out in outputs:
|
|
183
|
+
out_hash = _extract_hash(out)
|
|
184
|
+
out_uri = out.uri if getattr(out, "uri", None) else f"mcard://outputs/{out_hash[:16]}"
|
|
185
|
+
|
|
186
|
+
# Embed Merkle lineage into MCard metadata (AC-395-03-01)
|
|
187
|
+
lineage_block = {
|
|
188
|
+
"parents": parent_uris,
|
|
189
|
+
"parent_hashes": parent_hashes,
|
|
190
|
+
"action": {
|
|
191
|
+
"name": action_name,
|
|
192
|
+
"hash": action_hash,
|
|
193
|
+
},
|
|
194
|
+
"receipt": {
|
|
195
|
+
"uri": receipt_uri,
|
|
196
|
+
"hash": receipt_hash,
|
|
197
|
+
},
|
|
198
|
+
"step_id": step_id,
|
|
199
|
+
"timestamp": now,
|
|
200
|
+
}
|
|
201
|
+
if out.metadata is None:
|
|
202
|
+
out.metadata = {}
|
|
203
|
+
out.metadata["lineage"] = lineage_block
|
|
204
|
+
|
|
205
|
+
rec = LineageRecord(
|
|
206
|
+
output_uri=out_uri,
|
|
207
|
+
output_hash=out_hash,
|
|
208
|
+
parent_uris=parent_uris,
|
|
209
|
+
parent_hashes=parent_hashes,
|
|
210
|
+
action_name=action_name,
|
|
211
|
+
action_hash=action_hash,
|
|
212
|
+
receipt_uri=receipt_uri,
|
|
213
|
+
receipt_hash=receipt_hash,
|
|
214
|
+
step_id=step_id,
|
|
215
|
+
timestamp=now,
|
|
216
|
+
)
|
|
217
|
+
self._records[out_uri] = rec
|
|
218
|
+
norm_uri = self._normalize_uri(out_uri)
|
|
219
|
+
if norm_uri != out_uri:
|
|
220
|
+
self._records[norm_uri] = rec
|
|
221
|
+
if out_hash:
|
|
222
|
+
self._hash_to_uri[out_hash] = out_uri
|
|
223
|
+
records.append(rec)
|
|
224
|
+
|
|
225
|
+
# Record parents metadata
|
|
226
|
+
for p in parents:
|
|
227
|
+
if p.uri and p.uri not in self._card_meta:
|
|
228
|
+
self._card_meta[p.uri] = {
|
|
229
|
+
"label": p.uri.split("/")[-1],
|
|
230
|
+
"hash": _extract_hash(p),
|
|
231
|
+
"type": "mcard",
|
|
232
|
+
}
|
|
233
|
+
self._card_meta[self._normalize_uri(p.uri)] = self._card_meta[p.uri]
|
|
234
|
+
self._card_meta[out_uri] = {
|
|
235
|
+
"label": out_uri.split("/")[-1],
|
|
236
|
+
"hash": out_hash,
|
|
237
|
+
"type": "mcard",
|
|
238
|
+
}
|
|
239
|
+
self._card_meta[norm_uri] = self._card_meta[out_uri]
|
|
240
|
+
|
|
241
|
+
return records
|
|
242
|
+
|
|
243
|
+
@staticmethod
|
|
244
|
+
def _normalize_uri(uri: str) -> str:
|
|
245
|
+
if not uri:
|
|
246
|
+
return ""
|
|
247
|
+
if uri.startswith("mcard://"):
|
|
248
|
+
return f"mcard:{uri[8:].lstrip('/')}"
|
|
249
|
+
if uri.startswith("mcard:"):
|
|
250
|
+
return f"mcard://{uri[6:].lstrip('/')}"
|
|
251
|
+
return f"mcard:{uri.lstrip('/')}"
|
|
252
|
+
|
|
253
|
+
def get_mcard_lineage(self, mcard_uri_or_hash: str) -> LineageDAG:
|
|
254
|
+
"""Reconstruct complete provenance tree in <5ms (AC-395-03-02)."""
|
|
255
|
+
t_start = time.perf_counter()
|
|
256
|
+
|
|
257
|
+
target_uri = self._hash_to_uri.get(mcard_uri_or_hash, mcard_uri_or_hash)
|
|
258
|
+
dag = LineageDAG(target_uri=target_uri)
|
|
259
|
+
|
|
260
|
+
visited: set[str] = set()
|
|
261
|
+
queue = [target_uri]
|
|
262
|
+
|
|
263
|
+
while queue:
|
|
264
|
+
curr_uri = queue.pop(0)
|
|
265
|
+
if curr_uri in visited:
|
|
266
|
+
continue
|
|
267
|
+
visited.add(curr_uri)
|
|
268
|
+
|
|
269
|
+
# Node for this card
|
|
270
|
+
meta = self._card_meta.get(curr_uri) or self._card_meta.get(self._normalize_uri(curr_uri), {})
|
|
271
|
+
node_label = meta.get("label", curr_uri.split("/")[-1])
|
|
272
|
+
node_hash = meta.get("hash", "")
|
|
273
|
+
dag.add_node(LineageNode(id=curr_uri, type="mcard", label=node_label, hash=node_hash))
|
|
274
|
+
|
|
275
|
+
# Find if this card has derivation records
|
|
276
|
+
rec = self._records.get(curr_uri) or self._records.get(self._normalize_uri(curr_uri))
|
|
277
|
+
if rec:
|
|
278
|
+
# Add Action node and edges
|
|
279
|
+
action_id = f"action:{rec.action_name}:{rec.step_id}"
|
|
280
|
+
dag.add_node(LineageNode(id=action_id, type="action", label=rec.action_name, hash=rec.action_hash))
|
|
281
|
+
|
|
282
|
+
# Edges: Parents -> Action
|
|
283
|
+
for p_uri in rec.parent_uris:
|
|
284
|
+
dag.add_edge(LineageEdge(source=p_uri, target=action_id, relation="derives"))
|
|
285
|
+
if p_uri not in visited:
|
|
286
|
+
queue.append(p_uri)
|
|
287
|
+
|
|
288
|
+
# Edge: Action -> Output
|
|
289
|
+
dag.add_edge(LineageEdge(source=action_id, target=curr_uri, relation="transformed_by"))
|
|
290
|
+
|
|
291
|
+
# Receipt node and edge if present
|
|
292
|
+
if rec.receipt_uri:
|
|
293
|
+
dag.add_node(LineageNode(
|
|
294
|
+
id=rec.receipt_uri,
|
|
295
|
+
type="receipt",
|
|
296
|
+
label="VCardReceipt",
|
|
297
|
+
hash=rec.receipt_hash or "",
|
|
298
|
+
))
|
|
299
|
+
dag.add_edge(LineageEdge(source=rec.receipt_uri, target=curr_uri, relation="certified_by"))
|
|
300
|
+
|
|
301
|
+
t_elapsed = (time.perf_counter() - t_start) * 1000.0
|
|
302
|
+
dag.reconstruction_time_ms = round(t_elapsed, 3)
|
|
303
|
+
return dag
|
|
304
|
+
|
|
305
|
+
def reset(self) -> None:
|
|
306
|
+
"""Reset the registry state."""
|
|
307
|
+
self._records.clear()
|
|
308
|
+
self._hash_to_uri.clear()
|
|
309
|
+
self._card_meta.clear()
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
# Global default registry
|
|
313
|
+
_default_lineage_registry = LineageRegistry()
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def get_default_lineage_registry() -> LineageRegistry:
|
|
317
|
+
return _default_lineage_registry
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def get_mcard_lineage(mcard_uri_or_hash: str) -> LineageDAG:
|
|
321
|
+
"""Public helper to query MCard provenance using the default registry."""
|
|
322
|
+
return _default_lineage_registry.get_mcard_lineage(mcard_uri_or_hash)
|
clm/layer2/mcard_fs.py
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
"""MCard Universal Virtual Filesystem Engine (Sprint 191/193/383).
|
|
2
|
+
|
|
3
|
+
Python parity with Rust `clm_rust_core::mcard_fs::MCardFileSystem`.
|
|
4
|
+
Relocated to Layer 2 (INV-00, INV-09, DEL-383-05).
|
|
5
|
+
Enforces:
|
|
6
|
+
1. Zero-FS Hermeticity (INV-09): Works with in-memory SQLite without host disk I/O.
|
|
7
|
+
2. Handle Registry Singularity (INV-04) & Tri-Database Separation (INV-05):
|
|
8
|
+
Wraps canonical `card`, `handle_registry`, and `handle_history` tables.
|
|
9
|
+
3. Content-Addressable Identity (INV-02): BLAKE3 verification on all reads.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import datetime
|
|
15
|
+
import json
|
|
16
|
+
import sqlite3
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from pydantic import BaseModel, ConfigDict
|
|
20
|
+
|
|
21
|
+
from clm.cross_cutting.domain_models import MCard
|
|
22
|
+
from clm.cross_cutting.domain_types import AgentDid, ContentHash, make_content_hash
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class MCardStat(BaseModel):
|
|
26
|
+
"""Metadata stat for a virtual MCard file."""
|
|
27
|
+
|
|
28
|
+
model_config = ConfigDict(frozen=True)
|
|
29
|
+
|
|
30
|
+
uri: str
|
|
31
|
+
hash: ContentHash
|
|
32
|
+
size_bytes: int
|
|
33
|
+
created_at: str
|
|
34
|
+
updated_at: str
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class MCardFileSystem:
|
|
38
|
+
"""Universal immutable filesystem engine mapping URIs to content-addressed MCards."""
|
|
39
|
+
|
|
40
|
+
def __init__(self, conn: sqlite3.Connection | None = None, context_prefix: str = "") -> None:
|
|
41
|
+
self.conn = conn or sqlite3.connect(":memory:")
|
|
42
|
+
self.context_prefix = context_prefix.strip("/")
|
|
43
|
+
self._init_schema()
|
|
44
|
+
|
|
45
|
+
def with_prefix(self, prefix: str) -> MCardFileSystem:
|
|
46
|
+
"""Returns a new MCardFileSystem handle isolated to the given context prefix."""
|
|
47
|
+
return MCardFileSystem(conn=self.conn, context_prefix=prefix)
|
|
48
|
+
|
|
49
|
+
def canonicalize_uri(self, path: str) -> str:
|
|
50
|
+
"""Resolves canonical URI with context prefix scoping (DOD-191-04)."""
|
|
51
|
+
clean = path.lstrip("/")
|
|
52
|
+
if clean.startswith("mcard:"):
|
|
53
|
+
rest_clean = clean[len("mcard:"):].lstrip("/")
|
|
54
|
+
if not self.context_prefix or rest_clean.startswith(self.context_prefix):
|
|
55
|
+
return f"mcard:{rest_clean}"
|
|
56
|
+
else:
|
|
57
|
+
return f"mcard:{self.context_prefix}/{rest_clean}"
|
|
58
|
+
elif not self.context_prefix:
|
|
59
|
+
return f"mcard:{clean}"
|
|
60
|
+
else:
|
|
61
|
+
return f"mcard:{self.context_prefix}/{clean}"
|
|
62
|
+
|
|
63
|
+
def _init_schema(self) -> None:
|
|
64
|
+
with self.conn:
|
|
65
|
+
self.conn.executescript(
|
|
66
|
+
"""
|
|
67
|
+
CREATE TABLE IF NOT EXISTS card (
|
|
68
|
+
hash TEXT PRIMARY KEY,
|
|
69
|
+
content TEXT NOT NULL,
|
|
70
|
+
g_time TEXT
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
CREATE TABLE IF NOT EXISTS handle_registry (
|
|
74
|
+
handle TEXT PRIMARY KEY,
|
|
75
|
+
current_hash TEXT NOT NULL,
|
|
76
|
+
created_at TEXT NOT NULL,
|
|
77
|
+
updated_at TEXT NOT NULL
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
CREATE TABLE IF NOT EXISTS handle_history (
|
|
81
|
+
handle TEXT NOT NULL,
|
|
82
|
+
previous_hash TEXT NOT NULL,
|
|
83
|
+
changed_at TEXT NOT NULL
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
CREATE INDEX IF NOT EXISTS idx_handle_reg_prefix ON handle_registry(handle);
|
|
87
|
+
"""
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def write_file(
|
|
91
|
+
self,
|
|
92
|
+
uri: str,
|
|
93
|
+
payload: Any,
|
|
94
|
+
author: AgentDid | str,
|
|
95
|
+
metadata: dict[str, Any] | None = None,
|
|
96
|
+
) -> MCard:
|
|
97
|
+
"""Writes an MCard into the filesystem and updates the handle registry."""
|
|
98
|
+
canonical_uri = self.canonicalize_uri(uri)
|
|
99
|
+
now = datetime.datetime.now(datetime.timezone.utc).isoformat()
|
|
100
|
+
|
|
101
|
+
def _do_write() -> MCard:
|
|
102
|
+
# 1. Determine existing handle state
|
|
103
|
+
cur = self.conn.execute(
|
|
104
|
+
"SELECT current_hash, created_at FROM handle_registry WHERE handle = ?",
|
|
105
|
+
(canonical_uri,),
|
|
106
|
+
)
|
|
107
|
+
existing = cur.fetchone()
|
|
108
|
+
|
|
109
|
+
if existing:
|
|
110
|
+
prev_hash, created_at = existing
|
|
111
|
+
hist_cur = self.conn.execute(
|
|
112
|
+
"SELECT count(*) FROM handle_history WHERE handle = ?", (canonical_uri,)
|
|
113
|
+
)
|
|
114
|
+
count = hist_cur.fetchone()[0]
|
|
115
|
+
seq = count + 2
|
|
116
|
+
else:
|
|
117
|
+
prev_hash = None
|
|
118
|
+
created_at = now
|
|
119
|
+
seq = 1
|
|
120
|
+
|
|
121
|
+
# 2. Construct canonical MCard
|
|
122
|
+
mcard = MCard.create(
|
|
123
|
+
uri=canonical_uri,
|
|
124
|
+
payload=payload,
|
|
125
|
+
author=author,
|
|
126
|
+
sequence=seq,
|
|
127
|
+
metadata=metadata,
|
|
128
|
+
)
|
|
129
|
+
serialized = json.dumps(mcard.model_dump(), sort_keys=True)
|
|
130
|
+
hash_str = str(mcard.hash)
|
|
131
|
+
|
|
132
|
+
# 3. Insert CAS card blob
|
|
133
|
+
self.conn.execute(
|
|
134
|
+
"INSERT OR IGNORE INTO card (hash, content, g_time) VALUES (?, ?, ?)",
|
|
135
|
+
(hash_str, serialized, now),
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# 4. Update history if hash changed
|
|
139
|
+
if prev_hash and prev_hash != hash_str:
|
|
140
|
+
self.conn.execute(
|
|
141
|
+
"INSERT INTO handle_history (handle, previous_hash, changed_at) VALUES (?, ?, ?)",
|
|
142
|
+
(canonical_uri, prev_hash, now),
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# 5. Insert or replace handle entry
|
|
146
|
+
self.conn.execute(
|
|
147
|
+
"INSERT OR REPLACE INTO handle_registry (handle, current_hash, created_at, updated_at) "
|
|
148
|
+
"VALUES (?, ?, ?, ?)",
|
|
149
|
+
(canonical_uri, hash_str, created_at, now),
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
return mcard
|
|
153
|
+
|
|
154
|
+
if self.conn.in_transaction:
|
|
155
|
+
return _do_write()
|
|
156
|
+
else:
|
|
157
|
+
with self.conn:
|
|
158
|
+
return _do_write()
|
|
159
|
+
|
|
160
|
+
def read_file(self, uri: str) -> MCard:
|
|
161
|
+
"""Reads and cryptographically verifies an MCard by handle URI."""
|
|
162
|
+
canonical_uri = self.canonicalize_uri(uri)
|
|
163
|
+
cur = self.conn.execute(
|
|
164
|
+
"SELECT current_hash FROM handle_registry WHERE handle = ?", (canonical_uri,)
|
|
165
|
+
)
|
|
166
|
+
row = cur.fetchone()
|
|
167
|
+
if not row:
|
|
168
|
+
raise KeyError(f"Handle '{canonical_uri}' not found in MCardFs")
|
|
169
|
+
|
|
170
|
+
current_hash = row[0]
|
|
171
|
+
card_cur = self.conn.execute(
|
|
172
|
+
"SELECT content FROM card WHERE hash = ?", (current_hash,)
|
|
173
|
+
)
|
|
174
|
+
card_row = card_cur.fetchone()
|
|
175
|
+
if not card_row:
|
|
176
|
+
raise ValueError(f"Card content for hash '{current_hash}' corrupted or missing")
|
|
177
|
+
|
|
178
|
+
data = json.loads(card_row[0])
|
|
179
|
+
mcard = MCard(**data)
|
|
180
|
+
|
|
181
|
+
# Cryptographic integrity check (INV-02)
|
|
182
|
+
from clm.layer0.utils import clm_hash_string
|
|
183
|
+
canonical_serialized = json.dumps(mcard.payload, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
|
|
184
|
+
expected_hash = clm_hash_string(canonical_serialized)
|
|
185
|
+
|
|
186
|
+
if expected_hash != current_hash:
|
|
187
|
+
raise ValueError(
|
|
188
|
+
f"Cryptographic hash mismatch for '{canonical_uri}': registry={current_hash} computed={expected_hash}"
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
return mcard
|
|
192
|
+
|
|
193
|
+
def exists(self, uri: str) -> bool:
|
|
194
|
+
"""Checks if a handle exists in the filesystem."""
|
|
195
|
+
canonical_uri = self.canonicalize_uri(uri)
|
|
196
|
+
cur = self.conn.execute(
|
|
197
|
+
"SELECT count(*) FROM handle_registry WHERE handle = ?", (canonical_uri,)
|
|
198
|
+
)
|
|
199
|
+
return cur.fetchone()[0] > 0
|
|
200
|
+
|
|
201
|
+
def stat_file(self, uri: str) -> MCardStat:
|
|
202
|
+
"""Returns stat metadata for a handle URI."""
|
|
203
|
+
canonical_uri = self.canonicalize_uri(uri)
|
|
204
|
+
cur = self.conn.execute(
|
|
205
|
+
"SELECT current_hash, created_at, updated_at FROM handle_registry WHERE handle = ?",
|
|
206
|
+
(canonical_uri,),
|
|
207
|
+
)
|
|
208
|
+
row = cur.fetchone()
|
|
209
|
+
if not row:
|
|
210
|
+
raise KeyError(f"Handle '{canonical_uri}' not found")
|
|
211
|
+
|
|
212
|
+
current_hash, created_at, updated_at = row
|
|
213
|
+
card_cur = self.conn.execute(
|
|
214
|
+
"SELECT length(content) FROM card WHERE hash = ?", (current_hash,)
|
|
215
|
+
)
|
|
216
|
+
card_row = card_cur.fetchone()
|
|
217
|
+
size_bytes = card_row[0] if card_row else 0
|
|
218
|
+
|
|
219
|
+
return MCardStat(
|
|
220
|
+
uri=canonical_uri,
|
|
221
|
+
hash=make_content_hash(current_hash),
|
|
222
|
+
size_bytes=size_bytes,
|
|
223
|
+
created_at=created_at,
|
|
224
|
+
updated_at=updated_at,
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
def list_dir(self, prefix: str) -> list[str]:
|
|
228
|
+
"""Lists all handles matching the specified prefix."""
|
|
229
|
+
canonical_prefix = self.canonicalize_uri(prefix)
|
|
230
|
+
if canonical_prefix.endswith("/"):
|
|
231
|
+
pattern = f"{canonical_prefix}%"
|
|
232
|
+
elif canonical_prefix == "mcard:":
|
|
233
|
+
pattern = "mcard:%"
|
|
234
|
+
else:
|
|
235
|
+
pattern = f"{canonical_prefix}/%"
|
|
236
|
+
|
|
237
|
+
cur = self.conn.execute(
|
|
238
|
+
"SELECT handle FROM handle_registry WHERE handle LIKE ? ORDER BY handle ASC",
|
|
239
|
+
(pattern,),
|
|
240
|
+
)
|
|
241
|
+
return [r[0] for r in cur.fetchall()]
|
|
242
|
+
|
|
243
|
+
def delete_file(self, uri: str) -> None:
|
|
244
|
+
"""Deregisters a handle URI while preserving the CAS content blob."""
|
|
245
|
+
canonical_uri = self.canonicalize_uri(uri)
|
|
246
|
+
now = datetime.datetime.now(datetime.timezone.utc).isoformat()
|
|
247
|
+
with self.conn:
|
|
248
|
+
cur = self.conn.execute(
|
|
249
|
+
"SELECT current_hash FROM handle_registry WHERE handle = ?", (canonical_uri,)
|
|
250
|
+
)
|
|
251
|
+
row = cur.fetchone()
|
|
252
|
+
if not row:
|
|
253
|
+
raise KeyError(f"Handle '{canonical_uri}' not found for deletion")
|
|
254
|
+
|
|
255
|
+
self.conn.execute(
|
|
256
|
+
"INSERT INTO handle_history (handle, previous_hash, changed_at) VALUES (?, ?, ?)",
|
|
257
|
+
(canonical_uri, row[0], now),
|
|
258
|
+
)
|
|
259
|
+
self.conn.execute("DELETE FROM handle_registry WHERE handle = ?", (canonical_uri,))
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
__all__ = ["MCardStat", "MCardFileSystem"]
|
clm/layer2/merkle.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Layer 2: Merkle Tree & Directory Ingestion.
|
|
2
|
+
|
|
3
|
+
Canonical Path: kernel/scripts/clm/layer2/merkle.py
|
|
4
|
+
Parity: Rust `src/layer2/merkle.rs`, TypeScript `src/layer2/merkleTree.ts`
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from clm.layer0.algebra_ingest import directory_fs_scan_py, directory_to_db_py
|
|
10
|
+
|
|
11
|
+
__all__ = ["directory_fs_scan_py", "directory_to_db_py"]
|