clm-kernel 0.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (333) hide show
  1. clm/__init__.py +230 -0
  2. clm/__main__.py +25 -0
  3. clm/cross_cutting/__init__.py +50 -0
  4. clm/cross_cutting/_test_runner.py +36 -0
  5. clm/cross_cutting/async_utils.py +25 -0
  6. clm/cross_cutting/cel_builder.py +123 -0
  7. clm/cross_cutting/cel_converter.py +697 -0
  8. clm/cross_cutting/cel_engine.py +252 -0
  9. clm/cross_cutting/cel_ops.py +72 -0
  10. clm/cross_cutting/cel_ops_db.py +157 -0
  11. clm/cross_cutting/cel_ops_files.py +99 -0
  12. clm/cross_cutting/cel_ops_state.py +71 -0
  13. clm/cross_cutting/cel_ops_strings.py +64 -0
  14. clm/cross_cutting/cel_resolver.py +63 -0
  15. clm/cross_cutting/cel_slices.py +56 -0
  16. clm/cross_cutting/cel_store.py +330 -0
  17. clm/cross_cutting/clm_logger.py +140 -0
  18. clm/cross_cutting/config/__init__.py +155 -0
  19. clm/cross_cutting/config/config_constants.py +392 -0
  20. clm/cross_cutting/config/env_parameters.py +90 -0
  21. clm/cross_cutting/config/logging.py +378 -0
  22. clm/cross_cutting/config/settings.py +504 -0
  23. clm/cross_cutting/context.py +5 -0
  24. clm/cross_cutting/domain_models.py +55 -0
  25. clm/cross_cutting/domain_types.py +61 -0
  26. clm/cross_cutting/effects.py +67 -0
  27. clm/cross_cutting/errors.py +153 -0
  28. clm/cross_cutting/governance.py +331 -0
  29. clm/cross_cutting/guards.py +41 -0
  30. clm/cross_cutting/mcard.py +321 -0
  31. clm/cross_cutting/native.py +185 -0
  32. clm/cross_cutting/observability.py +289 -0
  33. clm/cross_cutting/pcard.py +434 -0
  34. clm/cross_cutting/registry.py +71 -0
  35. clm/cross_cutting/resources.py +84 -0
  36. clm/cross_cutting/secrets.py +44 -0
  37. clm/cross_cutting/subprocess_utils.py +118 -0
  38. clm/cross_cutting/telemetry.py +164 -0
  39. clm/cross_cutting/testing/__init__.py +8 -0
  40. clm/cross_cutting/testing/comparator.py +187 -0
  41. clm/cross_cutting/testing/datasets.py +87 -0
  42. clm/cross_cutting/timeutil.py +20 -0
  43. clm/cross_cutting/types/new_type_check.py +0 -0
  44. clm/cross_cutting/utils/__init__.py +1 -0
  45. clm/cross_cutting/utils/url_safety.py +50 -0
  46. clm/cross_cutting/vcard.py +560 -0
  47. clm/cross_cutting/writer.py +118 -0
  48. clm/layer0/README.md +50 -0
  49. clm/layer0/__init__.py +223 -0
  50. clm/layer0/algebra.py +60 -0
  51. clm/layer0/algebra_baldwin.py +133 -0
  52. clm/layer0/algebra_baldwin_generators.py +36 -0
  53. clm/layer0/algebra_equivalence.py +262 -0
  54. clm/layer0/algebra_ingest.py +306 -0
  55. clm/layer0/algebra_manifest_io.py +156 -0
  56. clm/layer0/assembler.py +481 -0
  57. clm/layer0/cel/__init__.py +23 -0
  58. clm/layer0/cel_eval.py +491 -0
  59. clm/layer0/codec.py +106 -0
  60. clm/layer0/context.py +122 -0
  61. clm/layer0/db.py +160 -0
  62. clm/layer0/exceptions.py +73 -0
  63. clm/layer0/fibration.py +106 -0
  64. clm/layer0/gates.py +50 -0
  65. clm/layer0/hash.py +80 -0
  66. clm/layer0/loader.py +826 -0
  67. clm/layer0/math_utils.py +163 -0
  68. clm/layer0/mcard.py +96 -0
  69. clm/layer0/model/__init__.py +0 -0
  70. clm/layer0/model/action.py +223 -0
  71. clm/layer0/model/card.py +280 -0
  72. clm/layer0/model/card_triad.py +427 -0
  73. clm/layer0/model/compatibility.py +60 -0
  74. clm/layer0/model/dictionary.py +46 -0
  75. clm/layer0/model/dots.py +331 -0
  76. clm/layer0/model/event_producer.py +88 -0
  77. clm/layer0/model/g_time.py +89 -0
  78. clm/layer0/model/handle.py +163 -0
  79. clm/layer0/model/hash/__init__.py +6 -0
  80. clm/layer0/model/hash/algorithms/__init__.py +3 -0
  81. clm/layer0/model/hash/algorithms/custom_hash.py +7 -0
  82. clm/layer0/model/hash/algorithms/local_sha256.py +178 -0
  83. clm/layer0/model/hash/constants.py +53 -0
  84. clm/layer0/model/hash/enums.py +60 -0
  85. clm/layer0/model/hash/validator.py +307 -0
  86. clm/layer0/model/interpreter.py +275 -0
  87. clm/layer0/model/pagination.py +45 -0
  88. clm/layer0/model/pcard.py +716 -0
  89. clm/layer0/model/schema.py +44 -0
  90. clm/layer0/model/utils/__init__.py +1 -0
  91. clm/layer0/model/utils/content_analyzer.py +152 -0
  92. clm/layer0/model/validators/__init__.py +1 -0
  93. clm/layer0/model/validators/base_validator.py +44 -0
  94. clm/layer0/model/validators/binary_validator.py +66 -0
  95. clm/layer0/model/validators/text_validator.py +82 -0
  96. clm/layer0/model/validators/validation_registry.py +49 -0
  97. clm/layer0/model/vcard.py +823 -0
  98. clm/layer0/model/vcard_ext/__init__.py +39 -0
  99. clm/layer0/model/vcard_ext/core.py +60 -0
  100. clm/layer0/model/vcard_ext/network.py +23 -0
  101. clm/layer0/model/vcard_ext/observability.py +63 -0
  102. clm/layer0/model/vcard_ext/storage.py +60 -0
  103. clm/layer0/model/vcard_ext/vendors.py +211 -0
  104. clm/layer0/model/vcard_sandwich.py +119 -0
  105. clm/layer0/model/vcard_vocabulary.py +463 -0
  106. clm/layer0/model/workflow.py +274 -0
  107. clm/layer0/narrative.py +431 -0
  108. clm/layer0/ontology/collection.py +125 -0
  109. clm/layer0/parser.py +429 -0
  110. clm/layer0/relations.py +73 -0
  111. clm/layer0/rule_evaluator.py +251 -0
  112. clm/layer0/schema/__init__.py +38 -0
  113. clm/layer0/schema/ddl.py +97 -0
  114. clm/layer0/templates/abstract.yaml +99 -0
  115. clm/layer0/templates/balanced.yaml +253 -0
  116. clm/layer0/templates/concrete.yaml +172 -0
  117. clm/layer0/type_registry.py +223 -0
  118. clm/layer0/types.py +204 -0
  119. clm/layer0/utils.py +886 -0
  120. clm/layer0/verifier_core.py +262 -0
  121. clm/layer1/__init__.py +31 -0
  122. clm/layer1/action_dispatcher.py +275 -0
  123. clm/layer1/adapter_registry.py +233 -0
  124. clm/layer1/algebra_baldwin_generators.py +192 -0
  125. clm/layer1/arrow_compiler.py +525 -0
  126. clm/layer1/batch_processor.py +152 -0
  127. clm/layer1/cel/__init__.py +27 -0
  128. clm/layer1/cel/builder.py +173 -0
  129. clm/layer1/engine.py +759 -0
  130. clm/layer1/engine_async.py +89 -0
  131. clm/layer1/eval_callback.py +49 -0
  132. clm/layer1/gate_composer.py +229 -0
  133. clm/layer1/hoare_sandwich.py +298 -0
  134. clm/layer1/identity_guard.py +348 -0
  135. clm/layer1/matrix.py +138 -0
  136. clm/layer1/neural_runtime.py +221 -0
  137. clm/layer1/operations/__init__.py +123 -0
  138. clm/layer1/operations/augment.py +156 -0
  139. clm/layer1/operations/base.py +109 -0
  140. clm/layer1/operations/builtins.py +338 -0
  141. clm/layer1/operations/dispatch.py +144 -0
  142. clm/layer1/operations/exclude.py +107 -0
  143. clm/layer1/operations/handle.py +197 -0
  144. clm/layer1/operations/identity.py +139 -0
  145. clm/layer1/operations/invert.py +66 -0
  146. clm/layer1/operations/loader.py +116 -0
  147. clm/layer1/operations/manipulators/__init__.py +23 -0
  148. clm/layer1/operations/manipulators/document.py +211 -0
  149. clm/layer1/operations/manipulators/media.py +134 -0
  150. clm/layer1/operations/manipulators/structured.py +159 -0
  151. clm/layer1/operations/manipulators/tabular.py +334 -0
  152. clm/layer1/operations/port.py +65 -0
  153. clm/layer1/operations/services.py +529 -0
  154. clm/layer1/operations/split.py +117 -0
  155. clm/layer1/operations/substitute.py +75 -0
  156. clm/layer1/petri.py +1052 -0
  157. clm/layer1/pocketflow_orchestrator.py +408 -0
  158. clm/layer1/pocketflow_scheduler.py +519 -0
  159. clm/layer1/protocols.py +197 -0
  160. clm/layer1/runtime_adapter.py +718 -0
  161. clm/layer1/runtimes/__init__.py +71 -0
  162. clm/layer1/runtimes/_generated_assertions.py +125 -0
  163. clm/layer1/runtimes/_generated_types.py +42 -0
  164. clm/layer1/runtimes/base.py +265 -0
  165. clm/layer1/runtimes/binary.py +101 -0
  166. clm/layer1/runtimes/factory.py +203 -0
  167. clm/layer1/runtimes/javascript.py +321 -0
  168. clm/layer1/runtimes/javascript_runtime.js +33 -0
  169. clm/layer1/runtimes/js_scripts/isomorphism.mjs +22 -0
  170. clm/layer1/runtimes/js_scripts/loader.mjs +52 -0
  171. clm/layer1/runtimes/lambda_calc.py +126 -0
  172. clm/layer1/runtimes/python.py +288 -0
  173. clm/layer1/runtimes/python_runtime.py +41 -0
  174. clm/layer1/runtimes/script.py +114 -0
  175. clm/layer1/sandbox.py +334 -0
  176. clm/layer1/savepoint_guard.py +136 -0
  177. clm/layer1/sparse_net.py +122 -0
  178. clm/layer1/spec_tester.py +165 -0
  179. clm/layer1/store.py +295 -0
  180. clm/layer1/test_runner.py +498 -0
  181. clm/layer1/type_inspector.py +166 -0
  182. clm/layer1/verifier.py +772 -0
  183. clm/layer1/vm.py +91 -0
  184. clm/layer2/__init__.py +78 -0
  185. clm/layer2/card_collection.py +678 -0
  186. clm/layer2/classifier.py +479 -0
  187. clm/layer2/engine/__init__.py +1 -0
  188. clm/layer2/engine/abstract_sql_engine.py +320 -0
  189. clm/layer2/engine/base.py +193 -0
  190. clm/layer2/engine/duckdb_engine.py +576 -0
  191. clm/layer2/engine/sqlite_engine.py +518 -0
  192. clm/layer2/fiber/__init__.py +31 -0
  193. clm/layer2/fiber/context.py +89 -0
  194. clm/layer2/fiber/disposable.py +163 -0
  195. clm/layer2/fiber/fiber.py +125 -0
  196. clm/layer2/fiber/hoare.py +191 -0
  197. clm/layer2/file_io.py +376 -0
  198. clm/layer2/file_registrar.py +77 -0
  199. clm/layer2/ingest.py +202 -0
  200. clm/layer2/lineage.py +322 -0
  201. clm/layer2/mcard_fs.py +262 -0
  202. clm/layer2/merkle.py +11 -0
  203. clm/layer2/mime.py +244 -0
  204. clm/layer2/mime_detector.py +245 -0
  205. clm/layer2/private_collection.py +118 -0
  206. clm/layer2/shared_store.py +11 -0
  207. clm/layer2/storage/__init__.py +190 -0
  208. clm/layer2/storage/db_bitemporal.py +141 -0
  209. clm/layer2/storage/db_driver.py +331 -0
  210. clm/layer2/storage/db_hyperlinks.py +214 -0
  211. clm/layer2/storage/db_pool.py +139 -0
  212. clm/layer2/storage/db_repository.py +229 -0
  213. clm/layer2/storage/savepoint.py +78 -0
  214. clm/layer2/storage/sqlite_wal.py +74 -0
  215. clm/layer2/tridb.py +245 -0
  216. clm/layer2/vcard_receipt.py +71 -0
  217. clm/layer2/vfs.py +17 -0
  218. clm/layer3/__init__.py +141 -0
  219. clm/layer3/agency/__init__.py +19 -0
  220. clm/layer3/agency/config.py +220 -0
  221. clm/layer3/agency/providers/__init__.py +15 -0
  222. clm/layer3/agency/providers/base.py +91 -0
  223. clm/layer3/agency/providers/mlc_llm.py +169 -0
  224. clm/layer3/agency/providers/ollama.py +203 -0
  225. clm/layer3/agency/router.py +71 -0
  226. clm/layer3/agency/runtime.py +524 -0
  227. clm/layer3/gateway.py +353 -0
  228. clm/layer3/open_interpreter_adapter.py +159 -0
  229. clm/layer3/rag/__init__.py +70 -0
  230. clm/layer3/rag/cli.py +228 -0
  231. clm/layer3/rag/config.py +153 -0
  232. clm/layer3/rag/embeddings/__init__.py +16 -0
  233. clm/layer3/rag/embeddings/base.py +79 -0
  234. clm/layer3/rag/embeddings/ollama.py +211 -0
  235. clm/layer3/rag/embeddings/vision.py +290 -0
  236. clm/layer3/rag/engine.py +276 -0
  237. clm/layer3/rag/graph/__init__.py +22 -0
  238. clm/layer3/rag/graph/community.py +198 -0
  239. clm/layer3/rag/graph/engine.py +433 -0
  240. clm/layer3/rag/graph/extractor.py +255 -0
  241. clm/layer3/rag/graph/schema.py +115 -0
  242. clm/layer3/rag/graph/store.py +641 -0
  243. clm/layer3/rag/indexer.py +333 -0
  244. clm/layer3/rag/llm_providers.json +52 -0
  245. clm/layer3/rag/semantic_versioning.py +313 -0
  246. clm/layer3/rag/vector/__init__.py +28 -0
  247. clm/layer3/rag/vector/handle_vector_store.py +749 -0
  248. clm/layer3/rag/vector/schema.py +151 -0
  249. clm/layer3/rag/vector/store.py +632 -0
  250. clm/layer3/reticulum/__init__.py +55 -0
  251. clm/layer3/reticulum/bridge.py +74 -0
  252. clm/layer3/reticulum/daemon.py +88 -0
  253. clm/layer3/reticulum/discovery.py +164 -0
  254. clm/layer3/reticulum/identity.py +107 -0
  255. clm/layer3/reticulum/media.py +148 -0
  256. clm/layer3/reticulum/transport.py +133 -0
  257. clm/layer3/satori/__init__.py +209 -0
  258. clm/layer3/satori/alpha_conversion.py +168 -0
  259. clm/layer3/satori/beta_reduction.py +359 -0
  260. clm/layer3/satori/continuation.py +38 -0
  261. clm/layer3/satori/eta_conversion.py +205 -0
  262. clm/layer3/satori/free_variables.py +163 -0
  263. clm/layer3/satori/io_effects.py +370 -0
  264. clm/layer3/satori/lambda_runtime.py +726 -0
  265. clm/layer3/satori/lambda_term.py +230 -0
  266. clm/layer3/satori/sheaf_audit.py +261 -0
  267. clm/layer3/satori/speech_act.py +279 -0
  268. clm/layer4/README.md +9 -0
  269. clm/layer4/__init__.py +67 -0
  270. clm/layer4/_sovereign_executor.py +580 -0
  271. clm/layer4/action_commands.py +719 -0
  272. clm/layer4/api.py +236 -0
  273. clm/layer4/bootstrap/__init__.py +13 -0
  274. clm/layer4/bootstrap/bootstrapper.py +118 -0
  275. clm/layer4/bootstrap/cleanup.py +44 -0
  276. clm/layer4/bootstrap/clm_hooks.py +49 -0
  277. clm/layer4/bootstrap/genesis.py +77 -0
  278. clm/layer4/bootstrap/injection.py +125 -0
  279. clm/layer4/cli.py +549 -0
  280. clm/layer4/collection_manager.py +553 -0
  281. clm/layer4/commands/__init__.py +81 -0
  282. clm/layer4/commands/action_cmd.py +42 -0
  283. clm/layer4/commands/check_cmd.py +82 -0
  284. clm/layer4/commands/envelope.py +73 -0
  285. clm/layer4/commands/evaluate_cmd.py +224 -0
  286. clm/layer4/commands/mcard_cmd.py +24 -0
  287. clm/layer4/commands/petri_cmd.py +92 -0
  288. clm/layer4/commands/tridb_cmd.py +319 -0
  289. clm/layer4/commands/version_cmd.py +27 -0
  290. clm/layer4/diff_commands.py +167 -0
  291. clm/layer4/eoa_sponsor.py +232 -0
  292. clm/layer4/events.py +69 -0
  293. clm/layer4/flux_gateway.py +133 -0
  294. clm/layer4/gateway.py +91 -0
  295. clm/layer4/loader.py +307 -0
  296. clm/layer4/mcard_commands.py +166 -0
  297. clm/layer4/membrane.py +273 -0
  298. clm/layer4/navigation.py +50 -0
  299. clm/layer4/protocol/protocol_entry_python.py +108 -0
  300. clm/layer4/protocol/python_sync.py +126 -0
  301. clm/layer4/runner.py +150 -0
  302. clm/layer4/satori.py +262 -0
  303. clm/layer4/server/python_sync_client.py +150 -0
  304. clm/layer4/server/python_sync_server.py +139 -0
  305. clm/layer4/server/service_api.py +315 -0
  306. clm/layer4/server/start_servers.py +107 -0
  307. clm/layer4/server/stop_servers.py +24 -0
  308. clm/layer4/simulated_optimization_fix.py +0 -0
  309. clm/layer4/storage.py +463 -0
  310. clm/layer4/tridb_admin.py +268 -0
  311. clm/layer4/websocket/test_ws_client.py +104 -0
  312. clm/layer4/websocket/test_ws_server.py +138 -0
  313. clm/layer4/websocket/ws_server_local.py +160 -0
  314. clm/layer5/__init__.py +51 -0
  315. clm/layer5/async_batch_writer.py +133 -0
  316. clm/layer5/learning.py +276 -0
  317. clm/layer5/telemetry_hook.py +107 -0
  318. clm/layer5/type_lattice.py +238 -0
  319. clm/py.typed +0 -0
  320. clm/schemas/__init__.py +652 -0
  321. clm/schemas/abstract_face_schema.yaml +77 -0
  322. clm/schemas/cel_schema.yaml +416 -0
  323. clm/schemas/lifecycle/__init__.py +45 -0
  324. clm/schemas/lifecycle/models.py +123 -0
  325. clm/schemas/lifecycle/service.py +364 -0
  326. clm/schemas/mcard_schema.sql +192 -0
  327. clm/schemas/mcard_vector_schema.sql +283 -0
  328. clm/schemas/mlp-context-v1.json +91 -0
  329. clm_kernel-0.0.0.dist-info/METADATA +121 -0
  330. clm_kernel-0.0.0.dist-info/RECORD +333 -0
  331. clm_kernel-0.0.0.dist-info/WHEEL +4 -0
  332. clm_kernel-0.0.0.dist-info/entry_points.txt +3 -0
  333. clm_kernel-0.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,641 @@
1
+ """
2
+ Graph Store
3
+
4
+ SQLite-based knowledge graph storage with traversal capabilities.
5
+ """
6
+
7
+ import json
8
+ import logging
9
+ import sqlite3
10
+ from datetime import datetime, timezone
11
+ from typing import Any
12
+
13
+ from .extractor import Entity, ExtractionResult
14
+ from .schema import GRAPH_SCHEMAS
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ # ─────────────────────────────────────────────────────────────────────────────
20
+ # Graph Store
21
+ # ─────────────────────────────────────────────────────────────────────────────
22
+
23
+
24
+ class GraphStore:
25
+ """
26
+ SQLite-based storage for knowledge graph.
27
+
28
+ Features:
29
+ - Entity and relationship storage
30
+ - Graph traversal (BFS)
31
+ - Multi-hop pathfinding
32
+ - Community management
33
+
34
+ Usage:
35
+ store = GraphStore(db_path="graph.db")
36
+
37
+ # Add entities
38
+ entity_id = store.add_entity(entity, source_hash)
39
+
40
+ # Add relationships
41
+ store.add_relationship(source_id, target_id, "relates_to", source_hash)
42
+
43
+ # Find related entities
44
+ related = store.find_related("MCard", hops=2)
45
+ """
46
+
47
+ def __init__(self, db_path: str = ":memory:"):
48
+ """
49
+ Initialize graph store.
50
+
51
+ Args:
52
+ db_path: Path to SQLite database
53
+ """
54
+ self.db_path = db_path
55
+ self.conn = self._init_database()
56
+
57
+ def _init_database(self) -> sqlite3.Connection:
58
+ """Initialize SQLite connection and create tables."""
59
+ conn = sqlite3.connect(self.db_path, check_same_thread=False)
60
+ cursor = conn.cursor()
61
+
62
+ # Create all graph tables
63
+ for _name, schema in GRAPH_SCHEMAS.items():
64
+ cursor.execute(schema)
65
+
66
+ conn.commit()
67
+ logger.debug(f"Initialized graph store at {self.db_path}")
68
+ return conn
69
+
70
+ # ─────────────────────────────────────────────────────────────────────────
71
+ # Entity Operations
72
+ # ─────────────────────────────────────────────────────────────────────────
73
+
74
+ def add_entity(
75
+ self, entity: Entity, source_hash: str, embedding: bytes = None
76
+ ) -> int:
77
+ """
78
+ Add an entity to the graph.
79
+
80
+ Args:
81
+ entity: Entity to add
82
+ source_hash: Source MCard hash
83
+ embedding: Optional entity embedding
84
+
85
+ Returns:
86
+ Entity ID
87
+ """
88
+ now = datetime.now(timezone.utc).isoformat()
89
+ cursor = self.conn.cursor()
90
+
91
+ try:
92
+ cursor.execute(
93
+ """
94
+ INSERT INTO graph_entities (name, type, description, source_hash, embedding, created_at)
95
+ VALUES (?, ?, ?, ?, ?, ?)
96
+ """,
97
+ (
98
+ entity.name,
99
+ entity.type,
100
+ entity.description,
101
+ source_hash,
102
+ embedding,
103
+ now,
104
+ ),
105
+ )
106
+ self.conn.commit()
107
+ return cursor.lastrowid
108
+
109
+ except sqlite3.IntegrityError:
110
+ # Entity already exists, get its ID
111
+ cursor.execute(
112
+ """
113
+ SELECT id FROM graph_entities
114
+ WHERE name = ? AND type = ? AND source_hash = ?
115
+ """,
116
+ (entity.name, entity.type, source_hash),
117
+ )
118
+ row = cursor.fetchone()
119
+ return row[0] if row else -1
120
+
121
+ def get_entity_by_name(self, name: str) -> dict[str, Any] | None:
122
+ """Get entity by name (case-insensitive)."""
123
+ cursor = self.conn.cursor()
124
+ cursor.execute(
125
+ """
126
+ SELECT id, name, type, description, source_hash, created_at
127
+ FROM graph_entities
128
+ WHERE LOWER(name) = LOWER(?)
129
+ LIMIT 1
130
+ """,
131
+ (name,),
132
+ )
133
+
134
+ row = cursor.fetchone()
135
+ if row:
136
+ return {
137
+ "id": row[0],
138
+ "name": row[1],
139
+ "type": row[2],
140
+ "description": row[3],
141
+ "source_hash": row[4],
142
+ "created_at": row[5],
143
+ }
144
+ return None
145
+
146
+ def get_entity_by_id(self, entity_id: int) -> dict[str, Any] | None:
147
+ """Get entity by ID."""
148
+ cursor = self.conn.cursor()
149
+ cursor.execute(
150
+ """
151
+ SELECT id, name, type, description, source_hash, created_at
152
+ FROM graph_entities
153
+ WHERE id = ?
154
+ """,
155
+ (entity_id,),
156
+ )
157
+
158
+ row = cursor.fetchone()
159
+ if row:
160
+ return {
161
+ "id": row[0],
162
+ "name": row[1],
163
+ "type": row[2],
164
+ "description": row[3],
165
+ "source_hash": row[4],
166
+ "created_at": row[5],
167
+ }
168
+ return None
169
+
170
+ def search_entities(
171
+ self, query: str, type_filter: str = None, limit: int = 10
172
+ ) -> list[dict[str, Any]]:
173
+ """Search entities by name pattern."""
174
+ cursor = self.conn.cursor()
175
+
176
+ if type_filter:
177
+ cursor.execute(
178
+ """
179
+ SELECT id, name, type, description, source_hash
180
+ FROM graph_entities
181
+ WHERE name LIKE ? AND type = ?
182
+ LIMIT ?
183
+ """,
184
+ (f"%{query}%", type_filter, limit),
185
+ )
186
+ else:
187
+ cursor.execute(
188
+ """
189
+ SELECT id, name, type, description, source_hash
190
+ FROM graph_entities
191
+ WHERE name LIKE ?
192
+ LIMIT ?
193
+ """,
194
+ (f"%{query}%", limit),
195
+ )
196
+
197
+ return [
198
+ {
199
+ "id": r[0],
200
+ "name": r[1],
201
+ "type": r[2],
202
+ "description": r[3],
203
+ "source_hash": r[4],
204
+ }
205
+ for r in cursor.fetchall()
206
+ ]
207
+
208
+ def get_entities_by_source(self, source_hash: str) -> list[dict[str, Any]]:
209
+ """Get all entities from a source MCard."""
210
+ cursor = self.conn.cursor()
211
+ cursor.execute(
212
+ """
213
+ SELECT id, name, type, description
214
+ FROM graph_entities
215
+ WHERE source_hash = ?
216
+ """,
217
+ (source_hash,),
218
+ )
219
+
220
+ return [
221
+ {"id": r[0], "name": r[1], "type": r[2], "description": r[3]}
222
+ for r in cursor.fetchall()
223
+ ]
224
+
225
+ # ─────────────────────────────────────────────────────────────────────────
226
+ # Community Operations
227
+ # ─────────────────────────────────────────────────────────────────────────
228
+
229
+ def add_community(
230
+ self,
231
+ title: str,
232
+ summary: str,
233
+ member_ids: list[int],
234
+ level: int = 0,
235
+ parent_id: int | None = None,
236
+ embedding: bytes = None,
237
+ ) -> int:
238
+ """Add a community summary."""
239
+ now = datetime.now(timezone.utc).isoformat()
240
+ cursor = self.conn.cursor()
241
+
242
+ cursor.execute(
243
+ """
244
+ INSERT INTO graph_communities
245
+ (title, summary, member_entity_ids, level, parent_community_id, embedding, created_at)
246
+ VALUES (?, ?, ?, ?, ?, ?, ?)
247
+ """,
248
+ (title, summary, json.dumps(member_ids), level, parent_id, embedding, now),
249
+ )
250
+
251
+ self.conn.commit()
252
+ return cursor.lastrowid
253
+
254
+ def get_communities(self, level: int = 0) -> list[dict[str, Any]]:
255
+ """Get communities by level."""
256
+ cursor = self.conn.cursor()
257
+ cursor.execute(
258
+ """
259
+ SELECT id, title, summary, member_entity_ids, level
260
+ FROM graph_communities
261
+ WHERE level = ?
262
+ """,
263
+ (level,),
264
+ )
265
+
266
+ results = []
267
+ for row in cursor.fetchall():
268
+ results.append(
269
+ {
270
+ "id": row[0],
271
+ "title": row[1],
272
+ "summary": row[2],
273
+ "member_ids": json.loads(row[3]) if row[3] else [],
274
+ "level": row[4],
275
+ }
276
+ )
277
+ return results
278
+
279
+ # ─────────────────────────────────────────────────────────────────────────
280
+ # Relationship Operations
281
+ # ─────────────────────────────────────────────────────────────────────────
282
+
283
+ def add_relationship(
284
+ self,
285
+ source_entity_id: int,
286
+ target_entity_id: int,
287
+ relationship: str,
288
+ source_hash: str,
289
+ description: str = "",
290
+ weight: float = 1.0,
291
+ ) -> int:
292
+ """
293
+ Add a relationship between entities.
294
+
295
+ Args:
296
+ source_entity_id: Source entity ID
297
+ target_entity_id: Target entity ID
298
+ relationship: Relationship type/verb
299
+ source_hash: Source MCard hash
300
+ description: Optional description
301
+ weight: Relationship weight
302
+
303
+ Returns:
304
+ Relationship ID
305
+ """
306
+ now = datetime.now(timezone.utc).isoformat()
307
+ cursor = self.conn.cursor()
308
+
309
+ try:
310
+ cursor.execute(
311
+ """
312
+ INSERT INTO graph_relationships
313
+ (source_entity_id, target_entity_id, relationship, description, weight, source_hash, created_at)
314
+ VALUES (?, ?, ?, ?, ?, ?, ?)
315
+ """,
316
+ (
317
+ source_entity_id,
318
+ target_entity_id,
319
+ relationship,
320
+ description,
321
+ weight,
322
+ source_hash,
323
+ now,
324
+ ),
325
+ )
326
+ self.conn.commit()
327
+ return cursor.lastrowid
328
+
329
+ except sqlite3.IntegrityError:
330
+ # Relationship already exists
331
+ return -1
332
+
333
+ def get_relationships_from(self, entity_id: int) -> list[dict[str, Any]]:
334
+ """Get outgoing relationships from an entity."""
335
+ cursor = self.conn.cursor()
336
+ cursor.execute(
337
+ """
338
+ SELECT r.id, r.relationship, r.description, r.weight,
339
+ e.id, e.name, e.type
340
+ FROM graph_relationships r
341
+ JOIN graph_entities e ON r.target_entity_id = e.id
342
+ WHERE r.source_entity_id = ?
343
+ """,
344
+ (entity_id,),
345
+ )
346
+
347
+ return [
348
+ {
349
+ "rel_id": r[0],
350
+ "relationship": r[1],
351
+ "description": r[2],
352
+ "weight": r[3],
353
+ "target_id": r[4],
354
+ "target_name": r[5],
355
+ "target_type": r[6],
356
+ }
357
+ for r in cursor.fetchall()
358
+ ]
359
+
360
+ def get_relationships_to(self, entity_id: int) -> list[dict[str, Any]]:
361
+ """Get incoming relationships to an entity."""
362
+ cursor = self.conn.cursor()
363
+ cursor.execute(
364
+ """
365
+ SELECT r.id, r.relationship, r.description, r.weight,
366
+ e.id, e.name, e.type
367
+ FROM graph_relationships r
368
+ JOIN graph_entities e ON r.source_entity_id = e.id
369
+ WHERE r.target_entity_id = ?
370
+ """,
371
+ (entity_id,),
372
+ )
373
+
374
+ return [
375
+ {
376
+ "rel_id": r[0],
377
+ "relationship": r[1],
378
+ "description": r[2],
379
+ "weight": r[3],
380
+ "source_id": r[4],
381
+ "source_name": r[5],
382
+ "source_type": r[6],
383
+ }
384
+ for r in cursor.fetchall()
385
+ ]
386
+
387
+ # ─────────────────────────────────────────────────────────────────────────
388
+ # Graph Traversal
389
+ # ─────────────────────────────────────────────────────────────────────────
390
+
391
+ def find_related(
392
+ self, entity_name: str, hops: int = 2, direction: str = "both"
393
+ ) -> list[dict[str, Any]]:
394
+ """
395
+ Find entities within N hops of a given entity.
396
+
397
+ Args:
398
+ entity_name: Starting entity name
399
+ hops: Maximum traversal depth
400
+ direction: 'outgoing', 'incoming', or 'both'
401
+
402
+ Returns:
403
+ List of related entities with their paths
404
+ """
405
+ # Find starting entity
406
+ start = self.get_entity_by_name(entity_name)
407
+ if not start:
408
+ return []
409
+
410
+ visited: set[int] = {start["id"]}
411
+ results = []
412
+ frontier = [(start["id"], 0, [start["name"]])] # (id, depth, path)
413
+
414
+ while frontier:
415
+ current_id, depth, path = frontier.pop(0)
416
+
417
+ if depth >= hops:
418
+ continue
419
+
420
+ # Get adjacent entities
421
+ neighbors = []
422
+
423
+ if direction in ("outgoing", "both"):
424
+ for rel in self.get_relationships_from(current_id):
425
+ neighbors.append(
426
+ (
427
+ rel["target_id"],
428
+ rel["target_name"],
429
+ rel["relationship"],
430
+ "outgoing",
431
+ )
432
+ )
433
+
434
+ if direction in ("incoming", "both"):
435
+ for rel in self.get_relationships_to(current_id):
436
+ neighbors.append(
437
+ (
438
+ rel["source_id"],
439
+ rel["source_name"],
440
+ rel["relationship"],
441
+ "incoming",
442
+ )
443
+ )
444
+
445
+ for neighbor_id, neighbor_name, rel, _dir in neighbors:
446
+ if neighbor_id not in visited:
447
+ visited.add(neighbor_id)
448
+ new_path = path + [f"--{rel}->", neighbor_name]
449
+
450
+ entity = self.get_entity_by_id(neighbor_id)
451
+ if entity:
452
+ results.append(
453
+ {
454
+ "entity": entity,
455
+ "depth": depth + 1,
456
+ "path": new_path,
457
+ "relationship": rel,
458
+ }
459
+ )
460
+
461
+ frontier.append((neighbor_id, depth + 1, new_path))
462
+
463
+ return results
464
+
465
+ def find_path(
466
+ self, source_name: str, target_name: str, max_depth: int = 4
467
+ ) -> list[str] | None:
468
+ """
469
+ Find shortest path between two entities.
470
+
471
+ Args:
472
+ source_name: Starting entity name
473
+ target_name: Target entity name
474
+ max_depth: Maximum path length
475
+
476
+ Returns:
477
+ List of entity names in path, or None if no path
478
+ """
479
+ source = self.get_entity_by_name(source_name)
480
+ target = self.get_entity_by_name(target_name)
481
+
482
+ if not source or not target:
483
+ return None
484
+
485
+ if source["id"] == target["id"]:
486
+ return [source_name]
487
+
488
+ # BFS for shortest path
489
+ visited = {source["id"]}
490
+ queue = [(source["id"], [source["name"]])]
491
+
492
+ while queue:
493
+ current_id, path = queue.pop(0)
494
+
495
+ if len(path) > max_depth:
496
+ continue
497
+
498
+ for rel in self.get_relationships_from(current_id):
499
+ neighbor_id = rel["target_id"]
500
+
501
+ if neighbor_id == target["id"]:
502
+ return path + [f"--{rel['relationship']}->", target_name]
503
+
504
+ if neighbor_id not in visited:
505
+ visited.add(neighbor_id)
506
+ queue.append(
507
+ (
508
+ neighbor_id,
509
+ path + [f"--{rel['relationship']}->", rel["target_name"]],
510
+ )
511
+ )
512
+
513
+ return None
514
+
515
+ # ─────────────────────────────────────────────────────────────────────────
516
+ # Extraction Tracking
517
+ # ─────────────────────────────────────────────────────────────────────────
518
+
519
+ def mark_extracted(self, source_hash: str, entity_count: int, rel_count: int):
520
+ """Mark a source as having been extracted."""
521
+ now = datetime.now(timezone.utc).isoformat()
522
+ cursor = self.conn.cursor()
523
+
524
+ cursor.execute(
525
+ """
526
+ INSERT OR REPLACE INTO graph_extractions
527
+ (hash, entity_count, relationship_count, extracted_at)
528
+ VALUES (?, ?, ?, ?)
529
+ """,
530
+ (source_hash, entity_count, rel_count, now),
531
+ )
532
+
533
+ self.conn.commit()
534
+
535
+ def is_extracted(self, source_hash: str) -> bool:
536
+ """Check if a source has been extracted."""
537
+ cursor = self.conn.cursor()
538
+ cursor.execute("SELECT 1 FROM graph_extractions WHERE hash = ?", (source_hash,))
539
+ return cursor.fetchone() is not None
540
+
541
+ # ─────────────────────────────────────────────────────────────────────────
542
+ # Statistics
543
+ # ─────────────────────────────────────────────────────────────────────────
544
+
545
+ def count_entities(self) -> int:
546
+ """Get total entity count."""
547
+ cursor = self.conn.cursor()
548
+ cursor.execute("SELECT COUNT(*) FROM graph_entities")
549
+ return cursor.fetchone()[0]
550
+
551
+ def count_relationships(self) -> int:
552
+ """Get total relationship count."""
553
+ cursor = self.conn.cursor()
554
+ cursor.execute("SELECT COUNT(*) FROM graph_relationships")
555
+ return cursor.fetchone()[0]
556
+
557
+ def count_extractions(self) -> int:
558
+ """Get count of extracted sources."""
559
+ cursor = self.conn.cursor()
560
+ cursor.execute("SELECT COUNT(*) FROM graph_extractions")
561
+ return cursor.fetchone()[0]
562
+
563
+ def get_entity_types(self) -> dict[str, int]:
564
+ """Get counts by entity type."""
565
+ cursor = self.conn.cursor()
566
+ cursor.execute(
567
+ """
568
+ SELECT type, COUNT(*) FROM graph_entities
569
+ GROUP BY type
570
+ ORDER BY COUNT(*) DESC
571
+ """
572
+ )
573
+ return {row[0]: row[1] for row in cursor.fetchall()}
574
+
575
+ def get_stats(self) -> dict[str, Any]:
576
+ """Get graph statistics."""
577
+ return {
578
+ "entity_count": self.count_entities(),
579
+ "relationship_count": self.count_relationships(),
580
+ "extraction_count": self.count_extractions(),
581
+ "entity_types": self.get_entity_types(),
582
+ }
583
+
584
+ def clear(self):
585
+ """Clear all graph data."""
586
+ cursor = self.conn.cursor()
587
+ cursor.execute("DELETE FROM graph_relationships")
588
+ cursor.execute("DELETE FROM graph_entities")
589
+ cursor.execute("DELETE FROM graph_communities")
590
+ cursor.execute("DELETE FROM graph_extractions")
591
+ self.conn.commit()
592
+ logger.info("Graph store cleared")
593
+
594
+
595
+ # ─────────────────────────────────────────────────────────────────────────────
596
+ # Batch Operations
597
+ # ─────────────────────────────────────────────────────────────────────────────
598
+
599
+
600
+ def store_extraction_result(
601
+ store: GraphStore, result: ExtractionResult, source_hash: str
602
+ ) -> tuple[int, int]:
603
+ """
604
+ Store an extraction result in the graph.
605
+
606
+ Args:
607
+ store: GraphStore instance
608
+ result: ExtractionResult from GraphExtractor
609
+ source_hash: Source MCard hash
610
+
611
+ Returns:
612
+ Tuple of (entity_count, relationship_count) stored
613
+ """
614
+ if not result.success:
615
+ return 0, 0
616
+
617
+ entity_map = {} # name -> id
618
+
619
+ # Add entities
620
+ for entity in result.entities:
621
+ entity_id = store.add_entity(entity, source_hash)
622
+ if entity_id > 0:
623
+ entity_map[entity.name.lower()] = entity_id
624
+
625
+ # Add relationships
626
+ rel_count = 0
627
+ for rel in result.relationships:
628
+ source_id = entity_map.get(rel.source.lower())
629
+ target_id = entity_map.get(rel.target.lower())
630
+
631
+ if source_id and target_id:
632
+ rel_id = store.add_relationship(
633
+ source_id, target_id, rel.relationship, source_hash, rel.description
634
+ )
635
+ if rel_id > 0:
636
+ rel_count += 1
637
+
638
+ # Mark as extracted
639
+ store.mark_extracted(source_hash, len(entity_map), rel_count)
640
+
641
+ return len(entity_map), rel_count