codemesh 0.1.1__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 (52) hide show
  1. codemesh/__init__.py +5 -0
  2. codemesh/__main__.py +8 -0
  3. codemesh/cli/__init__.py +3 -0
  4. codemesh/cli/init.py +208 -0
  5. codemesh/cli/install_cmd.py +208 -0
  6. codemesh/cli/main.py +469 -0
  7. codemesh/context/__init__.py +3 -0
  8. codemesh/context/builder.py +388 -0
  9. codemesh/db/__init__.py +3 -0
  10. codemesh/db/connection.py +66 -0
  11. codemesh/db/queries.py +696 -0
  12. codemesh/db/schema.py +125 -0
  13. codemesh/embedding/__init__.py +3 -0
  14. codemesh/extraction/__init__.py +7 -0
  15. codemesh/extraction/languages/__init__.py +95 -0
  16. codemesh/extraction/languages/c_family.py +614 -0
  17. codemesh/extraction/languages/go.py +397 -0
  18. codemesh/extraction/languages/java.py +603 -0
  19. codemesh/extraction/languages/python.py +718 -0
  20. codemesh/extraction/languages/rust.py +435 -0
  21. codemesh/extraction/languages/swift.py +464 -0
  22. codemesh/extraction/languages/typescript.py +1222 -0
  23. codemesh/extraction/orchestrator.py +218 -0
  24. codemesh/graph/__init__.py +8 -0
  25. codemesh/graph/query_manager.py +117 -0
  26. codemesh/graph/traverser.py +107 -0
  27. codemesh/indexer.py +240 -0
  28. codemesh/mcp/__init__.py +3 -0
  29. codemesh/mcp/server.py +60 -0
  30. codemesh/mcp/tools.py +605 -0
  31. codemesh/querier.py +269 -0
  32. codemesh/resolution/__init__.py +7 -0
  33. codemesh/resolution/frameworks/__init__.py +15 -0
  34. codemesh/resolution/frameworks/django.py +30 -0
  35. codemesh/resolution/frameworks/fastapi.py +23 -0
  36. codemesh/resolution/import_resolver.py +69 -0
  37. codemesh/resolution/name_matcher.py +30 -0
  38. codemesh/resolution/resolver.py +268 -0
  39. codemesh/retrieval/__init__.py +7 -0
  40. codemesh/search/__init__.py +3 -0
  41. codemesh/sync/__init__.py +3 -0
  42. codemesh/sync/watcher.py +135 -0
  43. codemesh/types.py +148 -0
  44. codemesh/viz/__init__.py +0 -0
  45. codemesh/viz/graph_builder.py +162 -0
  46. codemesh/viz/server.py +122 -0
  47. codemesh/viz/templates/index.html +359 -0
  48. codemesh-0.1.1.dist-info/METADATA +337 -0
  49. codemesh-0.1.1.dist-info/RECORD +52 -0
  50. codemesh-0.1.1.dist-info/WHEEL +4 -0
  51. codemesh-0.1.1.dist-info/entry_points.txt +2 -0
  52. codemesh-0.1.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,603 @@
1
+ """Java tree-sitter extractor.
2
+
3
+ Handles:
4
+ - class_declaration with fields, methods, constructors
5
+ - interface_declaration
6
+ - method_declaration, constructor_declaration
7
+ - field_declaration (static final → CONSTANT, else VARIABLE)
8
+ - enum_declaration
9
+ - import_declaration (scoped_identifier like java.util.List)
10
+ - package_declaration
11
+ - method_invocation (call edges)
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import hashlib
17
+ import logging
18
+ from pathlib import Path # noqa: TC003
19
+ from typing import Any
20
+
21
+ from codemesh.types import Edge, EdgeKind, Language, Node, NodeKind
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ class JavaExtractor:
27
+ """Extracts Java code symbols from tree-sitter AST."""
28
+
29
+ def extract(
30
+ self,
31
+ file_path: Path,
32
+ source: bytes,
33
+ root_node: Any,
34
+ language: Language,
35
+ ) -> tuple[list[Node], list[Edge]]:
36
+ nodes: list[Node] = []
37
+ edges: list[Edge] = []
38
+
39
+ file_id = self._node_id(file_path, 1, root_node.end_point[0] + 1)
40
+ file_node = Node(
41
+ id=file_id,
42
+ kind=NodeKind.FILE,
43
+ name=file_path.name,
44
+ qualified_name=str(file_path),
45
+ file_path=file_path,
46
+ language=language,
47
+ start_line=1,
48
+ end_line=root_node.end_point[0] + 1,
49
+ )
50
+ nodes.append(file_node)
51
+
52
+ self._walk(source, root_node, file_path, file_id, nodes, edges)
53
+ return nodes, edges
54
+
55
+ # ── Top-level dispatch ──────────────────────────────────────────────
56
+
57
+ def _walk(
58
+ self,
59
+ source: bytes,
60
+ node: Any,
61
+ file_path: Path,
62
+ parent_id: str,
63
+ nodes: list[Node],
64
+ edges: list[Edge],
65
+ ) -> None:
66
+ kind = node.type
67
+
68
+ if kind == "package_declaration":
69
+ self._extract_package(source, node, file_path, parent_id, nodes, edges)
70
+ elif kind == "import_declaration":
71
+ self._extract_import(source, node, file_path, parent_id, nodes, edges)
72
+ elif kind == "class_declaration":
73
+ self._extract_class(source, node, file_path, parent_id, nodes, edges)
74
+ elif kind == "interface_declaration":
75
+ self._extract_interface(source, node, file_path, parent_id, nodes, edges)
76
+ elif kind == "method_declaration":
77
+ self._extract_method(source, node, file_path, parent_id, nodes, edges)
78
+ elif kind == "constructor_declaration":
79
+ self._extract_constructor(source, node, file_path, parent_id, nodes, edges)
80
+ elif kind == "field_declaration":
81
+ self._extract_field(source, node, file_path, parent_id, nodes, edges)
82
+ elif kind == "enum_declaration":
83
+ self._extract_enum(source, node, file_path, parent_id, nodes, edges)
84
+ else:
85
+ for child in node.children:
86
+ self._walk(source, child, file_path, parent_id, nodes, edges)
87
+
88
+ # ── Package ──────────────────────────────────────────────────────────
89
+
90
+ def _extract_package(
91
+ self,
92
+ source: bytes,
93
+ node: Any,
94
+ file_path: Path,
95
+ parent_id: str,
96
+ nodes: list[Node],
97
+ edges: list[Edge],
98
+ ) -> str:
99
+ start_line = node.start_point[0] + 1
100
+ end_line = node.end_point[0] + 1
101
+ node_id = self._node_id(file_path, start_line, end_line)
102
+
103
+ # Package name may be a scoped_identifier or identifier
104
+ name = self._get_node_text(source, node)
105
+ # Strip "package " prefix and ";" suffix for a clean name
106
+ pkg_name = name
107
+ for child in node.children:
108
+ if child.type in ("scoped_identifier", "identifier"):
109
+ pkg_name = source[child.start_byte : child.end_byte].decode()
110
+ break
111
+
112
+ pkg_node = Node(
113
+ id=node_id,
114
+ kind=NodeKind.MODULE,
115
+ name=pkg_name,
116
+ qualified_name=pkg_name,
117
+ file_path=file_path,
118
+ language=Language.JAVA,
119
+ start_line=start_line,
120
+ end_line=end_line,
121
+ parent_id=parent_id,
122
+ )
123
+ nodes.append(pkg_node)
124
+ edges.append(
125
+ Edge(
126
+ id=self._edge_id(parent_id, node_id, EdgeKind.CONTAINS),
127
+ source_id=parent_id,
128
+ target_id=node_id,
129
+ kind=EdgeKind.CONTAINS,
130
+ )
131
+ )
132
+ return node_id
133
+
134
+ # ── Import ───────────────────────────────────────────────────────────
135
+
136
+ def _extract_import(
137
+ self,
138
+ source: bytes,
139
+ node: Any,
140
+ file_path: Path,
141
+ parent_id: str,
142
+ nodes: list[Node],
143
+ edges: list[Edge],
144
+ ) -> None:
145
+ start_line = node.start_point[0] + 1
146
+ end_line = node.end_point[0] + 1
147
+ node_id = self._node_id(file_path, start_line, end_line)
148
+ text = source[node.start_byte : node.end_byte].decode().strip()
149
+ import_node = Node(
150
+ id=node_id,
151
+ kind=NodeKind.IMPORT,
152
+ name=text[:80],
153
+ qualified_name=f"import:{text[:80]}",
154
+ file_path=file_path,
155
+ language=Language.JAVA,
156
+ start_line=start_line,
157
+ end_line=end_line,
158
+ parent_id=parent_id,
159
+ )
160
+ nodes.append(import_node)
161
+ edges.append(
162
+ Edge(
163
+ id=self._edge_id(parent_id, node_id, EdgeKind.CONTAINS),
164
+ source_id=parent_id,
165
+ target_id=node_id,
166
+ kind=EdgeKind.CONTAINS,
167
+ )
168
+ )
169
+ edges.append(
170
+ Edge(
171
+ id=self._edge_id(parent_id, f"unresolved:{text}", EdgeKind.IMPORTS),
172
+ source_id=parent_id,
173
+ target_id=f"unresolved:{text}",
174
+ kind=EdgeKind.IMPORTS,
175
+ confidence=0.5,
176
+ )
177
+ )
178
+
179
+ # ── Class ────────────────────────────────────────────────────────────
180
+
181
+ def _extract_class(
182
+ self,
183
+ source: bytes,
184
+ node: Any,
185
+ file_path: Path,
186
+ parent_id: str,
187
+ nodes: list[Node],
188
+ edges: list[Edge],
189
+ ) -> str:
190
+ name_node = node.child_by_field_name("name")
191
+ if name_node is None:
192
+ return ""
193
+ name = source[name_node.start_byte : name_node.end_byte].decode()
194
+ start_line = node.start_point[0] + 1
195
+ end_line = node.end_point[0] + 1
196
+ node_id = self._node_id(file_path, start_line, end_line)
197
+
198
+ # Extract superclass (extends)
199
+ super_node = node.child_by_field_name("superclass")
200
+ bases: list[str] = []
201
+ if super_node:
202
+ bases.append(self._get_node_text(source, super_node).strip())
203
+
204
+ # Extract interfaces (implements)
205
+ interfaces_node = node.child_by_field_name("interfaces")
206
+ implements: list[str] = []
207
+ if interfaces_node:
208
+ for child in interfaces_node.children:
209
+ if child.type in ("type_identifier", "scoped_identifier", "generic_type"):
210
+ implements.append(source[child.start_byte : child.end_byte].decode())
211
+
212
+ qualified = self._build_qualified_name(file_path, name, parent_id, nodes)
213
+ class_node = Node(
214
+ id=node_id,
215
+ kind=NodeKind.CLASS,
216
+ name=name,
217
+ qualified_name=qualified,
218
+ file_path=file_path,
219
+ language=Language.JAVA,
220
+ start_line=start_line,
221
+ end_line=end_line,
222
+ parent_id=parent_id,
223
+ metadata={"bases": ",".join(bases)},
224
+ )
225
+ nodes.append(class_node)
226
+ edges.append(
227
+ Edge(
228
+ id=self._edge_id(parent_id, node_id, EdgeKind.CONTAINS),
229
+ source_id=parent_id,
230
+ target_id=node_id,
231
+ kind=EdgeKind.CONTAINS,
232
+ )
233
+ )
234
+
235
+ # Extends edges
236
+ for base_name in bases:
237
+ edges.append(
238
+ Edge(
239
+ id=self._edge_id(node_id, f"unresolved:{base_name}", EdgeKind.EXTENDS),
240
+ source_id=node_id,
241
+ target_id=f"unresolved:{base_name}",
242
+ kind=EdgeKind.EXTENDS,
243
+ confidence=0.5,
244
+ )
245
+ )
246
+
247
+ # Implements edges
248
+ for impl_name in implements:
249
+ edges.append(
250
+ Edge(
251
+ id=self._edge_id(node_id, f"unresolved:{impl_name}", EdgeKind.IMPLEMENTS),
252
+ source_id=node_id,
253
+ target_id=f"unresolved:{impl_name}",
254
+ kind=EdgeKind.IMPLEMENTS,
255
+ confidence=0.5,
256
+ )
257
+ )
258
+
259
+ # Walk class body
260
+ body = node.child_by_field_name("body")
261
+ if body:
262
+ for child in body.children:
263
+ self._walk(source, child, file_path, node_id, nodes, edges)
264
+ return node_id
265
+
266
+ # ── Interface ────────────────────────────────────────────────────────
267
+
268
+ def _extract_interface(
269
+ self,
270
+ source: bytes,
271
+ node: Any,
272
+ file_path: Path,
273
+ parent_id: str,
274
+ nodes: list[Node],
275
+ edges: list[Edge],
276
+ ) -> str:
277
+ name_node = node.child_by_field_name("name")
278
+ if name_node is None:
279
+ return ""
280
+ name = source[name_node.start_byte : name_node.end_byte].decode()
281
+ start_line = node.start_point[0] + 1
282
+ end_line = node.end_point[0] + 1
283
+ node_id = self._node_id(file_path, start_line, end_line)
284
+ qualified = self._build_qualified_name(file_path, name, parent_id, nodes)
285
+ iface_node = Node(
286
+ id=node_id,
287
+ kind=NodeKind.INTERFACE,
288
+ name=name,
289
+ qualified_name=qualified,
290
+ file_path=file_path,
291
+ language=Language.JAVA,
292
+ start_line=start_line,
293
+ end_line=end_line,
294
+ parent_id=parent_id,
295
+ )
296
+ nodes.append(iface_node)
297
+ edges.append(
298
+ Edge(
299
+ id=self._edge_id(parent_id, node_id, EdgeKind.CONTAINS),
300
+ source_id=parent_id,
301
+ target_id=node_id,
302
+ kind=EdgeKind.CONTAINS,
303
+ )
304
+ )
305
+ # Walk interface body for method signatures etc.
306
+ body = node.child_by_field_name("body")
307
+ if body:
308
+ for child in body.children:
309
+ self._walk(source, child, file_path, node_id, nodes, edges)
310
+ return node_id
311
+
312
+ # ── Method ───────────────────────────────────────────────────────────
313
+
314
+ def _extract_method(
315
+ self,
316
+ source: bytes,
317
+ node: Any,
318
+ file_path: Path,
319
+ parent_id: str,
320
+ nodes: list[Node],
321
+ edges: list[Edge],
322
+ ) -> str:
323
+ name_node = node.child_by_field_name("name")
324
+ if name_node is None:
325
+ return ""
326
+ name = source[name_node.start_byte : name_node.end_byte].decode()
327
+ start_line = node.start_point[0] + 1
328
+ end_line = node.end_point[0] + 1
329
+ node_id = self._node_id(file_path, start_line, end_line)
330
+
331
+ # Build signature from parameters and return type
332
+ params_node = node.child_by_field_name("parameters")
333
+ return_node = node.child_by_field_name("type")
334
+ signature = self._build_java_signature(source, name, params_node, return_node)
335
+
336
+ qualified = self._build_qualified_name(file_path, name, parent_id, nodes)
337
+ method_node = Node(
338
+ id=node_id,
339
+ kind=NodeKind.METHOD,
340
+ name=name,
341
+ qualified_name=qualified,
342
+ file_path=file_path,
343
+ language=Language.JAVA,
344
+ start_line=start_line,
345
+ end_line=end_line,
346
+ parent_id=parent_id,
347
+ signature=signature,
348
+ )
349
+ nodes.append(method_node)
350
+ edges.append(
351
+ Edge(
352
+ id=self._edge_id(parent_id, node_id, EdgeKind.CONTAINS),
353
+ source_id=parent_id,
354
+ target_id=node_id,
355
+ kind=EdgeKind.CONTAINS,
356
+ )
357
+ )
358
+ self._extract_calls(source, node, node_id, file_path, edges)
359
+ return node_id
360
+
361
+ # ── Constructor ──────────────────────────────────────────────────────
362
+
363
+ def _extract_constructor(
364
+ self,
365
+ source: bytes,
366
+ node: Any,
367
+ file_path: Path,
368
+ parent_id: str,
369
+ nodes: list[Node],
370
+ edges: list[Edge],
371
+ ) -> str:
372
+ name_node = node.child_by_field_name("name")
373
+ if name_node is None:
374
+ return ""
375
+ name = source[name_node.start_byte : name_node.end_byte].decode()
376
+ start_line = node.start_point[0] + 1
377
+ end_line = node.end_point[0] + 1
378
+ node_id = self._node_id(file_path, start_line, end_line)
379
+
380
+ params_node = node.child_by_field_name("parameters")
381
+ if params_node:
382
+ source[params_node.start_byte : params_node.end_byte].decode().strip()
383
+
384
+ qualified = self._build_qualified_name(file_path, name, parent_id, nodes)
385
+ ctor_node = Node(
386
+ id=node_id,
387
+ kind=NodeKind.METHOD,
388
+ name=name,
389
+ qualified_name=qualified,
390
+ file_path=file_path,
391
+ language=Language.JAVA,
392
+ start_line=start_line,
393
+ end_line=end_line,
394
+ parent_id=parent_id,
395
+ )
396
+ nodes.append(ctor_node)
397
+ edges.append(
398
+ Edge(
399
+ id=self._edge_id(parent_id, node_id, EdgeKind.CONTAINS),
400
+ source_id=parent_id,
401
+ target_id=node_id,
402
+ kind=EdgeKind.CONTAINS,
403
+ )
404
+ )
405
+ self._extract_calls(source, node, node_id, file_path, edges)
406
+ return node_id
407
+
408
+ # ── Field / Constant ─────────────────────────────────────────────────
409
+
410
+ def _extract_field(
411
+ self,
412
+ source: bytes,
413
+ node: Any,
414
+ file_path: Path,
415
+ parent_id: str,
416
+ nodes: list[Node],
417
+ edges: list[Edge],
418
+ ) -> None:
419
+ start_line = node.start_point[0] + 1
420
+ end_line = node.end_point[0] + 1
421
+ node_id = self._node_id(file_path, start_line, end_line)
422
+
423
+ # Check modifiers for static final → CONSTANT
424
+ modifiers_node = node.child_by_field_name("modifiers")
425
+ is_constant = False
426
+ if modifiers_node:
427
+ mod_text = source[modifiers_node.start_byte : modifiers_node.end_byte].decode()
428
+ is_constant = "static" in mod_text and "final" in mod_text
429
+ kind = NodeKind.CONSTANT if is_constant else NodeKind.VARIABLE
430
+
431
+ # Extract the declarator name
432
+ name_node = node.child_by_field_name("declarator")
433
+ if name_node is None:
434
+ # fallback: search for identifier child
435
+ for child in node.children:
436
+ if child.type == "identifier":
437
+ name_node = child
438
+ break
439
+ if name_node is None:
440
+ return
441
+ name = source[name_node.start_byte : name_node.end_byte].decode()
442
+
443
+ qualified = self._build_qualified_name(file_path, name, parent_id, nodes)
444
+ field_node = Node(
445
+ id=node_id,
446
+ kind=kind,
447
+ name=name,
448
+ qualified_name=qualified,
449
+ file_path=file_path,
450
+ language=Language.JAVA,
451
+ start_line=start_line,
452
+ end_line=end_line,
453
+ parent_id=parent_id,
454
+ )
455
+ nodes.append(field_node)
456
+ edges.append(
457
+ Edge(
458
+ id=self._edge_id(parent_id, node_id, EdgeKind.CONTAINS),
459
+ source_id=parent_id,
460
+ target_id=node_id,
461
+ kind=EdgeKind.CONTAINS,
462
+ )
463
+ )
464
+
465
+ # ── Enum ─────────────────────────────────────────────────────────────
466
+
467
+ def _extract_enum(
468
+ self,
469
+ source: bytes,
470
+ node: Any,
471
+ file_path: Path,
472
+ parent_id: str,
473
+ nodes: list[Node],
474
+ edges: list[Edge],
475
+ ) -> str:
476
+ name_node = node.child_by_field_name("name")
477
+ if name_node is None:
478
+ return ""
479
+ name = source[name_node.start_byte : name_node.end_byte].decode()
480
+ start_line = node.start_point[0] + 1
481
+ end_line = node.end_point[0] + 1
482
+ node_id = self._node_id(file_path, start_line, end_line)
483
+ qualified = self._build_qualified_name(file_path, name, parent_id, nodes)
484
+ enum_node = Node(
485
+ id=node_id,
486
+ kind=NodeKind.ENUM,
487
+ name=name,
488
+ qualified_name=qualified,
489
+ file_path=file_path,
490
+ language=Language.JAVA,
491
+ start_line=start_line,
492
+ end_line=end_line,
493
+ parent_id=parent_id,
494
+ )
495
+ nodes.append(enum_node)
496
+ edges.append(
497
+ Edge(
498
+ id=self._edge_id(parent_id, node_id, EdgeKind.CONTAINS),
499
+ source_id=parent_id,
500
+ target_id=node_id,
501
+ kind=EdgeKind.CONTAINS,
502
+ )
503
+ )
504
+ # Walk enum body
505
+ body = node.child_by_field_name("body")
506
+ if body:
507
+ for child in body.children:
508
+ self._walk(source, child, file_path, node_id, nodes, edges)
509
+ return node_id
510
+
511
+ # ── Call extraction ──────────────────────────────────────────────────
512
+
513
+ def _extract_calls(
514
+ self,
515
+ source: bytes,
516
+ func_node: Any,
517
+ func_id: str,
518
+ file_path: Path,
519
+ edges: list[Edge],
520
+ ) -> None:
521
+ body = func_node.child_by_field_name("body")
522
+ if body is None:
523
+ return
524
+ self._find_calls(source, body, func_id, file_path, edges)
525
+
526
+ def _find_calls(
527
+ self,
528
+ source: bytes,
529
+ node: Any,
530
+ caller_id: str,
531
+ file_path: Path,
532
+ edges: list[Edge],
533
+ ) -> None:
534
+ if node.type == "method_invocation":
535
+ name_node = node.child_by_field_name("name")
536
+ if name_node:
537
+ call_name = source[name_node.start_byte : name_node.end_byte].decode()
538
+ # Also check for object (e.g., obj.method())
539
+ obj_node = node.child_by_field_name("object")
540
+ if obj_node:
541
+ obj_name = source[obj_node.start_byte : obj_node.end_byte].decode()
542
+ call_name = f"{obj_name}.{call_name}"
543
+ edges.append(
544
+ Edge(
545
+ id=self._edge_id(caller_id, f"unresolved:{call_name}", EdgeKind.CALLS),
546
+ source_id=caller_id,
547
+ target_id=f"unresolved:{call_name}",
548
+ kind=EdgeKind.CALLS,
549
+ confidence=0.5,
550
+ line=node.start_point[0] + 1,
551
+ )
552
+ )
553
+ for child in node.children:
554
+ self._find_calls(source, child, caller_id, file_path, edges)
555
+
556
+ # ── Helpers ──────────────────────────────────────────────────────────
557
+
558
+ def _build_java_signature(
559
+ self,
560
+ source: bytes,
561
+ name: str,
562
+ params_node: Any | None,
563
+ return_node: Any | None,
564
+ ) -> str:
565
+ params_str = ""
566
+ if params_node:
567
+ params_str = source[params_node.start_byte : params_node.end_byte].decode().strip()
568
+ return_str = ""
569
+ if return_node:
570
+ return_str = source[return_node.start_byte : return_node.end_byte].decode().strip()
571
+ if return_str:
572
+ return f"{name}{params_str} -> {return_str}"
573
+ return f"{name}{params_str}"
574
+
575
+ def _build_qualified_name(
576
+ self,
577
+ file_path: Path,
578
+ name: str,
579
+ parent_id: str,
580
+ nodes: list[Node],
581
+ ) -> str:
582
+ for n in nodes:
583
+ if n.id == parent_id:
584
+ if n.kind in (NodeKind.CLASS, NodeKind.INTERFACE, NodeKind.ENUM, NodeKind.MODULE):
585
+ return f"{n.qualified_name}.{name}"
586
+ elif n.kind == NodeKind.FILE:
587
+ return f"{file_path.stem}.{name}"
588
+ break
589
+ return name
590
+
591
+ @staticmethod
592
+ def _get_node_text(source: bytes, node: Any) -> str:
593
+ return source[node.start_byte : node.end_byte].decode()
594
+
595
+ @staticmethod
596
+ def _node_id(file: Path, start: int, end: int) -> str:
597
+ raw = f"{file}:{start}:{end}"
598
+ return hashlib.sha256(raw.encode()).hexdigest()[:16]
599
+
600
+ @staticmethod
601
+ def _edge_id(source: str, target: str, kind: EdgeKind) -> str:
602
+ raw = f"{source}:{target}:{kind.value}"
603
+ return hashlib.sha256(raw.encode()).hexdigest()[:16]