shotgun-sh 0.3.3.dev1__py3-none-any.whl → 0.6.2__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 (159) hide show
  1. shotgun/agents/agent_manager.py +497 -30
  2. shotgun/agents/cancellation.py +103 -0
  3. shotgun/agents/common.py +90 -77
  4. shotgun/agents/config/README.md +0 -1
  5. shotgun/agents/config/manager.py +52 -8
  6. shotgun/agents/config/models.py +21 -27
  7. shotgun/agents/config/provider.py +44 -27
  8. shotgun/agents/conversation/history/file_content_deduplication.py +66 -43
  9. shotgun/agents/conversation/history/token_counting/base.py +51 -9
  10. shotgun/agents/export.py +12 -13
  11. shotgun/agents/file_read.py +176 -0
  12. shotgun/agents/messages.py +15 -3
  13. shotgun/agents/models.py +90 -2
  14. shotgun/agents/plan.py +12 -13
  15. shotgun/agents/research.py +13 -10
  16. shotgun/agents/router/__init__.py +47 -0
  17. shotgun/agents/router/models.py +384 -0
  18. shotgun/agents/router/router.py +185 -0
  19. shotgun/agents/router/tools/__init__.py +18 -0
  20. shotgun/agents/router/tools/delegation_tools.py +557 -0
  21. shotgun/agents/router/tools/plan_tools.py +403 -0
  22. shotgun/agents/runner.py +17 -2
  23. shotgun/agents/specify.py +12 -13
  24. shotgun/agents/tasks.py +12 -13
  25. shotgun/agents/tools/__init__.py +8 -0
  26. shotgun/agents/tools/codebase/directory_lister.py +27 -39
  27. shotgun/agents/tools/codebase/file_read.py +26 -35
  28. shotgun/agents/tools/codebase/query_graph.py +9 -0
  29. shotgun/agents/tools/codebase/retrieve_code.py +9 -0
  30. shotgun/agents/tools/file_management.py +81 -3
  31. shotgun/agents/tools/file_read_tools/__init__.py +7 -0
  32. shotgun/agents/tools/file_read_tools/multimodal_file_read.py +167 -0
  33. shotgun/agents/tools/markdown_tools/__init__.py +62 -0
  34. shotgun/agents/tools/markdown_tools/insert_section.py +148 -0
  35. shotgun/agents/tools/markdown_tools/models.py +86 -0
  36. shotgun/agents/tools/markdown_tools/remove_section.py +114 -0
  37. shotgun/agents/tools/markdown_tools/replace_section.py +119 -0
  38. shotgun/agents/tools/markdown_tools/utils.py +453 -0
  39. shotgun/agents/tools/registry.py +46 -6
  40. shotgun/agents/tools/web_search/__init__.py +1 -2
  41. shotgun/agents/tools/web_search/gemini.py +1 -3
  42. shotgun/agents/tools/web_search/openai.py +42 -23
  43. shotgun/attachments/__init__.py +41 -0
  44. shotgun/attachments/errors.py +60 -0
  45. shotgun/attachments/models.py +107 -0
  46. shotgun/attachments/parser.py +257 -0
  47. shotgun/attachments/processor.py +193 -0
  48. shotgun/build_constants.py +4 -7
  49. shotgun/cli/clear.py +2 -2
  50. shotgun/cli/codebase/commands.py +181 -65
  51. shotgun/cli/compact.py +2 -2
  52. shotgun/cli/context.py +2 -2
  53. shotgun/cli/error_handler.py +2 -2
  54. shotgun/cli/run.py +90 -0
  55. shotgun/cli/spec/backup.py +2 -1
  56. shotgun/codebase/__init__.py +2 -0
  57. shotgun/codebase/benchmarks/__init__.py +35 -0
  58. shotgun/codebase/benchmarks/benchmark_runner.py +309 -0
  59. shotgun/codebase/benchmarks/exporters.py +119 -0
  60. shotgun/codebase/benchmarks/formatters/__init__.py +49 -0
  61. shotgun/codebase/benchmarks/formatters/base.py +34 -0
  62. shotgun/codebase/benchmarks/formatters/json_formatter.py +106 -0
  63. shotgun/codebase/benchmarks/formatters/markdown.py +136 -0
  64. shotgun/codebase/benchmarks/models.py +129 -0
  65. shotgun/codebase/core/__init__.py +4 -0
  66. shotgun/codebase/core/call_resolution.py +91 -0
  67. shotgun/codebase/core/change_detector.py +11 -6
  68. shotgun/codebase/core/errors.py +159 -0
  69. shotgun/codebase/core/extractors/__init__.py +23 -0
  70. shotgun/codebase/core/extractors/base.py +138 -0
  71. shotgun/codebase/core/extractors/factory.py +63 -0
  72. shotgun/codebase/core/extractors/go/__init__.py +7 -0
  73. shotgun/codebase/core/extractors/go/extractor.py +122 -0
  74. shotgun/codebase/core/extractors/javascript/__init__.py +7 -0
  75. shotgun/codebase/core/extractors/javascript/extractor.py +132 -0
  76. shotgun/codebase/core/extractors/protocol.py +109 -0
  77. shotgun/codebase/core/extractors/python/__init__.py +7 -0
  78. shotgun/codebase/core/extractors/python/extractor.py +141 -0
  79. shotgun/codebase/core/extractors/rust/__init__.py +7 -0
  80. shotgun/codebase/core/extractors/rust/extractor.py +139 -0
  81. shotgun/codebase/core/extractors/types.py +15 -0
  82. shotgun/codebase/core/extractors/typescript/__init__.py +7 -0
  83. shotgun/codebase/core/extractors/typescript/extractor.py +92 -0
  84. shotgun/codebase/core/gitignore.py +252 -0
  85. shotgun/codebase/core/ingestor.py +644 -354
  86. shotgun/codebase/core/kuzu_compat.py +119 -0
  87. shotgun/codebase/core/language_config.py +239 -0
  88. shotgun/codebase/core/manager.py +256 -46
  89. shotgun/codebase/core/metrics_collector.py +310 -0
  90. shotgun/codebase/core/metrics_types.py +347 -0
  91. shotgun/codebase/core/parallel_executor.py +424 -0
  92. shotgun/codebase/core/work_distributor.py +254 -0
  93. shotgun/codebase/core/worker.py +768 -0
  94. shotgun/codebase/indexing_state.py +86 -0
  95. shotgun/codebase/models.py +94 -0
  96. shotgun/codebase/service.py +13 -0
  97. shotgun/exceptions.py +9 -9
  98. shotgun/main.py +3 -16
  99. shotgun/posthog_telemetry.py +165 -24
  100. shotgun/prompts/agents/export.j2 +2 -0
  101. shotgun/prompts/agents/file_read.j2 +48 -0
  102. shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +19 -52
  103. shotgun/prompts/agents/partials/content_formatting.j2 +12 -33
  104. shotgun/prompts/agents/partials/interactive_mode.j2 +9 -32
  105. shotgun/prompts/agents/partials/router_delegation_mode.j2 +35 -0
  106. shotgun/prompts/agents/plan.j2 +38 -12
  107. shotgun/prompts/agents/research.j2 +70 -31
  108. shotgun/prompts/agents/router.j2 +713 -0
  109. shotgun/prompts/agents/specify.j2 +53 -16
  110. shotgun/prompts/agents/state/codebase/codebase_graphs_available.j2 +14 -1
  111. shotgun/prompts/agents/state/system_state.j2 +24 -13
  112. shotgun/prompts/agents/tasks.j2 +72 -34
  113. shotgun/settings.py +49 -10
  114. shotgun/tui/app.py +154 -24
  115. shotgun/tui/commands/__init__.py +9 -1
  116. shotgun/tui/components/attachment_bar.py +87 -0
  117. shotgun/tui/components/mode_indicator.py +120 -25
  118. shotgun/tui/components/prompt_input.py +25 -28
  119. shotgun/tui/components/status_bar.py +14 -7
  120. shotgun/tui/dependencies.py +58 -8
  121. shotgun/tui/protocols.py +55 -0
  122. shotgun/tui/screens/chat/chat.tcss +24 -1
  123. shotgun/tui/screens/chat/chat_screen.py +1376 -213
  124. shotgun/tui/screens/chat/codebase_index_prompt_screen.py +8 -4
  125. shotgun/tui/screens/chat_screen/attachment_hint.py +40 -0
  126. shotgun/tui/screens/chat_screen/command_providers.py +0 -97
  127. shotgun/tui/screens/chat_screen/history/agent_response.py +7 -3
  128. shotgun/tui/screens/chat_screen/history/chat_history.py +58 -6
  129. shotgun/tui/screens/chat_screen/history/formatters.py +75 -15
  130. shotgun/tui/screens/chat_screen/history/partial_response.py +11 -1
  131. shotgun/tui/screens/chat_screen/history/user_question.py +25 -3
  132. shotgun/tui/screens/chat_screen/messages.py +219 -0
  133. shotgun/tui/screens/database_locked_dialog.py +219 -0
  134. shotgun/tui/screens/database_timeout_dialog.py +158 -0
  135. shotgun/tui/screens/kuzu_error_dialog.py +135 -0
  136. shotgun/tui/screens/model_picker.py +1 -3
  137. shotgun/tui/screens/models.py +11 -0
  138. shotgun/tui/state/processing_state.py +19 -0
  139. shotgun/tui/utils/mode_progress.py +20 -86
  140. shotgun/tui/widgets/__init__.py +2 -1
  141. shotgun/tui/widgets/approval_widget.py +152 -0
  142. shotgun/tui/widgets/cascade_confirmation_widget.py +203 -0
  143. shotgun/tui/widgets/plan_panel.py +129 -0
  144. shotgun/tui/widgets/step_checkpoint_widget.py +180 -0
  145. shotgun/tui/widgets/widget_coordinator.py +18 -0
  146. shotgun/utils/file_system_utils.py +4 -1
  147. {shotgun_sh-0.3.3.dev1.dist-info → shotgun_sh-0.6.2.dist-info}/METADATA +88 -35
  148. shotgun_sh-0.6.2.dist-info/RECORD +291 -0
  149. shotgun/cli/export.py +0 -81
  150. shotgun/cli/plan.py +0 -73
  151. shotgun/cli/research.py +0 -93
  152. shotgun/cli/specify.py +0 -70
  153. shotgun/cli/tasks.py +0 -78
  154. shotgun/sentry_telemetry.py +0 -232
  155. shotgun/tui/screens/onboarding.py +0 -580
  156. shotgun_sh-0.3.3.dev1.dist-info/RECORD +0 -229
  157. {shotgun_sh-0.3.3.dev1.dist-info → shotgun_sh-0.6.2.dist-info}/WHEEL +0 -0
  158. {shotgun_sh-0.3.3.dev1.dist-info → shotgun_sh-0.6.2.dist-info}/entry_points.txt +0 -0
  159. {shotgun_sh-0.3.3.dev1.dist-info → shotgun_sh-0.6.2.dist-info}/licenses/LICENSE +0 -0
@@ -5,22 +5,34 @@ from __future__ import annotations
5
5
  import asyncio
6
6
  import hashlib
7
7
  import json
8
+ import shutil
8
9
  import time
9
10
  import uuid
10
11
  from collections.abc import Awaitable, Callable
11
12
  from pathlib import Path
12
- from typing import Any, ClassVar
13
+ from typing import TYPE_CHECKING, Any, ClassVar
13
14
 
14
15
  import anyio
15
- import kuzu
16
16
  from watchdog.events import FileSystemEvent, FileSystemEventHandler
17
17
  from watchdog.observers import Observer
18
18
 
19
+ from shotgun.codebase.core.kuzu_compat import get_kuzu
20
+
21
+ if TYPE_CHECKING:
22
+ import real_ladybug as kuzu
23
+
24
+ from shotgun.codebase.core.errors import (
25
+ DatabaseIssue,
26
+ KuzuErrorType,
27
+ classify_kuzu_error,
28
+ )
19
29
  from shotgun.codebase.models import (
20
30
  CodebaseGraph,
21
31
  FileChange,
22
32
  GraphStatus,
33
+ NodeLabel,
23
34
  OperationStats,
35
+ RelationshipType,
24
36
  )
25
37
  from shotgun.logging_config import get_logger
26
38
 
@@ -190,7 +202,7 @@ class CodebaseGraphManager:
190
202
  return cls._lock
191
203
 
192
204
  @classmethod
193
- def _generate_graph_id(cls, repo_path: str) -> str:
205
+ def generate_graph_id(cls, repo_path: str) -> str:
194
206
  """Generate deterministic graph ID from repository path."""
195
207
  normalized = str(Path(repo_path).resolve())
196
208
  return hashlib.sha256(normalized.encode()).hexdigest()[:12]
@@ -265,7 +277,8 @@ class CodebaseGraphManager:
265
277
  """
266
278
  graph_path = self.storage_dir / f"{graph_id}.kuzu"
267
279
 
268
- # Create database and connection
280
+ # Create database and connection (lazy import for Windows compatibility)
281
+ kuzu = get_kuzu()
269
282
  lock = await self._get_lock()
270
283
  async with lock:
271
284
  db = kuzu.Database(str(graph_path))
@@ -348,7 +361,7 @@ class CodebaseGraphManager:
348
361
  Created graph metadata
349
362
  """
350
363
  repo_path = str(Path(repo_path).resolve())
351
- graph_id = self._generate_graph_id(repo_path)
364
+ graph_id = self.generate_graph_id(repo_path)
352
365
 
353
366
  # Use repository name as default name
354
367
  if not name:
@@ -412,7 +425,8 @@ class CodebaseGraphManager:
412
425
  # Run build in thread pool
413
426
  await anyio.to_thread.run_sync(ingestor.build_graph_from_directory, repo_path)
414
427
 
415
- # Get statistics
428
+ # Get statistics (lazy import for Windows compatibility)
429
+ kuzu = get_kuzu()
416
430
  lock = await self._get_lock()
417
431
  async with lock:
418
432
  db = kuzu.Database(str(graph_path))
@@ -554,6 +568,8 @@ class CodebaseGraphManager:
554
568
  "relationships_removed": 0,
555
569
  }
556
570
 
571
+ # Lazy import for Windows compatibility
572
+ kuzu = get_kuzu()
557
573
  lock = await self._get_lock()
558
574
  async with lock:
559
575
  if graph_id not in self._connections:
@@ -622,6 +638,8 @@ class CodebaseGraphManager:
622
638
  languages = build_options.get("languages")
623
639
  exclude_patterns = build_options.get("exclude_patterns")
624
640
 
641
+ # Lazy import for Windows compatibility
642
+ kuzu = get_kuzu()
625
643
  lock = await self._get_lock()
626
644
  async with lock:
627
645
  if graph_id not in self._connections:
@@ -1044,6 +1062,8 @@ class CodebaseGraphManager:
1044
1062
  self, graph_id: str, query: str, parameters: dict[str, Any] | None = None
1045
1063
  ) -> list[dict[str, Any]]:
1046
1064
  """Internal query execution with connection management."""
1065
+ # Lazy import for Windows compatibility
1066
+ kuzu = get_kuzu()
1047
1067
  lock = await self._get_lock()
1048
1068
  async with lock:
1049
1069
  if graph_id not in self._connections:
@@ -1216,8 +1236,19 @@ class CodebaseGraphManager:
1216
1236
  indexed_from_cwds=indexed_from_cwds,
1217
1237
  )
1218
1238
  except Exception as e:
1239
+ # Classify the error to determine if we should re-raise
1240
+ error_type = classify_kuzu_error(e)
1241
+
1242
+ if error_type == KuzuErrorType.LOCKED:
1243
+ # Don't mask lock errors - let caller handle them
1244
+ logger.warning(
1245
+ f"Database locked - graph_id: {graph_id}, error: {str(e)}"
1246
+ )
1247
+ raise
1248
+
1219
1249
  logger.error(
1220
- f"Failed to get graph metadata - graph_id: {graph_id}, error: {str(e)}"
1250
+ f"Failed to get graph metadata - graph_id: {graph_id}, "
1251
+ f"error_type: {error_type.value}, error: {str(e)}"
1221
1252
  )
1222
1253
  return None
1223
1254
 
@@ -1264,6 +1295,7 @@ class CodebaseGraphManager:
1264
1295
 
1265
1296
  # Try to open the database
1266
1297
  def _open_and_query(g: str = gid, p: Path = db_path) -> bool:
1298
+ kuzu = get_kuzu()
1267
1299
  db = kuzu.Database(str(p))
1268
1300
  conn = kuzu.Connection(db)
1269
1301
  try:
@@ -1336,6 +1368,174 @@ class CodebaseGraphManager:
1336
1368
 
1337
1369
  return removed_graphs
1338
1370
 
1371
+ async def _try_open_database(self, graph_id: str, db_path: Path) -> bool:
1372
+ """Try to open a database and verify it has a Project node.
1373
+
1374
+ Args:
1375
+ graph_id: The graph identifier
1376
+ db_path: Path to the database file
1377
+
1378
+ Returns:
1379
+ True if database opened and has Project node, False otherwise
1380
+ """
1381
+ lock = await self._get_lock()
1382
+ async with lock:
1383
+ # Close existing connections if any
1384
+ if graph_id in self._connections:
1385
+ try:
1386
+ self._connections[graph_id].close()
1387
+ except Exception as e:
1388
+ logger.debug(f"Failed to close connection for {graph_id}: {e}")
1389
+ del self._connections[graph_id]
1390
+ if graph_id in self._databases:
1391
+ try:
1392
+ self._databases[graph_id].close()
1393
+ except Exception as e:
1394
+ logger.debug(f"Failed to close database for {graph_id}: {e}")
1395
+ del self._databases[graph_id]
1396
+
1397
+ def _open_and_query() -> bool:
1398
+ kuzu = get_kuzu()
1399
+ db = kuzu.Database(str(db_path))
1400
+ conn = kuzu.Connection(db)
1401
+ try:
1402
+ result = conn.execute(
1403
+ "MATCH (p:Project {graph_id: $graph_id}) RETURN p",
1404
+ {"graph_id": graph_id},
1405
+ )
1406
+ return result.has_next() if hasattr(result, "has_next") else False
1407
+ finally:
1408
+ conn.close()
1409
+ db.close()
1410
+
1411
+ return await anyio.to_thread.run_sync(_open_and_query)
1412
+
1413
+ async def _check_single_database(
1414
+ self, graph_id: str, path: Path, timeout_seconds: float
1415
+ ) -> DatabaseIssue | None:
1416
+ """Check a single database for issues.
1417
+
1418
+ Args:
1419
+ graph_id: The graph identifier
1420
+ path: Path to the database file
1421
+ timeout_seconds: How long to wait for the database to respond
1422
+
1423
+ Returns:
1424
+ DatabaseIssue if problem found, None if database is healthy
1425
+ """
1426
+ try:
1427
+ has_project = await asyncio.wait_for(
1428
+ self._try_open_database(graph_id, path), timeout=timeout_seconds
1429
+ )
1430
+ if not has_project:
1431
+ return DatabaseIssue(
1432
+ graph_id=graph_id,
1433
+ graph_path=path,
1434
+ error_type=KuzuErrorType.SCHEMA,
1435
+ message="Database has no Project node (incomplete build)",
1436
+ )
1437
+ return None
1438
+
1439
+ except asyncio.TimeoutError:
1440
+ return DatabaseIssue(
1441
+ graph_id=graph_id,
1442
+ graph_path=path,
1443
+ error_type=KuzuErrorType.TIMEOUT,
1444
+ message=f"Database operation timed out after {timeout_seconds}s",
1445
+ )
1446
+
1447
+ except Exception as e:
1448
+ error_type = classify_kuzu_error(e)
1449
+ logger.debug(f"Detected {error_type.value} issue with {graph_id}: {e}")
1450
+ return DatabaseIssue(
1451
+ graph_id=graph_id,
1452
+ graph_path=path,
1453
+ error_type=error_type,
1454
+ message=str(e),
1455
+ )
1456
+
1457
+ async def detect_database_issues(
1458
+ self, timeout_seconds: float = 10.0
1459
+ ) -> list[DatabaseIssue]:
1460
+ """Detect issues with Kuzu databases without deleting them.
1461
+
1462
+ This method iterates through all .kuzu files in the storage directory,
1463
+ attempts to open them, and returns information about any issues found.
1464
+ Unlike cleanup_corrupted_databases(), this method does NOT delete anything -
1465
+ it only detects and reports issues for the caller to handle.
1466
+
1467
+ Args:
1468
+ timeout_seconds: How long to wait for each database to respond.
1469
+ Default is 10s; use 90s for retry with large codebases.
1470
+
1471
+ Returns:
1472
+ List of DatabaseIssue objects describing any problems found
1473
+ """
1474
+ issues: list[DatabaseIssue] = []
1475
+
1476
+ for path in self.storage_dir.glob("*.kuzu"):
1477
+ graph_id = path.stem
1478
+ issue = await self._check_single_database(graph_id, path, timeout_seconds)
1479
+ if issue:
1480
+ issues.append(issue)
1481
+
1482
+ return issues
1483
+
1484
+ async def delete_database(self, graph_id: str) -> bool:
1485
+ """Delete a database file and its WAL file.
1486
+
1487
+ Args:
1488
+ graph_id: The ID of the graph to delete
1489
+
1490
+ Returns:
1491
+ True if deletion was successful, False otherwise
1492
+ """
1493
+ graph_path = self.storage_dir / f"{graph_id}.kuzu"
1494
+
1495
+ try:
1496
+ # Clean up any open connections
1497
+ lock = await self._get_lock()
1498
+ async with lock:
1499
+ if graph_id in self._connections:
1500
+ try:
1501
+ self._connections[graph_id].close()
1502
+ except Exception as e:
1503
+ logger.debug(
1504
+ f"Failed to close connection during delete for {graph_id}: {e}"
1505
+ )
1506
+ del self._connections[graph_id]
1507
+ if graph_id in self._databases:
1508
+ try:
1509
+ self._databases[graph_id].close()
1510
+ except Exception as e:
1511
+ logger.debug(
1512
+ f"Failed to close database during delete for {graph_id}: {e}"
1513
+ )
1514
+ del self._databases[graph_id]
1515
+
1516
+ # Remove the database (could be file or directory)
1517
+ if graph_path.exists():
1518
+ if graph_path.is_dir():
1519
+ await anyio.to_thread.run_sync(shutil.rmtree, graph_path)
1520
+ else:
1521
+ await anyio.to_thread.run_sync(graph_path.unlink)
1522
+
1523
+ # Also delete WAL file if it exists
1524
+ wal_path = graph_path.with_suffix(graph_path.suffix + ".wal")
1525
+ if wal_path.exists():
1526
+ await anyio.to_thread.run_sync(wal_path.unlink)
1527
+ logger.debug(f"Deleted WAL file: {wal_path}")
1528
+
1529
+ logger.info(f"Deleted database: {graph_id}")
1530
+ return True
1531
+ else:
1532
+ logger.warning(f"Database file not found for deletion: {graph_id}")
1533
+ return False
1534
+
1535
+ except Exception as e:
1536
+ logger.error(f"Failed to delete database {graph_id}: {e}")
1537
+ return False
1538
+
1339
1539
  async def list_graphs(self) -> list[CodebaseGraph]:
1340
1540
  """List all available graphs.
1341
1541
 
@@ -1477,20 +1677,20 @@ class CodebaseGraphManager:
1477
1677
  Returns:
1478
1678
  Tuple of (node_stats, relationship_stats)
1479
1679
  """
1480
- node_stats = {}
1680
+ node_stats: dict[str, int] = {}
1481
1681
 
1482
- # Count each node type
1682
+ # Count each node type (excluding ExternalPackage which is rarely needed)
1483
1683
  node_types = [
1484
- "Project",
1485
- "Package",
1486
- "Module",
1487
- "Class",
1488
- "Function",
1489
- "Method",
1490
- "File",
1491
- "Folder",
1492
- "FileMetadata",
1493
- "DeletionLog",
1684
+ NodeLabel.PROJECT,
1685
+ NodeLabel.PACKAGE,
1686
+ NodeLabel.MODULE,
1687
+ NodeLabel.CLASS,
1688
+ NodeLabel.FUNCTION,
1689
+ NodeLabel.METHOD,
1690
+ NodeLabel.FILE,
1691
+ NodeLabel.FOLDER,
1692
+ NodeLabel.FILE_METADATA,
1693
+ NodeLabel.DELETION_LOG,
1494
1694
  ]
1495
1695
 
1496
1696
  for node_type in node_types:
@@ -1505,14 +1705,14 @@ class CodebaseGraphManager:
1505
1705
  logger.debug(f"Failed to count {node_type} nodes: {e}")
1506
1706
 
1507
1707
  # Count relationships - need to handle multiple tables for each type
1508
- rel_counts = {}
1708
+ rel_counts: dict[str, int] = {}
1509
1709
 
1510
1710
  # CONTAINS relationships
1511
1711
  for prefix in [
1512
- "CONTAINS_PACKAGE",
1513
- "CONTAINS_FOLDER",
1514
- "CONTAINS_FILE",
1515
- "CONTAINS_MODULE",
1712
+ RelationshipType.CONTAINS_PACKAGE,
1713
+ RelationshipType.CONTAINS_FOLDER,
1714
+ RelationshipType.CONTAINS_FILE,
1715
+ RelationshipType.CONTAINS_MODULE,
1516
1716
  ]:
1517
1717
  count = 0
1518
1718
  for suffix in ["", "_PKG", "_FOLDER"]:
@@ -1530,13 +1730,13 @@ class CodebaseGraphManager:
1530
1730
 
1531
1731
  # Other relationships
1532
1732
  for rel_type in [
1533
- "DEFINES",
1534
- "DEFINES_FUNC",
1535
- "DEFINES_METHOD",
1536
- "INHERITS",
1537
- "OVERRIDES",
1538
- "DEPENDS_ON_EXTERNAL",
1539
- "IMPORTS",
1733
+ RelationshipType.DEFINES,
1734
+ RelationshipType.DEFINES_FUNC,
1735
+ RelationshipType.DEFINES_METHOD,
1736
+ RelationshipType.INHERITS,
1737
+ RelationshipType.OVERRIDES,
1738
+ RelationshipType.DEPENDS_ON_EXTERNAL,
1739
+ RelationshipType.IMPORTS,
1540
1740
  ]:
1541
1741
  try:
1542
1742
  result = await self._execute_query(
@@ -1549,7 +1749,12 @@ class CodebaseGraphManager:
1549
1749
 
1550
1750
  # CALLS relationships (multiple tables)
1551
1751
  calls_count = 0
1552
- for table in ["CALLS", "CALLS_FM", "CALLS_MF", "CALLS_MM"]:
1752
+ for table in [
1753
+ RelationshipType.CALLS,
1754
+ RelationshipType.CALLS_FM,
1755
+ RelationshipType.CALLS_MF,
1756
+ RelationshipType.CALLS_MM,
1757
+ ]:
1553
1758
  try:
1554
1759
  result = await self._execute_query(
1555
1760
  graph_id, f"MATCH ()-[r:{table}]->() RETURN COUNT(r) as count"
@@ -1563,16 +1768,21 @@ class CodebaseGraphManager:
1563
1768
 
1564
1769
  # TRACKS relationships
1565
1770
  tracks_count = 0
1566
- for entity in ["Module", "Class", "Function", "Method"]:
1771
+ for tracks_rel in [
1772
+ RelationshipType.TRACKS_MODULE,
1773
+ RelationshipType.TRACKS_CLASS,
1774
+ RelationshipType.TRACKS_FUNCTION,
1775
+ RelationshipType.TRACKS_METHOD,
1776
+ ]:
1567
1777
  try:
1568
1778
  result = await self._execute_query(
1569
1779
  graph_id,
1570
- f"MATCH ()-[r:TRACKS_{entity}]->() RETURN COUNT(r) as count",
1780
+ f"MATCH ()-[r:{tracks_rel}]->() RETURN COUNT(r) as count",
1571
1781
  )
1572
1782
  if result:
1573
1783
  tracks_count += result[0]["count"]
1574
1784
  except Exception as e:
1575
- logger.debug(f"Failed to count TRACKS_{entity} relationships: {e}")
1785
+ logger.debug(f"Failed to count {tracks_rel} relationships: {e}")
1576
1786
  if tracks_count > 0:
1577
1787
  rel_counts["TRACKS (total)"] = tracks_count
1578
1788
 
@@ -1586,16 +1796,16 @@ class CodebaseGraphManager:
1586
1796
 
1587
1797
  # Print node stats
1588
1798
  for node_type in [
1589
- "Project",
1590
- "Package",
1591
- "Module",
1592
- "Class",
1593
- "Function",
1594
- "Method",
1595
- "File",
1596
- "Folder",
1597
- "FileMetadata",
1598
- "DeletionLog",
1799
+ NodeLabel.PROJECT,
1800
+ NodeLabel.PACKAGE,
1801
+ NodeLabel.MODULE,
1802
+ NodeLabel.CLASS,
1803
+ NodeLabel.FUNCTION,
1804
+ NodeLabel.METHOD,
1805
+ NodeLabel.FILE,
1806
+ NodeLabel.FOLDER,
1807
+ NodeLabel.FILE_METADATA,
1808
+ NodeLabel.DELETION_LOG,
1599
1809
  ]:
1600
1810
  count = node_stats.get(node_type, 0)
1601
1811
  logger.info(f"{node_type}: {count}")
@@ -1781,7 +1991,7 @@ class CodebaseGraphManager:
1781
1991
  Graph ID of the graph being built
1782
1992
  """
1783
1993
  repo_path = str(Path(repo_path).resolve())
1784
- graph_id = self._generate_graph_id(repo_path)
1994
+ graph_id = self.generate_graph_id(repo_path)
1785
1995
 
1786
1996
  # Use repository name as default name
1787
1997
  if not name: