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.
- shotgun/agents/agent_manager.py +497 -30
- shotgun/agents/cancellation.py +103 -0
- shotgun/agents/common.py +90 -77
- shotgun/agents/config/README.md +0 -1
- shotgun/agents/config/manager.py +52 -8
- shotgun/agents/config/models.py +48 -45
- shotgun/agents/config/provider.py +44 -29
- shotgun/agents/conversation/history/file_content_deduplication.py +66 -43
- shotgun/agents/conversation/history/token_counting/base.py +51 -9
- shotgun/agents/export.py +12 -13
- shotgun/agents/file_read.py +176 -0
- shotgun/agents/messages.py +15 -3
- shotgun/agents/models.py +90 -2
- shotgun/agents/plan.py +12 -13
- shotgun/agents/research.py +13 -10
- shotgun/agents/router/__init__.py +47 -0
- shotgun/agents/router/models.py +384 -0
- shotgun/agents/router/router.py +185 -0
- shotgun/agents/router/tools/__init__.py +18 -0
- shotgun/agents/router/tools/delegation_tools.py +557 -0
- shotgun/agents/router/tools/plan_tools.py +403 -0
- shotgun/agents/runner.py +17 -2
- shotgun/agents/specify.py +12 -13
- shotgun/agents/tasks.py +12 -13
- shotgun/agents/tools/__init__.py +8 -0
- shotgun/agents/tools/codebase/directory_lister.py +27 -39
- shotgun/agents/tools/codebase/file_read.py +26 -35
- shotgun/agents/tools/codebase/query_graph.py +9 -0
- shotgun/agents/tools/codebase/retrieve_code.py +9 -0
- shotgun/agents/tools/file_management.py +81 -3
- shotgun/agents/tools/file_read_tools/__init__.py +7 -0
- shotgun/agents/tools/file_read_tools/multimodal_file_read.py +167 -0
- shotgun/agents/tools/markdown_tools/__init__.py +62 -0
- shotgun/agents/tools/markdown_tools/insert_section.py +148 -0
- shotgun/agents/tools/markdown_tools/models.py +86 -0
- shotgun/agents/tools/markdown_tools/remove_section.py +114 -0
- shotgun/agents/tools/markdown_tools/replace_section.py +119 -0
- shotgun/agents/tools/markdown_tools/utils.py +453 -0
- shotgun/agents/tools/registry.py +41 -0
- shotgun/agents/tools/web_search/__init__.py +1 -2
- shotgun/agents/tools/web_search/gemini.py +1 -3
- shotgun/agents/tools/web_search/openai.py +42 -23
- shotgun/attachments/__init__.py +41 -0
- shotgun/attachments/errors.py +60 -0
- shotgun/attachments/models.py +107 -0
- shotgun/attachments/parser.py +257 -0
- shotgun/attachments/processor.py +193 -0
- shotgun/cli/clear.py +2 -2
- shotgun/cli/codebase/commands.py +181 -65
- shotgun/cli/compact.py +2 -2
- shotgun/cli/context.py +2 -2
- shotgun/cli/run.py +90 -0
- shotgun/cli/spec/backup.py +2 -1
- shotgun/cli/spec/commands.py +2 -0
- shotgun/cli/spec/models.py +18 -0
- shotgun/cli/spec/pull_service.py +122 -68
- shotgun/codebase/__init__.py +2 -0
- shotgun/codebase/benchmarks/__init__.py +35 -0
- shotgun/codebase/benchmarks/benchmark_runner.py +309 -0
- shotgun/codebase/benchmarks/exporters.py +119 -0
- shotgun/codebase/benchmarks/formatters/__init__.py +49 -0
- shotgun/codebase/benchmarks/formatters/base.py +34 -0
- shotgun/codebase/benchmarks/formatters/json_formatter.py +106 -0
- shotgun/codebase/benchmarks/formatters/markdown.py +136 -0
- shotgun/codebase/benchmarks/models.py +129 -0
- shotgun/codebase/core/__init__.py +4 -0
- shotgun/codebase/core/call_resolution.py +91 -0
- shotgun/codebase/core/change_detector.py +11 -6
- shotgun/codebase/core/errors.py +159 -0
- shotgun/codebase/core/extractors/__init__.py +23 -0
- shotgun/codebase/core/extractors/base.py +138 -0
- shotgun/codebase/core/extractors/factory.py +63 -0
- shotgun/codebase/core/extractors/go/__init__.py +7 -0
- shotgun/codebase/core/extractors/go/extractor.py +122 -0
- shotgun/codebase/core/extractors/javascript/__init__.py +7 -0
- shotgun/codebase/core/extractors/javascript/extractor.py +132 -0
- shotgun/codebase/core/extractors/protocol.py +109 -0
- shotgun/codebase/core/extractors/python/__init__.py +7 -0
- shotgun/codebase/core/extractors/python/extractor.py +141 -0
- shotgun/codebase/core/extractors/rust/__init__.py +7 -0
- shotgun/codebase/core/extractors/rust/extractor.py +139 -0
- shotgun/codebase/core/extractors/types.py +15 -0
- shotgun/codebase/core/extractors/typescript/__init__.py +7 -0
- shotgun/codebase/core/extractors/typescript/extractor.py +92 -0
- shotgun/codebase/core/gitignore.py +252 -0
- shotgun/codebase/core/ingestor.py +644 -354
- shotgun/codebase/core/kuzu_compat.py +119 -0
- shotgun/codebase/core/language_config.py +239 -0
- shotgun/codebase/core/manager.py +256 -46
- shotgun/codebase/core/metrics_collector.py +310 -0
- shotgun/codebase/core/metrics_types.py +347 -0
- shotgun/codebase/core/parallel_executor.py +424 -0
- shotgun/codebase/core/work_distributor.py +254 -0
- shotgun/codebase/core/worker.py +768 -0
- shotgun/codebase/indexing_state.py +86 -0
- shotgun/codebase/models.py +94 -0
- shotgun/codebase/service.py +13 -0
- shotgun/exceptions.py +1 -1
- shotgun/main.py +2 -10
- shotgun/prompts/agents/export.j2 +2 -0
- shotgun/prompts/agents/file_read.j2 +48 -0
- shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +20 -28
- shotgun/prompts/agents/partials/content_formatting.j2 +12 -33
- shotgun/prompts/agents/partials/interactive_mode.j2 +9 -32
- shotgun/prompts/agents/partials/router_delegation_mode.j2 +35 -0
- shotgun/prompts/agents/plan.j2 +43 -1
- shotgun/prompts/agents/research.j2 +75 -20
- shotgun/prompts/agents/router.j2 +713 -0
- shotgun/prompts/agents/specify.j2 +94 -4
- shotgun/prompts/agents/state/codebase/codebase_graphs_available.j2 +14 -1
- shotgun/prompts/agents/state/system_state.j2 +24 -15
- shotgun/prompts/agents/tasks.j2 +77 -23
- shotgun/settings.py +44 -0
- shotgun/shotgun_web/shared_specs/upload_pipeline.py +38 -0
- shotgun/tui/app.py +90 -23
- shotgun/tui/commands/__init__.py +9 -1
- shotgun/tui/components/attachment_bar.py +87 -0
- shotgun/tui/components/mode_indicator.py +120 -25
- shotgun/tui/components/prompt_input.py +23 -28
- shotgun/tui/components/status_bar.py +5 -4
- shotgun/tui/dependencies.py +58 -8
- shotgun/tui/protocols.py +37 -0
- shotgun/tui/screens/chat/chat.tcss +24 -1
- shotgun/tui/screens/chat/chat_screen.py +1374 -211
- shotgun/tui/screens/chat/codebase_index_prompt_screen.py +8 -4
- shotgun/tui/screens/chat_screen/attachment_hint.py +40 -0
- shotgun/tui/screens/chat_screen/command_providers.py +0 -97
- shotgun/tui/screens/chat_screen/history/agent_response.py +7 -3
- shotgun/tui/screens/chat_screen/history/chat_history.py +49 -6
- shotgun/tui/screens/chat_screen/history/formatters.py +75 -15
- shotgun/tui/screens/chat_screen/history/partial_response.py +11 -1
- shotgun/tui/screens/chat_screen/history/user_question.py +25 -3
- shotgun/tui/screens/chat_screen/messages.py +219 -0
- shotgun/tui/screens/database_locked_dialog.py +219 -0
- shotgun/tui/screens/database_timeout_dialog.py +158 -0
- shotgun/tui/screens/kuzu_error_dialog.py +135 -0
- shotgun/tui/screens/model_picker.py +14 -9
- shotgun/tui/screens/models.py +11 -0
- shotgun/tui/screens/shotgun_auth.py +50 -0
- shotgun/tui/screens/spec_pull.py +2 -0
- shotgun/tui/state/processing_state.py +19 -0
- shotgun/tui/utils/mode_progress.py +20 -86
- shotgun/tui/widgets/__init__.py +2 -1
- shotgun/tui/widgets/approval_widget.py +152 -0
- shotgun/tui/widgets/cascade_confirmation_widget.py +203 -0
- shotgun/tui/widgets/plan_panel.py +129 -0
- shotgun/tui/widgets/step_checkpoint_widget.py +180 -0
- shotgun/tui/widgets/widget_coordinator.py +18 -0
- shotgun/utils/file_system_utils.py +4 -1
- {shotgun_sh-0.2.29.dev2.dist-info → shotgun_sh-0.6.1.dev1.dist-info}/METADATA +88 -34
- shotgun_sh-0.6.1.dev1.dist-info/RECORD +292 -0
- shotgun/cli/export.py +0 -81
- shotgun/cli/plan.py +0 -73
- shotgun/cli/research.py +0 -93
- shotgun/cli/specify.py +0 -70
- shotgun/cli/tasks.py +0 -78
- shotgun/tui/screens/onboarding.py +0 -580
- shotgun_sh-0.2.29.dev2.dist-info/RECORD +0 -229
- {shotgun_sh-0.2.29.dev2.dist-info → shotgun_sh-0.6.1.dev1.dist-info}/WHEEL +0 -0
- {shotgun_sh-0.2.29.dev2.dist-info → shotgun_sh-0.6.1.dev1.dist-info}/entry_points.txt +0 -0
- {shotgun_sh-0.2.29.dev2.dist-info → shotgun_sh-0.6.1.dev1.dist-info}/licenses/LICENSE +0 -0
shotgun/cli/spec/pull_service.py
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
"""Shared spec pull service for CLI and TUI."""
|
|
2
2
|
|
|
3
|
+
import time
|
|
3
4
|
from collections.abc import Callable
|
|
4
5
|
from dataclasses import dataclass
|
|
5
6
|
from datetime import datetime, timezone
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
|
|
8
9
|
from shotgun.logging_config import get_logger
|
|
10
|
+
from shotgun.posthog_telemetry import track_event
|
|
9
11
|
from shotgun.shotgun_web.specs_client import SpecsClient
|
|
10
12
|
from shotgun.shotgun_web.supabase_client import download_file_from_url
|
|
11
13
|
|
|
12
14
|
from .backup import clear_shotgun_dir, create_backup
|
|
13
|
-
from .models import SpecMeta
|
|
15
|
+
from .models import PullPhase, PullSource, SpecMeta
|
|
14
16
|
|
|
15
17
|
logger = get_logger(__name__)
|
|
16
18
|
|
|
@@ -53,6 +55,7 @@ class SpecPullService:
|
|
|
53
55
|
shotgun_dir: Path,
|
|
54
56
|
on_progress: Callable[[PullProgress], None] | None = None,
|
|
55
57
|
is_cancelled: Callable[[], bool] | None = None,
|
|
58
|
+
source: PullSource = PullSource.CLI,
|
|
56
59
|
) -> PullResult:
|
|
57
60
|
"""Pull a spec version to the local directory.
|
|
58
61
|
|
|
@@ -61,10 +64,14 @@ class SpecPullService:
|
|
|
61
64
|
shotgun_dir: Target directory (typically .shotgun/)
|
|
62
65
|
on_progress: Optional callback for progress updates
|
|
63
66
|
is_cancelled: Optional callback to check if cancelled
|
|
67
|
+
source: Source of the pull request (CLI or TUI)
|
|
64
68
|
|
|
65
69
|
Returns:
|
|
66
70
|
PullResult with success status and details
|
|
67
71
|
"""
|
|
72
|
+
start_time = time.time()
|
|
73
|
+
current_phase: PullPhase = PullPhase.STARTING
|
|
74
|
+
track_event("spec_pull_started", {"source": source.value})
|
|
68
75
|
|
|
69
76
|
def report(
|
|
70
77
|
phase: str,
|
|
@@ -83,83 +90,130 @@ class SpecPullService:
|
|
|
83
90
|
)
|
|
84
91
|
|
|
85
92
|
def check_cancelled() -> None:
|
|
93
|
+
nonlocal current_phase
|
|
86
94
|
if is_cancelled and is_cancelled():
|
|
95
|
+
track_event(
|
|
96
|
+
"spec_pull_cancelled",
|
|
97
|
+
{"source": source.value, "phase": current_phase.value},
|
|
98
|
+
)
|
|
87
99
|
raise CancelledError()
|
|
88
100
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
101
|
+
try:
|
|
102
|
+
# Phase 1: Fetch version metadata
|
|
103
|
+
current_phase = PullPhase.FETCHING
|
|
104
|
+
report("Fetching version info...")
|
|
105
|
+
check_cancelled()
|
|
92
106
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
107
|
+
response = await self._client.get_version_with_files(version_id)
|
|
108
|
+
spec_name = response.spec_name
|
|
109
|
+
files = response.files
|
|
110
|
+
|
|
111
|
+
if not files:
|
|
112
|
+
track_event(
|
|
113
|
+
"spec_pull_failed",
|
|
114
|
+
{
|
|
115
|
+
"source": source.value,
|
|
116
|
+
"error_type": "EmptyVersion",
|
|
117
|
+
"phase": current_phase.value,
|
|
118
|
+
},
|
|
119
|
+
)
|
|
120
|
+
return PullResult(
|
|
121
|
+
success=False,
|
|
122
|
+
spec_name=spec_name,
|
|
123
|
+
error="No files in this version.",
|
|
124
|
+
)
|
|
96
125
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
126
|
+
# Phase 2: Backup existing content
|
|
127
|
+
current_phase = PullPhase.BACKUP
|
|
128
|
+
backup_path: str | None = None
|
|
129
|
+
if shotgun_dir.exists():
|
|
130
|
+
report("Backing up existing files...")
|
|
131
|
+
check_cancelled()
|
|
132
|
+
|
|
133
|
+
backup_path = await create_backup(shotgun_dir)
|
|
134
|
+
if backup_path:
|
|
135
|
+
clear_shotgun_dir(shotgun_dir)
|
|
136
|
+
|
|
137
|
+
# Ensure directory exists
|
|
138
|
+
shotgun_dir.mkdir(parents=True, exist_ok=True)
|
|
139
|
+
|
|
140
|
+
# Phase 3: Download files
|
|
141
|
+
current_phase = PullPhase.DOWNLOADING
|
|
142
|
+
total_files = len(files)
|
|
143
|
+
total_bytes = 0
|
|
144
|
+
for idx, file_info in enumerate(files):
|
|
145
|
+
check_cancelled()
|
|
146
|
+
|
|
147
|
+
report(
|
|
148
|
+
f"Downloading files ({idx + 1}/{total_files})...",
|
|
149
|
+
file_index=idx,
|
|
150
|
+
total_files=total_files,
|
|
151
|
+
current_file=file_info.relative_path,
|
|
152
|
+
)
|
|
103
153
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
154
|
+
if not file_info.download_url:
|
|
155
|
+
logger.warning(
|
|
156
|
+
"Skipping file without download URL: %s",
|
|
157
|
+
file_info.relative_path,
|
|
158
|
+
)
|
|
159
|
+
continue
|
|
109
160
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
clear_shotgun_dir(shotgun_dir)
|
|
161
|
+
content = await download_file_from_url(file_info.download_url)
|
|
162
|
+
total_bytes += file_info.size_bytes
|
|
113
163
|
|
|
114
|
-
|
|
115
|
-
|
|
164
|
+
local_path = shotgun_dir / file_info.relative_path
|
|
165
|
+
local_path.parent.mkdir(parents=True, exist_ok=True)
|
|
166
|
+
local_path.write_bytes(content)
|
|
116
167
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
168
|
+
# Phase 4: Write meta.json
|
|
169
|
+
current_phase = PullPhase.FINALIZING
|
|
170
|
+
report("Finalizing...")
|
|
120
171
|
check_cancelled()
|
|
121
172
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
173
|
+
meta = SpecMeta(
|
|
174
|
+
version_id=response.version.id,
|
|
175
|
+
spec_id=response.spec_id,
|
|
176
|
+
spec_name=response.spec_name,
|
|
177
|
+
workspace_id=response.workspace_id,
|
|
178
|
+
is_latest=response.version.is_latest,
|
|
179
|
+
pulled_at=datetime.now(timezone.utc),
|
|
180
|
+
backup_path=backup_path,
|
|
181
|
+
web_url=response.web_url,
|
|
182
|
+
)
|
|
183
|
+
meta_path = shotgun_dir / "meta.json"
|
|
184
|
+
meta_path.write_text(meta.model_dump_json(indent=2))
|
|
185
|
+
|
|
186
|
+
# Track successful completion
|
|
187
|
+
duration = time.time() - start_time
|
|
188
|
+
track_event(
|
|
189
|
+
"spec_pull_completed",
|
|
190
|
+
{
|
|
191
|
+
"source": source.value,
|
|
192
|
+
"file_count": total_files,
|
|
193
|
+
"total_bytes": total_bytes,
|
|
194
|
+
"duration_seconds": round(duration, 2),
|
|
195
|
+
"had_backup": backup_path is not None,
|
|
196
|
+
},
|
|
127
197
|
)
|
|
128
198
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
workspace_id=response.workspace_id,
|
|
151
|
-
is_latest=response.version.is_latest,
|
|
152
|
-
pulled_at=datetime.now(timezone.utc),
|
|
153
|
-
backup_path=backup_path,
|
|
154
|
-
web_url=response.web_url,
|
|
155
|
-
)
|
|
156
|
-
meta_path = shotgun_dir / "meta.json"
|
|
157
|
-
meta_path.write_text(meta.model_dump_json(indent=2))
|
|
158
|
-
|
|
159
|
-
return PullResult(
|
|
160
|
-
success=True,
|
|
161
|
-
spec_name=spec_name,
|
|
162
|
-
file_count=total_files,
|
|
163
|
-
backup_path=backup_path,
|
|
164
|
-
web_url=response.web_url,
|
|
165
|
-
)
|
|
199
|
+
return PullResult(
|
|
200
|
+
success=True,
|
|
201
|
+
spec_name=spec_name,
|
|
202
|
+
file_count=total_files,
|
|
203
|
+
backup_path=backup_path,
|
|
204
|
+
web_url=response.web_url,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
except CancelledError:
|
|
208
|
+
# Already tracked in check_cancelled()
|
|
209
|
+
raise
|
|
210
|
+
except Exception as e:
|
|
211
|
+
track_event(
|
|
212
|
+
"spec_pull_failed",
|
|
213
|
+
{
|
|
214
|
+
"source": source.value,
|
|
215
|
+
"error_type": type(e).__name__,
|
|
216
|
+
"phase": current_phase.value,
|
|
217
|
+
},
|
|
218
|
+
)
|
|
219
|
+
raise
|
shotgun/codebase/__init__.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Shotgun codebase analysis and graph management."""
|
|
2
2
|
|
|
3
|
+
from shotgun.codebase.indexing_state import IndexingState
|
|
3
4
|
from shotgun.codebase.models import CodebaseGraph, GraphStatus, QueryResult, QueryType
|
|
4
5
|
from shotgun.codebase.service import CodebaseService
|
|
5
6
|
|
|
@@ -7,6 +8,7 @@ __all__ = [
|
|
|
7
8
|
"CodebaseService",
|
|
8
9
|
"CodebaseGraph",
|
|
9
10
|
"GraphStatus",
|
|
11
|
+
"IndexingState",
|
|
10
12
|
"QueryResult",
|
|
11
13
|
"QueryType",
|
|
12
14
|
]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Benchmark system for codebase indexing performance analysis.
|
|
2
|
+
|
|
3
|
+
This package provides tools for running benchmarks and reporting metrics
|
|
4
|
+
for the codebase indexing pipeline.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from shotgun.codebase.benchmarks.benchmark_runner import BenchmarkRunner
|
|
8
|
+
from shotgun.codebase.benchmarks.exporters import MetricsExporter
|
|
9
|
+
from shotgun.codebase.benchmarks.formatters import (
|
|
10
|
+
JsonFormatter,
|
|
11
|
+
MarkdownFormatter,
|
|
12
|
+
MetricsDisplayOptions,
|
|
13
|
+
get_formatter,
|
|
14
|
+
)
|
|
15
|
+
from shotgun.codebase.benchmarks.models import (
|
|
16
|
+
BenchmarkConfig,
|
|
17
|
+
BenchmarkMode,
|
|
18
|
+
BenchmarkResults,
|
|
19
|
+
BenchmarkRun,
|
|
20
|
+
OutputFormat,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"BenchmarkConfig",
|
|
25
|
+
"BenchmarkMode",
|
|
26
|
+
"BenchmarkResults",
|
|
27
|
+
"BenchmarkRun",
|
|
28
|
+
"BenchmarkRunner",
|
|
29
|
+
"JsonFormatter",
|
|
30
|
+
"MarkdownFormatter",
|
|
31
|
+
"MetricsDisplayOptions",
|
|
32
|
+
"MetricsExporter",
|
|
33
|
+
"OutputFormat",
|
|
34
|
+
"get_formatter",
|
|
35
|
+
]
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
"""Benchmark runner for codebase indexing performance analysis.
|
|
2
|
+
|
|
3
|
+
This module provides the BenchmarkRunner class for running benchmark iterations
|
|
4
|
+
and collecting performance statistics.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import gc
|
|
10
|
+
import hashlib
|
|
11
|
+
import json
|
|
12
|
+
import shutil
|
|
13
|
+
import time
|
|
14
|
+
from collections.abc import Callable
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from shotgun.codebase.benchmarks.models import (
|
|
19
|
+
BenchmarkConfig,
|
|
20
|
+
BenchmarkMode,
|
|
21
|
+
BenchmarkResults,
|
|
22
|
+
BenchmarkRun,
|
|
23
|
+
)
|
|
24
|
+
from shotgun.codebase.core import Ingestor, SimpleGraphBuilder
|
|
25
|
+
from shotgun.codebase.core.kuzu_compat import get_kuzu
|
|
26
|
+
from shotgun.codebase.core.metrics_collector import MetricsCollector
|
|
27
|
+
from shotgun.codebase.core.parser_loader import load_parsers
|
|
28
|
+
from shotgun.logging_config import get_logger
|
|
29
|
+
from shotgun.sdk.services import get_codebase_service
|
|
30
|
+
from shotgun.utils.file_system_utils import get_shotgun_home
|
|
31
|
+
|
|
32
|
+
logger = get_logger(__name__)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _compute_graph_id(codebase_path: Path) -> str:
|
|
36
|
+
"""Compute a unique graph ID from the codebase path.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
codebase_path: Path to the codebase
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
A 12-character hex string identifying this codebase
|
|
43
|
+
"""
|
|
44
|
+
return hashlib.sha256(str(codebase_path).encode()).hexdigest()[:12]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class BenchmarkRunner:
|
|
48
|
+
"""Runs benchmark iterations and collects statistics."""
|
|
49
|
+
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
codebase_path: Path,
|
|
53
|
+
codebase_name: str,
|
|
54
|
+
iterations: int = 1,
|
|
55
|
+
warmup_iterations: int = 0,
|
|
56
|
+
parallel: bool = True,
|
|
57
|
+
worker_count: int | None = None,
|
|
58
|
+
collect_file_metrics: bool = True,
|
|
59
|
+
collect_worker_metrics: bool = True,
|
|
60
|
+
progress_callback: Callable[..., Any] | None = None,
|
|
61
|
+
) -> None:
|
|
62
|
+
"""Initialize benchmark runner.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
codebase_path: Path to repository to benchmark
|
|
66
|
+
codebase_name: Human-readable name for the codebase
|
|
67
|
+
iterations: Number of measured benchmark runs
|
|
68
|
+
warmup_iterations: Number of warmup runs (not measured)
|
|
69
|
+
parallel: Whether to use parallel execution
|
|
70
|
+
worker_count: Number of workers (None = auto)
|
|
71
|
+
collect_file_metrics: Whether to collect per-file metrics
|
|
72
|
+
collect_worker_metrics: Whether to collect per-worker metrics
|
|
73
|
+
progress_callback: Optional callback for progress updates
|
|
74
|
+
"""
|
|
75
|
+
self.codebase_path = codebase_path.resolve()
|
|
76
|
+
self.codebase_name = codebase_name
|
|
77
|
+
self.iterations = iterations
|
|
78
|
+
self.warmup_iterations = warmup_iterations
|
|
79
|
+
self.parallel = parallel
|
|
80
|
+
self.worker_count = worker_count
|
|
81
|
+
self.collect_file_metrics = collect_file_metrics
|
|
82
|
+
self.collect_worker_metrics = collect_worker_metrics
|
|
83
|
+
self.progress_callback = progress_callback
|
|
84
|
+
|
|
85
|
+
# Configuration object
|
|
86
|
+
self.config = BenchmarkConfig(
|
|
87
|
+
mode=BenchmarkMode.PARALLEL if parallel else BenchmarkMode.SEQUENTIAL,
|
|
88
|
+
worker_count=worker_count,
|
|
89
|
+
iterations=iterations,
|
|
90
|
+
warmup_iterations=warmup_iterations,
|
|
91
|
+
collect_file_metrics=collect_file_metrics,
|
|
92
|
+
collect_worker_metrics=collect_worker_metrics,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Storage for database operations
|
|
96
|
+
self._storage_dir = get_shotgun_home() / "codebases"
|
|
97
|
+
self._service = get_codebase_service(self._storage_dir)
|
|
98
|
+
|
|
99
|
+
async def run(self) -> BenchmarkResults:
|
|
100
|
+
"""Run all benchmark iterations and return aggregated results.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
BenchmarkResults with all run data and statistics
|
|
104
|
+
"""
|
|
105
|
+
results = BenchmarkResults(
|
|
106
|
+
codebase_name=self.codebase_name,
|
|
107
|
+
codebase_path=str(self.codebase_path),
|
|
108
|
+
config=self.config,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# Run warmup iterations
|
|
112
|
+
for i in range(self.warmup_iterations):
|
|
113
|
+
logger.info(f"Running warmup iteration {i + 1}/{self.warmup_iterations}...")
|
|
114
|
+
if self.progress_callback:
|
|
115
|
+
self.progress_callback(
|
|
116
|
+
f"Warmup {i + 1}/{self.warmup_iterations}", None, None
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
run = await self._run_single_iteration(
|
|
120
|
+
run_id=i,
|
|
121
|
+
is_warmup=True,
|
|
122
|
+
)
|
|
123
|
+
results.add_run(run)
|
|
124
|
+
await self._cleanup_database()
|
|
125
|
+
|
|
126
|
+
# Run measured iterations
|
|
127
|
+
for i in range(self.iterations):
|
|
128
|
+
logger.info(f"Running benchmark iteration {i + 1}/{self.iterations}...")
|
|
129
|
+
if self.progress_callback:
|
|
130
|
+
self.progress_callback(
|
|
131
|
+
f"Benchmark {i + 1}/{self.iterations}", None, None
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
run = await self._run_single_iteration(
|
|
135
|
+
run_id=i,
|
|
136
|
+
is_warmup=False,
|
|
137
|
+
)
|
|
138
|
+
results.add_run(run)
|
|
139
|
+
|
|
140
|
+
# Clean up between iterations (but not after the last one)
|
|
141
|
+
if i < self.iterations - 1:
|
|
142
|
+
await self._cleanup_database()
|
|
143
|
+
|
|
144
|
+
# Register the codebase so it persists after benchmark
|
|
145
|
+
await self._register_codebase()
|
|
146
|
+
|
|
147
|
+
# Calculate statistics
|
|
148
|
+
results.calculate_statistics()
|
|
149
|
+
|
|
150
|
+
logger.info(
|
|
151
|
+
f"Benchmark complete: {self.iterations} iterations, "
|
|
152
|
+
f"avg {results.avg_duration_seconds:.2f}s"
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
return results
|
|
156
|
+
|
|
157
|
+
async def _run_single_iteration(
|
|
158
|
+
self,
|
|
159
|
+
run_id: int,
|
|
160
|
+
is_warmup: bool,
|
|
161
|
+
) -> BenchmarkRun:
|
|
162
|
+
"""Run a single benchmark iteration.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
run_id: Run number
|
|
166
|
+
is_warmup: Whether this is a warmup run
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
BenchmarkRun with collected metrics
|
|
170
|
+
"""
|
|
171
|
+
# Create metrics collector
|
|
172
|
+
metrics_collector = MetricsCollector(
|
|
173
|
+
codebase_name=self.codebase_name,
|
|
174
|
+
collect_file_metrics=self.collect_file_metrics,
|
|
175
|
+
collect_worker_metrics=self.collect_worker_metrics,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# Generate unique graph ID for this run
|
|
179
|
+
graph_id = _compute_graph_id(self.codebase_path)
|
|
180
|
+
|
|
181
|
+
# Create database
|
|
182
|
+
kuzu = get_kuzu()
|
|
183
|
+
graph_path = self._storage_dir / f"{graph_id}.kuzu"
|
|
184
|
+
|
|
185
|
+
# Ensure clean state
|
|
186
|
+
if graph_path.exists():
|
|
187
|
+
if graph_path.is_dir():
|
|
188
|
+
shutil.rmtree(graph_path)
|
|
189
|
+
else:
|
|
190
|
+
graph_path.unlink()
|
|
191
|
+
wal_path = self._storage_dir / f"{graph_id}.kuzu.wal"
|
|
192
|
+
if wal_path.exists():
|
|
193
|
+
wal_path.unlink()
|
|
194
|
+
|
|
195
|
+
# Create database and connection
|
|
196
|
+
db = kuzu.Database(str(graph_path))
|
|
197
|
+
conn = kuzu.Connection(db)
|
|
198
|
+
|
|
199
|
+
# Load parsers
|
|
200
|
+
parsers, queries = load_parsers()
|
|
201
|
+
|
|
202
|
+
# Create ingestor and builder
|
|
203
|
+
ingestor = Ingestor(conn)
|
|
204
|
+
ingestor.create_schema()
|
|
205
|
+
|
|
206
|
+
builder = SimpleGraphBuilder(
|
|
207
|
+
ingestor=ingestor,
|
|
208
|
+
repo_path=self.codebase_path,
|
|
209
|
+
parsers=parsers,
|
|
210
|
+
queries=queries,
|
|
211
|
+
metrics_collector=metrics_collector,
|
|
212
|
+
enable_parallel=self.parallel,
|
|
213
|
+
progress_callback=None, # Disable TUI progress in benchmark mode
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
# Run indexing
|
|
217
|
+
await builder.run()
|
|
218
|
+
|
|
219
|
+
# Get metrics
|
|
220
|
+
metrics = metrics_collector.get_metrics()
|
|
221
|
+
|
|
222
|
+
# Close connection
|
|
223
|
+
del conn
|
|
224
|
+
del db
|
|
225
|
+
|
|
226
|
+
return BenchmarkRun(
|
|
227
|
+
run_id=run_id,
|
|
228
|
+
is_warmup=is_warmup,
|
|
229
|
+
metrics=metrics,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
async def _cleanup_database(self) -> None:
|
|
233
|
+
"""Delete database files and clear caches between runs."""
|
|
234
|
+
graph_id = _compute_graph_id(self.codebase_path)
|
|
235
|
+
|
|
236
|
+
# Delete database file
|
|
237
|
+
graph_path = self._storage_dir / f"{graph_id}.kuzu"
|
|
238
|
+
if graph_path.exists():
|
|
239
|
+
if graph_path.is_dir():
|
|
240
|
+
shutil.rmtree(graph_path)
|
|
241
|
+
else:
|
|
242
|
+
graph_path.unlink()
|
|
243
|
+
logger.debug(f"Deleted database: {graph_path}")
|
|
244
|
+
|
|
245
|
+
# Delete WAL file
|
|
246
|
+
wal_path = self._storage_dir / f"{graph_id}.kuzu.wal"
|
|
247
|
+
if wal_path.exists():
|
|
248
|
+
wal_path.unlink()
|
|
249
|
+
logger.debug(f"Deleted WAL: {wal_path}")
|
|
250
|
+
|
|
251
|
+
# Force garbage collection
|
|
252
|
+
gc.collect()
|
|
253
|
+
|
|
254
|
+
async def _register_codebase(self) -> None:
|
|
255
|
+
"""Register the codebase so it appears in `shotgun codebase list`.
|
|
256
|
+
|
|
257
|
+
This creates a Project node in the database with metadata that
|
|
258
|
+
identifies the indexed codebase.
|
|
259
|
+
"""
|
|
260
|
+
graph_id = _compute_graph_id(self.codebase_path)
|
|
261
|
+
graph_path = self._storage_dir / f"{graph_id}.kuzu"
|
|
262
|
+
|
|
263
|
+
if not graph_path.exists():
|
|
264
|
+
logger.warning("Cannot register codebase: database not found")
|
|
265
|
+
return
|
|
266
|
+
|
|
267
|
+
kuzu = get_kuzu()
|
|
268
|
+
db = kuzu.Database(str(graph_path))
|
|
269
|
+
conn = kuzu.Connection(db)
|
|
270
|
+
|
|
271
|
+
try:
|
|
272
|
+
# Check if Project node already exists
|
|
273
|
+
result = conn.execute("MATCH (p:Project) RETURN p.graph_id LIMIT 1")
|
|
274
|
+
if result.has_next():
|
|
275
|
+
logger.debug("Project node already exists, skipping registration")
|
|
276
|
+
return
|
|
277
|
+
|
|
278
|
+
# Create Project node with metadata
|
|
279
|
+
current_time = int(time.time())
|
|
280
|
+
conn.execute(
|
|
281
|
+
"""
|
|
282
|
+
CREATE (p:Project {
|
|
283
|
+
name: $name,
|
|
284
|
+
repo_path: $repo_path,
|
|
285
|
+
graph_id: $graph_id,
|
|
286
|
+
created_at: $created_at,
|
|
287
|
+
updated_at: $updated_at,
|
|
288
|
+
schema_version: $schema_version,
|
|
289
|
+
build_options: $build_options,
|
|
290
|
+
indexed_from_cwds: $indexed_from_cwds
|
|
291
|
+
})
|
|
292
|
+
""",
|
|
293
|
+
{
|
|
294
|
+
"name": self.codebase_name,
|
|
295
|
+
"repo_path": str(self.codebase_path),
|
|
296
|
+
"graph_id": graph_id,
|
|
297
|
+
"created_at": current_time,
|
|
298
|
+
"updated_at": current_time,
|
|
299
|
+
"schema_version": "1.0.0",
|
|
300
|
+
"build_options": json.dumps({}),
|
|
301
|
+
"indexed_from_cwds": json.dumps([str(Path.cwd())]),
|
|
302
|
+
},
|
|
303
|
+
)
|
|
304
|
+
logger.info(
|
|
305
|
+
f"Registered codebase '{self.codebase_name}' with graph_id: {graph_id}"
|
|
306
|
+
)
|
|
307
|
+
finally:
|
|
308
|
+
del conn
|
|
309
|
+
del db
|