shotgun-sh 0.2.29.dev2__py3-none-any.whl → 0.6.1.dev1__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.

Potentially problematic release.


This version of shotgun-sh might be problematic. Click here for more details.

Files changed (161) 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 +48 -45
  7. shotgun/agents/config/provider.py +44 -29
  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 +41 -0
  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/cli/clear.py +2 -2
  49. shotgun/cli/codebase/commands.py +181 -65
  50. shotgun/cli/compact.py +2 -2
  51. shotgun/cli/context.py +2 -2
  52. shotgun/cli/run.py +90 -0
  53. shotgun/cli/spec/backup.py +2 -1
  54. shotgun/cli/spec/commands.py +2 -0
  55. shotgun/cli/spec/models.py +18 -0
  56. shotgun/cli/spec/pull_service.py +122 -68
  57. shotgun/codebase/__init__.py +2 -0
  58. shotgun/codebase/benchmarks/__init__.py +35 -0
  59. shotgun/codebase/benchmarks/benchmark_runner.py +309 -0
  60. shotgun/codebase/benchmarks/exporters.py +119 -0
  61. shotgun/codebase/benchmarks/formatters/__init__.py +49 -0
  62. shotgun/codebase/benchmarks/formatters/base.py +34 -0
  63. shotgun/codebase/benchmarks/formatters/json_formatter.py +106 -0
  64. shotgun/codebase/benchmarks/formatters/markdown.py +136 -0
  65. shotgun/codebase/benchmarks/models.py +129 -0
  66. shotgun/codebase/core/__init__.py +4 -0
  67. shotgun/codebase/core/call_resolution.py +91 -0
  68. shotgun/codebase/core/change_detector.py +11 -6
  69. shotgun/codebase/core/errors.py +159 -0
  70. shotgun/codebase/core/extractors/__init__.py +23 -0
  71. shotgun/codebase/core/extractors/base.py +138 -0
  72. shotgun/codebase/core/extractors/factory.py +63 -0
  73. shotgun/codebase/core/extractors/go/__init__.py +7 -0
  74. shotgun/codebase/core/extractors/go/extractor.py +122 -0
  75. shotgun/codebase/core/extractors/javascript/__init__.py +7 -0
  76. shotgun/codebase/core/extractors/javascript/extractor.py +132 -0
  77. shotgun/codebase/core/extractors/protocol.py +109 -0
  78. shotgun/codebase/core/extractors/python/__init__.py +7 -0
  79. shotgun/codebase/core/extractors/python/extractor.py +141 -0
  80. shotgun/codebase/core/extractors/rust/__init__.py +7 -0
  81. shotgun/codebase/core/extractors/rust/extractor.py +139 -0
  82. shotgun/codebase/core/extractors/types.py +15 -0
  83. shotgun/codebase/core/extractors/typescript/__init__.py +7 -0
  84. shotgun/codebase/core/extractors/typescript/extractor.py +92 -0
  85. shotgun/codebase/core/gitignore.py +252 -0
  86. shotgun/codebase/core/ingestor.py +644 -354
  87. shotgun/codebase/core/kuzu_compat.py +119 -0
  88. shotgun/codebase/core/language_config.py +239 -0
  89. shotgun/codebase/core/manager.py +256 -46
  90. shotgun/codebase/core/metrics_collector.py +310 -0
  91. shotgun/codebase/core/metrics_types.py +347 -0
  92. shotgun/codebase/core/parallel_executor.py +424 -0
  93. shotgun/codebase/core/work_distributor.py +254 -0
  94. shotgun/codebase/core/worker.py +768 -0
  95. shotgun/codebase/indexing_state.py +86 -0
  96. shotgun/codebase/models.py +94 -0
  97. shotgun/codebase/service.py +13 -0
  98. shotgun/exceptions.py +1 -1
  99. shotgun/main.py +2 -10
  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 +20 -28
  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 +43 -1
  107. shotgun/prompts/agents/research.j2 +75 -20
  108. shotgun/prompts/agents/router.j2 +713 -0
  109. shotgun/prompts/agents/specify.j2 +94 -4
  110. shotgun/prompts/agents/state/codebase/codebase_graphs_available.j2 +14 -1
  111. shotgun/prompts/agents/state/system_state.j2 +24 -15
  112. shotgun/prompts/agents/tasks.j2 +77 -23
  113. shotgun/settings.py +44 -0
  114. shotgun/shotgun_web/shared_specs/upload_pipeline.py +38 -0
  115. shotgun/tui/app.py +90 -23
  116. shotgun/tui/commands/__init__.py +9 -1
  117. shotgun/tui/components/attachment_bar.py +87 -0
  118. shotgun/tui/components/mode_indicator.py +120 -25
  119. shotgun/tui/components/prompt_input.py +23 -28
  120. shotgun/tui/components/status_bar.py +5 -4
  121. shotgun/tui/dependencies.py +58 -8
  122. shotgun/tui/protocols.py +37 -0
  123. shotgun/tui/screens/chat/chat.tcss +24 -1
  124. shotgun/tui/screens/chat/chat_screen.py +1374 -211
  125. shotgun/tui/screens/chat/codebase_index_prompt_screen.py +8 -4
  126. shotgun/tui/screens/chat_screen/attachment_hint.py +40 -0
  127. shotgun/tui/screens/chat_screen/command_providers.py +0 -97
  128. shotgun/tui/screens/chat_screen/history/agent_response.py +7 -3
  129. shotgun/tui/screens/chat_screen/history/chat_history.py +49 -6
  130. shotgun/tui/screens/chat_screen/history/formatters.py +75 -15
  131. shotgun/tui/screens/chat_screen/history/partial_response.py +11 -1
  132. shotgun/tui/screens/chat_screen/history/user_question.py +25 -3
  133. shotgun/tui/screens/chat_screen/messages.py +219 -0
  134. shotgun/tui/screens/database_locked_dialog.py +219 -0
  135. shotgun/tui/screens/database_timeout_dialog.py +158 -0
  136. shotgun/tui/screens/kuzu_error_dialog.py +135 -0
  137. shotgun/tui/screens/model_picker.py +14 -9
  138. shotgun/tui/screens/models.py +11 -0
  139. shotgun/tui/screens/shotgun_auth.py +50 -0
  140. shotgun/tui/screens/spec_pull.py +2 -0
  141. shotgun/tui/state/processing_state.py +19 -0
  142. shotgun/tui/utils/mode_progress.py +20 -86
  143. shotgun/tui/widgets/__init__.py +2 -1
  144. shotgun/tui/widgets/approval_widget.py +152 -0
  145. shotgun/tui/widgets/cascade_confirmation_widget.py +203 -0
  146. shotgun/tui/widgets/plan_panel.py +129 -0
  147. shotgun/tui/widgets/step_checkpoint_widget.py +180 -0
  148. shotgun/tui/widgets/widget_coordinator.py +18 -0
  149. shotgun/utils/file_system_utils.py +4 -1
  150. {shotgun_sh-0.2.29.dev2.dist-info → shotgun_sh-0.6.1.dev1.dist-info}/METADATA +88 -34
  151. shotgun_sh-0.6.1.dev1.dist-info/RECORD +292 -0
  152. shotgun/cli/export.py +0 -81
  153. shotgun/cli/plan.py +0 -73
  154. shotgun/cli/research.py +0 -93
  155. shotgun/cli/specify.py +0 -70
  156. shotgun/cli/tasks.py +0 -78
  157. shotgun/tui/screens/onboarding.py +0 -580
  158. shotgun_sh-0.2.29.dev2.dist-info/RECORD +0 -229
  159. {shotgun_sh-0.2.29.dev2.dist-info → shotgun_sh-0.6.1.dev1.dist-info}/WHEEL +0 -0
  160. {shotgun_sh-0.2.29.dev2.dist-info → shotgun_sh-0.6.1.dev1.dist-info}/entry_points.txt +0 -0
  161. {shotgun_sh-0.2.29.dev2.dist-info → shotgun_sh-0.6.1.dev1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,119 @@
1
+ """Metrics exporters for saving benchmark results to files.
2
+
3
+ This module provides the MetricsExporter class for exporting benchmark
4
+ results to various file formats.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from pathlib import Path
10
+ from typing import TYPE_CHECKING
11
+
12
+ from shotgun.codebase.benchmarks.formatters import (
13
+ JsonFormatter,
14
+ MarkdownFormatter,
15
+ MetricsDisplayOptions,
16
+ )
17
+ from shotgun.logging_config import get_logger
18
+
19
+ if TYPE_CHECKING:
20
+ from shotgun.codebase.benchmarks.models import BenchmarkResults
21
+
22
+ logger = get_logger(__name__)
23
+
24
+
25
+ class MetricsExporter:
26
+ """Export benchmark metrics to files."""
27
+
28
+ def __init__(self) -> None:
29
+ """Initialize metrics exporter."""
30
+ self._format_map = {
31
+ ".json": self._export_json,
32
+ ".md": self._export_markdown,
33
+ ".markdown": self._export_markdown,
34
+ }
35
+
36
+ def export(
37
+ self,
38
+ results: BenchmarkResults,
39
+ filepath: Path | str,
40
+ format: str | None = None,
41
+ options: MetricsDisplayOptions | None = None,
42
+ ) -> None:
43
+ """Export benchmark results to file.
44
+
45
+ The format is auto-detected from the file extension if not specified.
46
+
47
+ Args:
48
+ results: Benchmark results to export
49
+ filepath: Path to export file
50
+ format: Optional format override ("json", "markdown")
51
+ options: Display options for controlling what to include
52
+
53
+ Raises:
54
+ ValueError: If format cannot be determined or is unsupported
55
+ OSError: If file cannot be written
56
+ """
57
+ filepath = Path(filepath)
58
+ options = options or MetricsDisplayOptions()
59
+
60
+ # Determine format
61
+ if format:
62
+ format_lower = format.lower()
63
+ if format_lower == "json":
64
+ export_func = self._export_json
65
+ elif format_lower in ("markdown", "md"):
66
+ export_func = self._export_markdown
67
+ else:
68
+ raise ValueError(f"Unknown export format: {format}")
69
+ else:
70
+ # Auto-detect from extension
71
+ suffix = filepath.suffix.lower()
72
+ if suffix not in self._format_map:
73
+ raise ValueError(
74
+ f"Cannot determine format from extension '{suffix}'. "
75
+ f"Supported extensions: {', '.join(self._format_map.keys())}. "
76
+ f"Or specify format explicitly."
77
+ )
78
+ export_func = self._format_map[suffix]
79
+
80
+ # Ensure parent directory exists
81
+ filepath.parent.mkdir(parents=True, exist_ok=True)
82
+
83
+ # Export
84
+ export_func(results, filepath, options)
85
+ logger.info(f"Exported benchmark results to {filepath}")
86
+
87
+ def _export_json(
88
+ self,
89
+ results: BenchmarkResults,
90
+ filepath: Path,
91
+ options: MetricsDisplayOptions,
92
+ ) -> None:
93
+ """Export results to JSON file.
94
+
95
+ Args:
96
+ results: Benchmark results
97
+ filepath: Output path
98
+ options: Display options
99
+ """
100
+ formatter = JsonFormatter()
101
+ content = formatter.format_results(results, options)
102
+ filepath.write_text(content)
103
+
104
+ def _export_markdown(
105
+ self,
106
+ results: BenchmarkResults,
107
+ filepath: Path,
108
+ options: MetricsDisplayOptions,
109
+ ) -> None:
110
+ """Export results to Markdown file.
111
+
112
+ Args:
113
+ results: Benchmark results
114
+ filepath: Output path
115
+ options: Display options
116
+ """
117
+ formatter = MarkdownFormatter()
118
+ content = formatter.format_results(results, options)
119
+ filepath.write_text(content)
@@ -0,0 +1,49 @@
1
+ """Result formatters for benchmark output.
2
+
3
+ This package provides formatters for displaying benchmark results in various
4
+ formats: JSON and Markdown.
5
+ """
6
+
7
+ from shotgun.codebase.benchmarks.formatters.base import ResultFormatter
8
+ from shotgun.codebase.benchmarks.formatters.json_formatter import JsonFormatter
9
+ from shotgun.codebase.benchmarks.formatters.markdown import MarkdownFormatter
10
+
11
+ # Re-export MetricsDisplayOptions from models for convenience
12
+ from shotgun.codebase.benchmarks.models import MetricsDisplayOptions
13
+
14
+ __all__ = [
15
+ "JsonFormatter",
16
+ "MarkdownFormatter",
17
+ "MetricsDisplayOptions",
18
+ "ResultFormatter",
19
+ "get_formatter",
20
+ ]
21
+
22
+
23
+ def get_formatter(
24
+ output_format: str,
25
+ ) -> JsonFormatter | MarkdownFormatter:
26
+ """Get appropriate formatter for output format.
27
+
28
+ Args:
29
+ output_format: Format name - "json" or "markdown"
30
+
31
+ Returns:
32
+ Formatter instance
33
+
34
+ Raises:
35
+ ValueError: If output format is unknown
36
+ """
37
+ formatters: dict[str, type[JsonFormatter | MarkdownFormatter]] = {
38
+ "json": JsonFormatter,
39
+ "markdown": MarkdownFormatter,
40
+ }
41
+
42
+ format_lower = output_format.lower()
43
+ if format_lower not in formatters:
44
+ raise ValueError(
45
+ f"Unknown output format: {output_format}. "
46
+ f"Valid formats: {', '.join(formatters.keys())}"
47
+ )
48
+
49
+ return formatters[format_lower]()
@@ -0,0 +1,34 @@
1
+ """Base classes and protocols for formatters.
2
+
3
+ This module provides the Protocol interface that all formatters implement.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import TYPE_CHECKING, Protocol
9
+
10
+ if TYPE_CHECKING:
11
+ from shotgun.codebase.benchmarks.models import (
12
+ BenchmarkResults,
13
+ MetricsDisplayOptions,
14
+ )
15
+
16
+
17
+ class ResultFormatter(Protocol):
18
+ """Protocol for formatting benchmark results."""
19
+
20
+ def format_results(
21
+ self,
22
+ results: BenchmarkResults,
23
+ options: MetricsDisplayOptions,
24
+ ) -> str:
25
+ """Format benchmark results for display.
26
+
27
+ Args:
28
+ results: Benchmark results to format
29
+ options: Display options
30
+
31
+ Returns:
32
+ Formatted string ready for output
33
+ """
34
+ ...
@@ -0,0 +1,106 @@
1
+ """JSON formatter for benchmark results.
2
+
3
+ This module provides the JsonFormatter class for displaying benchmark results
4
+ as JSON.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ from typing import TYPE_CHECKING
11
+
12
+ if TYPE_CHECKING:
13
+ from shotgun.codebase.benchmarks.models import (
14
+ BenchmarkResults,
15
+ MetricsDisplayOptions,
16
+ )
17
+
18
+
19
+ class JsonFormatter:
20
+ """Format benchmark results as JSON."""
21
+
22
+ def format_results(
23
+ self,
24
+ results: BenchmarkResults,
25
+ options: MetricsDisplayOptions,
26
+ ) -> str:
27
+ """Format benchmark results as JSON.
28
+
29
+ Args:
30
+ results: Benchmark results to format
31
+ options: Display options
32
+
33
+ Returns:
34
+ JSON string
35
+ """
36
+ data = {
37
+ "codebase_name": results.codebase_name,
38
+ "codebase_path": results.codebase_path,
39
+ "config": {
40
+ "mode": results.config.mode,
41
+ "worker_count": results.config.worker_count,
42
+ "iterations": results.config.iterations,
43
+ "warmup_iterations": results.config.warmup_iterations,
44
+ },
45
+ "statistics": {
46
+ "avg_duration_seconds": results.avg_duration_seconds,
47
+ "min_duration_seconds": results.min_duration_seconds,
48
+ "max_duration_seconds": results.max_duration_seconds,
49
+ "std_dev_seconds": results.std_dev_seconds,
50
+ "avg_throughput": results.avg_throughput,
51
+ "avg_memory_mb": results.avg_memory_mb,
52
+ "speedup_factor": results.speedup_factor,
53
+ "efficiency": results.efficiency,
54
+ },
55
+ "runs": [],
56
+ }
57
+
58
+ # Add run data
59
+ for run in results.measured_runs:
60
+ run_data: dict[str, object] = {
61
+ "run_id": run.run_id,
62
+ "duration_seconds": run.metrics.total_duration_seconds,
63
+ "total_files": run.metrics.total_files,
64
+ "total_nodes": run.metrics.total_nodes,
65
+ "total_relationships": run.metrics.total_relationships,
66
+ "throughput": run.metrics.avg_throughput,
67
+ "peak_memory_mb": run.metrics.peak_memory_mb,
68
+ }
69
+
70
+ # Add phase metrics
71
+ if options.show_phase_metrics:
72
+ phase_data: dict[str, dict[str, object]] = {}
73
+ for name, phase in run.metrics.phase_metrics.items():
74
+ phase_data[name] = {
75
+ "duration_seconds": phase.duration_seconds,
76
+ "items_processed": phase.items_processed,
77
+ "throughput": phase.throughput,
78
+ "memory_mb": phase.memory_mb,
79
+ }
80
+ run_data["phase_metrics"] = phase_data
81
+
82
+ # Add file metrics
83
+ if options.show_file_metrics and run.metrics.file_metrics:
84
+ file_metrics_list = run.metrics.file_metrics
85
+ if options.top_n_files:
86
+ file_metrics_list = sorted(
87
+ file_metrics_list,
88
+ key=lambda f: f.parse_time_ms,
89
+ reverse=True,
90
+ )[: options.top_n_files]
91
+
92
+ run_data["file_metrics"] = [
93
+ {
94
+ "file_path": f.file_path,
95
+ "language": f.language,
96
+ "file_size_bytes": f.file_size_bytes,
97
+ "parse_time_ms": f.parse_time_ms,
98
+ "definitions_extracted": f.definitions_extracted,
99
+ }
100
+ for f in file_metrics_list
101
+ ]
102
+
103
+ runs_list: list[dict[str, object]] = data["runs"] # type: ignore[assignment]
104
+ runs_list.append(run_data)
105
+
106
+ return json.dumps(data, indent=2)
@@ -0,0 +1,136 @@
1
+ """Markdown formatter for benchmark results.
2
+
3
+ This module provides the MarkdownFormatter class for displaying benchmark results
4
+ as GitHub-compatible markdown.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import TYPE_CHECKING
10
+
11
+ if TYPE_CHECKING:
12
+ from shotgun.codebase.benchmarks.models import (
13
+ BenchmarkResults,
14
+ MetricsDisplayOptions,
15
+ )
16
+
17
+
18
+ class MarkdownFormatter:
19
+ """Format benchmark results as GitHub-compatible markdown."""
20
+
21
+ def format_results(
22
+ self,
23
+ results: BenchmarkResults,
24
+ options: MetricsDisplayOptions,
25
+ ) -> str:
26
+ """Format benchmark results as markdown.
27
+
28
+ Args:
29
+ results: Benchmark results to format
30
+ options: Display options
31
+
32
+ Returns:
33
+ Markdown string
34
+ """
35
+ lines = []
36
+
37
+ # Header
38
+ lines.append(f"# Indexing Benchmark: {results.codebase_name}")
39
+ lines.append("")
40
+ lines.append(f"**Path:** `{results.codebase_path}`")
41
+
42
+ mode = results.config.mode.capitalize()
43
+ worker_info = ""
44
+ if results.config.mode == "parallel":
45
+ worker_count = results.config.worker_count or "auto"
46
+ worker_info = f" ({worker_count} workers)"
47
+ lines.append(f"**Mode:** {mode}{worker_info}")
48
+ lines.append(
49
+ f"**Iterations:** {results.config.iterations} ({results.config.warmup_iterations} warmup)"
50
+ )
51
+ lines.append("")
52
+
53
+ # Summary statistics
54
+ lines.append("## Summary")
55
+ lines.append("")
56
+ lines.append("| Metric | Value |")
57
+ lines.append("|--------|-------|")
58
+
59
+ if results.config.iterations > 1:
60
+ lines.append(f"| Duration (avg) | {results.avg_duration_seconds:.2f}s |")
61
+ lines.append(f"| Duration (min) | {results.min_duration_seconds:.2f}s |")
62
+ lines.append(f"| Duration (max) | {results.max_duration_seconds:.2f}s |")
63
+ lines.append(f"| Duration (std dev) | {results.std_dev_seconds:.2f}s |")
64
+ else:
65
+ lines.append(f"| Duration | {results.avg_duration_seconds:.2f}s |")
66
+
67
+ lines.append(f"| Throughput | {results.avg_throughput:.1f} files/s |")
68
+ lines.append(f"| Peak Memory | {results.avg_memory_mb:.1f} MB |")
69
+
70
+ metrics = results.get_last_metrics()
71
+ if metrics:
72
+ lines.append(f"| Files Processed | {metrics.total_files:,} |")
73
+ lines.append(f"| Nodes Created | {metrics.total_nodes:,} |")
74
+ lines.append(f"| Relationships | {metrics.total_relationships:,} |")
75
+
76
+ if results.efficiency:
77
+ lines.append(
78
+ f"| Parallelism Efficiency | {results.efficiency * 100:.0f}% |"
79
+ )
80
+ if results.speedup_factor:
81
+ lines.append(f"| Speedup | {results.speedup_factor:.2f}x |")
82
+
83
+ lines.append("")
84
+
85
+ # Phase breakdown
86
+ if metrics and options.show_phase_metrics and not options.show_summary_only:
87
+ lines.append("## Phase Breakdown")
88
+ lines.append("")
89
+ lines.append("| Phase | Duration | Items | Throughput | Memory |")
90
+ lines.append("|-------|----------|-------|------------|--------|")
91
+
92
+ for name, phase in metrics.phase_metrics.items():
93
+ lines.append(
94
+ f"| {name} | {phase.duration_seconds:.2f}s | "
95
+ f"{phase.items_processed} | {phase.throughput:.1f}/s | "
96
+ f"{phase.memory_mb:.1f} MB |"
97
+ )
98
+
99
+ lines.append("")
100
+
101
+ # File metrics
102
+ if (
103
+ metrics
104
+ and options.show_file_metrics
105
+ and metrics.file_metrics
106
+ and not options.show_summary_only
107
+ ):
108
+ file_metrics = sorted(
109
+ metrics.file_metrics,
110
+ key=lambda f: f.parse_time_ms,
111
+ reverse=True,
112
+ )
113
+ if options.top_n_files:
114
+ file_metrics = file_metrics[: options.top_n_files]
115
+
116
+ if file_metrics:
117
+ title = "File Metrics"
118
+ if options.top_n_files:
119
+ title = f"Top {len(file_metrics)} Slowest Files"
120
+
121
+ lines.append(f"## {title}")
122
+ lines.append("")
123
+ lines.append("| File | Language | Size | Duration | Definitions |")
124
+ lines.append("|------|----------|------|----------|-------------|")
125
+
126
+ for f in file_metrics:
127
+ size_kb = f.file_size_bytes / 1024
128
+ lines.append(
129
+ f"| `{f.file_path}` | {f.language} | "
130
+ f"{size_kb:.1f} KB | {f.parse_time_ms:.1f}ms | "
131
+ f"{f.definitions_extracted} |"
132
+ )
133
+
134
+ lines.append("")
135
+
136
+ return "\n".join(lines)
@@ -0,0 +1,129 @@
1
+ """Pydantic models for benchmark system.
2
+
3
+ This module contains all data models used by the benchmark system.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import statistics
9
+ from enum import StrEnum
10
+
11
+ from pydantic import BaseModel, Field
12
+
13
+ from shotgun.codebase.core.metrics_types import IndexingMetrics
14
+
15
+
16
+ class BenchmarkMode(StrEnum):
17
+ """Execution mode for benchmarks."""
18
+
19
+ PARALLEL = "parallel"
20
+ SEQUENTIAL = "sequential"
21
+
22
+
23
+ class OutputFormat(StrEnum):
24
+ """Output format for benchmark results."""
25
+
26
+ JSON = "json"
27
+ MARKDOWN = "markdown"
28
+
29
+
30
+ class BenchmarkConfig(BaseModel):
31
+ """Configuration for benchmark execution."""
32
+
33
+ mode: BenchmarkMode = BenchmarkMode.PARALLEL
34
+ worker_count: int | None = None
35
+ iterations: int = 1
36
+ warmup_iterations: int = 0
37
+ collect_file_metrics: bool = True
38
+ collect_worker_metrics: bool = True
39
+
40
+
41
+ class BenchmarkRun(BaseModel):
42
+ """Results from a single benchmark run."""
43
+
44
+ run_id: int
45
+ is_warmup: bool
46
+ metrics: IndexingMetrics
47
+
48
+
49
+ class BenchmarkResults(BaseModel):
50
+ """Complete results from benchmark execution."""
51
+
52
+ codebase_name: str
53
+ codebase_path: str
54
+ config: BenchmarkConfig
55
+ warmup_runs: list[BenchmarkRun] = Field(default_factory=list)
56
+ measured_runs: list[BenchmarkRun] = Field(default_factory=list)
57
+
58
+ # Aggregate statistics (calculated after runs)
59
+ avg_duration_seconds: float = 0.0
60
+ min_duration_seconds: float = 0.0
61
+ max_duration_seconds: float = 0.0
62
+ std_dev_seconds: float = 0.0
63
+ avg_throughput: float = 0.0
64
+ avg_memory_mb: float = 0.0
65
+
66
+ # Comparison data
67
+ baseline_duration: float | None = None
68
+ speedup_factor: float | None = None
69
+ efficiency: float | None = None
70
+
71
+ def add_run(self, run: BenchmarkRun) -> None:
72
+ """Add a benchmark run to results.
73
+
74
+ Args:
75
+ run: Benchmark run to add
76
+ """
77
+ if run.is_warmup:
78
+ self.warmup_runs.append(run)
79
+ else:
80
+ self.measured_runs.append(run)
81
+
82
+ def calculate_statistics(self) -> None:
83
+ """Calculate aggregate statistics from measured runs."""
84
+ if not self.measured_runs:
85
+ return
86
+
87
+ durations = [r.metrics.total_duration_seconds for r in self.measured_runs]
88
+ throughputs = [r.metrics.avg_throughput for r in self.measured_runs]
89
+ memories = [r.metrics.peak_memory_mb for r in self.measured_runs]
90
+
91
+ self.avg_duration_seconds = statistics.mean(durations)
92
+ self.min_duration_seconds = min(durations)
93
+ self.max_duration_seconds = max(durations)
94
+ self.std_dev_seconds = (
95
+ statistics.stdev(durations) if len(durations) > 1 else 0.0
96
+ )
97
+ self.avg_throughput = statistics.mean(throughputs)
98
+ self.avg_memory_mb = statistics.mean(memories)
99
+
100
+ # Calculate efficiency if parallel mode with known worker count
101
+ if (
102
+ self.config.mode == BenchmarkMode.PARALLEL
103
+ and self.config.worker_count
104
+ and self.baseline_duration
105
+ ):
106
+ speedup = self.baseline_duration / self.avg_duration_seconds
107
+ self.speedup_factor = speedup
108
+ self.efficiency = speedup / self.config.worker_count
109
+
110
+ def get_last_metrics(self) -> IndexingMetrics | None:
111
+ """Get metrics from the last measured run.
112
+
113
+ Returns:
114
+ IndexingMetrics from last run, or None if no runs
115
+ """
116
+ if self.measured_runs:
117
+ return self.measured_runs[-1].metrics
118
+ return None
119
+
120
+
121
+ class MetricsDisplayOptions(BaseModel):
122
+ """Options for controlling metrics display."""
123
+
124
+ show_phase_metrics: bool = True
125
+ show_worker_metrics: bool = False
126
+ show_file_metrics: bool = False
127
+ show_summary_only: bool = False
128
+ top_n_files: int | None = None
129
+ min_file_duration_ms: float | None = None
@@ -5,6 +5,7 @@ from shotgun.codebase.core.code_retrieval import (
5
5
  retrieve_code_by_cypher,
6
6
  retrieve_code_by_qualified_name,
7
7
  )
8
+ from shotgun.codebase.core.gitignore import GitignoreManager, load_gitignore_for_repo
8
9
  from shotgun.codebase.core.ingestor import (
9
10
  CodebaseIngestor,
10
11
  Ingestor,
@@ -29,6 +30,9 @@ __all__ = [
29
30
  "Ingestor",
30
31
  "SimpleGraphBuilder",
31
32
  "CodebaseGraphManager",
33
+ # Gitignore support
34
+ "GitignoreManager",
35
+ "load_gitignore_for_repo",
32
36
  # Language configuration
33
37
  "LanguageConfig",
34
38
  "LANGUAGE_CONFIGS",
@@ -0,0 +1,91 @@
1
+ """Call resolution utilities for function/method call graph building.
2
+
3
+ This module provides shared utilities for resolving function calls
4
+ and calculating confidence scores for potential callee matches.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from collections.abc import Collection, Mapping
10
+
11
+
12
+ def calculate_callee_confidence(
13
+ caller_qn: str,
14
+ callee_qn: str,
15
+ module_qn: str,
16
+ object_name: str | None,
17
+ simple_name_lookup: Mapping[str, Collection[str]],
18
+ ) -> float:
19
+ """Calculate confidence score for a potential callee match.
20
+
21
+ Uses multiple heuristics to determine how likely a given callee
22
+ is the correct target of a function call:
23
+ 1. Module locality - functions in the same module are most likely
24
+ 2. Package locality - functions in the same package hierarchy
25
+ 3. Object/class match for method calls
26
+ 4. Standard library boost
27
+ 5. Name uniqueness boost
28
+
29
+ Args:
30
+ caller_qn: Qualified name of the calling function
31
+ callee_qn: Qualified name of the potential callee
32
+ module_qn: Qualified name of the current module
33
+ object_name: Object name for method calls (e.g., 'obj' in obj.method())
34
+ simple_name_lookup: Mapping from simple names to qualified names
35
+ (supports both set[str] and list[str] values)
36
+
37
+ Returns:
38
+ Confidence score between 0.0 and 1.0
39
+ """
40
+ score = 0.0
41
+
42
+ # 1. Module locality - functions in the same module are most likely
43
+ if callee_qn.startswith(module_qn + "."):
44
+ score += 0.5
45
+
46
+ # Even higher if in the same class
47
+ caller_parts = caller_qn.split(".")
48
+ callee_parts = callee_qn.split(".")
49
+ if len(caller_parts) >= 3 and len(callee_parts) >= 3:
50
+ if caller_parts[:-1] == callee_parts[:-1]: # Same class
51
+ score += 0.2
52
+
53
+ # 2. Package locality - functions in the same package hierarchy
54
+ elif "." in module_qn:
55
+ package = module_qn.rsplit(".", 1)[0]
56
+ if callee_qn.startswith(package + "."):
57
+ score += 0.3
58
+
59
+ # 3. Object/class match for method calls
60
+ if object_name:
61
+ # Check if callee is a method of a class matching the object name
62
+ callee_parts = callee_qn.split(".")
63
+ if len(callee_parts) >= 2:
64
+ # Simple heuristic: check if class name matches object name
65
+ # (In reality, we'd need type inference for accuracy)
66
+ class_name = callee_parts[-2]
67
+ if class_name.lower() == object_name.lower():
68
+ score += 0.3
69
+ elif object_name == "self" and callee_qn.startswith(
70
+ caller_qn.rsplit(".", 1)[0]
71
+ ):
72
+ # 'self' refers to the same class
73
+ score += 0.4
74
+
75
+ # 4. Standard library boost
76
+ # Give a small boost to standard library functions
77
+ if callee_qn.startswith(("builtins.", "typing.", "collections.")):
78
+ score += 0.1
79
+
80
+ # 5. Name uniqueness boost
81
+ # If function names are unique enough, boost confidence
82
+ callee_simple_name = callee_qn.split(".")[-1]
83
+ possible_matches = simple_name_lookup.get(callee_simple_name, [])
84
+ possible_count = len(possible_matches)
85
+ if possible_count == 1:
86
+ score += 0.2
87
+ elif possible_count <= 3:
88
+ score += 0.1
89
+
90
+ # Normalize to [0, 1]
91
+ return min(score, 1.0)