gitview 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gitview/__init__.py +3 -0
- gitview/backends/__init__.py +18 -0
- gitview/backends/anthropic_backend.py +66 -0
- gitview/backends/base.py +60 -0
- gitview/backends/ollama_backend.py +96 -0
- gitview/backends/openai_backend.py +71 -0
- gitview/backends/router.py +164 -0
- gitview/chunker.py +367 -0
- gitview/cli.py +332 -0
- gitview/extractor.py +423 -0
- gitview/storyteller.py +352 -0
- gitview/summarizer.py +270 -0
- gitview/writer.py +271 -0
- gitview-0.1.0.dist-info/METADATA +468 -0
- gitview-0.1.0.dist-info/RECORD +19 -0
- gitview-0.1.0.dist-info/WHEEL +5 -0
- gitview-0.1.0.dist-info/entry_points.txt +2 -0
- gitview-0.1.0.dist-info/licenses/LICENSE +21 -0
- gitview-0.1.0.dist-info/top_level.txt +1 -0
gitview/storyteller.py
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
"""Generate global narrative from phase summaries."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import List, Dict, Any, Optional
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
|
|
8
|
+
from .chunker import Phase
|
|
9
|
+
from .backends import LLMRouter, LLMMessage
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class StoryTeller:
|
|
13
|
+
"""Generate global repository story from phase summaries."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, backend: Optional[str] = None, model: Optional[str] = None,
|
|
16
|
+
api_key: Optional[str] = None, **kwargs):
|
|
17
|
+
"""
|
|
18
|
+
Initialize storyteller with LLM backend.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
backend: LLM backend ('anthropic', 'openai', 'ollama')
|
|
22
|
+
model: Model identifier (uses backend defaults if not specified)
|
|
23
|
+
api_key: API key for the backend (if required)
|
|
24
|
+
**kwargs: Additional backend parameters
|
|
25
|
+
"""
|
|
26
|
+
self.router = LLMRouter(backend=backend, model=model, api_key=api_key, **kwargs)
|
|
27
|
+
self.model = self.router.model
|
|
28
|
+
|
|
29
|
+
def generate_global_story(self, phases: List[Phase],
|
|
30
|
+
repo_name: Optional[str] = None) -> Dict[str, str]:
|
|
31
|
+
"""
|
|
32
|
+
Generate comprehensive repository story from phases.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
phases: List of Phase objects with summaries
|
|
36
|
+
repo_name: Optional repository name
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Dict with different story sections:
|
|
40
|
+
- 'executive_summary': High-level overview
|
|
41
|
+
- 'timeline': Chronological timeline with headings
|
|
42
|
+
- 'technical_evolution': Technical architecture evolution
|
|
43
|
+
- 'deletion_story': Story of what was removed and why
|
|
44
|
+
- 'full_narrative': Complete detailed narrative
|
|
45
|
+
"""
|
|
46
|
+
# Ensure all phases have summaries
|
|
47
|
+
if any(p.summary is None for p in phases):
|
|
48
|
+
raise ValueError("All phases must have summaries before generating global story")
|
|
49
|
+
|
|
50
|
+
print("Generating global story...")
|
|
51
|
+
|
|
52
|
+
# Prepare phase summaries data
|
|
53
|
+
phase_summaries = self._prepare_phase_summaries(phases)
|
|
54
|
+
|
|
55
|
+
# Generate different story sections
|
|
56
|
+
stories = {}
|
|
57
|
+
|
|
58
|
+
print(" - Executive summary...")
|
|
59
|
+
stories['executive_summary'] = self._generate_executive_summary(
|
|
60
|
+
phase_summaries, repo_name
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
print(" - Timeline...")
|
|
64
|
+
stories['timeline'] = self._generate_timeline(phase_summaries, repo_name)
|
|
65
|
+
|
|
66
|
+
print(" - Technical evolution...")
|
|
67
|
+
stories['technical_evolution'] = self._generate_technical_evolution(
|
|
68
|
+
phase_summaries, repo_name
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
print(" - Deletion story...")
|
|
72
|
+
stories['deletion_story'] = self._generate_deletion_story(
|
|
73
|
+
phase_summaries, repo_name
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
print(" - Full narrative...")
|
|
77
|
+
stories['full_narrative'] = self._generate_full_narrative(
|
|
78
|
+
phase_summaries, repo_name
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
return stories
|
|
82
|
+
|
|
83
|
+
def _prepare_phase_summaries(self, phases: List[Phase]) -> List[Dict[str, Any]]:
|
|
84
|
+
"""Prepare phase summaries for LLM prompts."""
|
|
85
|
+
summaries = []
|
|
86
|
+
|
|
87
|
+
for phase in phases:
|
|
88
|
+
summaries.append({
|
|
89
|
+
'phase_number': phase.phase_number,
|
|
90
|
+
'start_date': phase.start_date[:10],
|
|
91
|
+
'end_date': phase.end_date[:10],
|
|
92
|
+
'commit_count': phase.commit_count,
|
|
93
|
+
'loc_delta': phase.loc_delta,
|
|
94
|
+
'loc_delta_percent': phase.loc_delta_percent,
|
|
95
|
+
'total_insertions': phase.total_insertions,
|
|
96
|
+
'total_deletions': phase.total_deletions,
|
|
97
|
+
'authors': phase.authors,
|
|
98
|
+
'primary_author': phase.primary_author,
|
|
99
|
+
'has_large_deletion': phase.has_large_deletion,
|
|
100
|
+
'has_large_addition': phase.has_large_addition,
|
|
101
|
+
'has_refactor': phase.has_refactor,
|
|
102
|
+
'readme_changed': phase.readme_changed,
|
|
103
|
+
'summary': phase.summary,
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
return summaries
|
|
107
|
+
|
|
108
|
+
def _generate_executive_summary(self, phase_summaries: List[Dict[str, Any]],
|
|
109
|
+
repo_name: Optional[str] = None) -> str:
|
|
110
|
+
"""Generate high-level executive summary."""
|
|
111
|
+
prompt = self._build_executive_summary_prompt(phase_summaries, repo_name)
|
|
112
|
+
|
|
113
|
+
messages = [LLMMessage(role="user", content=prompt)]
|
|
114
|
+
response = self.router.generate(messages, max_tokens=1500)
|
|
115
|
+
|
|
116
|
+
return response.content.strip()
|
|
117
|
+
|
|
118
|
+
def _generate_timeline(self, phase_summaries: List[Dict[str, Any]],
|
|
119
|
+
repo_name: Optional[str] = None) -> str:
|
|
120
|
+
"""Generate chronological timeline."""
|
|
121
|
+
prompt = self._build_timeline_prompt(phase_summaries, repo_name)
|
|
122
|
+
|
|
123
|
+
messages = [LLMMessage(role="user", content=prompt)]
|
|
124
|
+
response = self.router.generate(messages, max_tokens=3000)
|
|
125
|
+
|
|
126
|
+
return response.content.strip()
|
|
127
|
+
|
|
128
|
+
def _generate_technical_evolution(self, phase_summaries: List[Dict[str, Any]],
|
|
129
|
+
repo_name: Optional[str] = None) -> str:
|
|
130
|
+
"""Generate technical architecture evolution story."""
|
|
131
|
+
prompt = self._build_technical_evolution_prompt(phase_summaries, repo_name)
|
|
132
|
+
|
|
133
|
+
messages = [LLMMessage(role="user", content=prompt)]
|
|
134
|
+
response = self.router.generate(messages, max_tokens=3000)
|
|
135
|
+
|
|
136
|
+
return response.content.strip()
|
|
137
|
+
|
|
138
|
+
def _generate_deletion_story(self, phase_summaries: List[Dict[str, Any]],
|
|
139
|
+
repo_name: Optional[str] = None) -> str:
|
|
140
|
+
"""Generate story of code deletions and cleanups."""
|
|
141
|
+
prompt = self._build_deletion_story_prompt(phase_summaries, repo_name)
|
|
142
|
+
|
|
143
|
+
messages = [LLMMessage(role="user", content=prompt)]
|
|
144
|
+
response = self.router.generate(messages, max_tokens=2000)
|
|
145
|
+
|
|
146
|
+
return response.content.strip()
|
|
147
|
+
|
|
148
|
+
def _generate_full_narrative(self, phase_summaries: List[Dict[str, Any]],
|
|
149
|
+
repo_name: Optional[str] = None) -> str:
|
|
150
|
+
"""Generate complete detailed narrative."""
|
|
151
|
+
prompt = self._build_full_narrative_prompt(phase_summaries, repo_name)
|
|
152
|
+
|
|
153
|
+
messages = [LLMMessage(role="user", content=prompt)]
|
|
154
|
+
response = self.router.generate(messages, max_tokens=4000)
|
|
155
|
+
|
|
156
|
+
return response.content.strip()
|
|
157
|
+
|
|
158
|
+
def _build_executive_summary_prompt(self, phase_summaries: List[Dict[str, Any]],
|
|
159
|
+
repo_name: Optional[str]) -> str:
|
|
160
|
+
"""Build prompt for executive summary."""
|
|
161
|
+
repo_title = repo_name or "this repository"
|
|
162
|
+
|
|
163
|
+
# Calculate totals
|
|
164
|
+
total_commits = sum(p['commit_count'] for p in phase_summaries)
|
|
165
|
+
total_insertions = sum(p['total_insertions'] for p in phase_summaries)
|
|
166
|
+
total_deletions = sum(p['total_deletions'] for p in phase_summaries)
|
|
167
|
+
all_authors = set()
|
|
168
|
+
for p in phase_summaries:
|
|
169
|
+
all_authors.update(p['authors'])
|
|
170
|
+
|
|
171
|
+
prompt = f"""You are writing an executive summary of the evolution of {repo_title}.
|
|
172
|
+
|
|
173
|
+
**Overall Statistics:**
|
|
174
|
+
- Total Phases: {len(phase_summaries)}
|
|
175
|
+
- Total Commits: {total_commits:,}
|
|
176
|
+
- Total Insertions: +{total_insertions:,} lines
|
|
177
|
+
- Total Deletions: -{total_deletions:,} lines
|
|
178
|
+
- Contributors: {len(all_authors)}
|
|
179
|
+
- Time Span: {phase_summaries[0]['start_date']} to {phase_summaries[-1]['end_date']}
|
|
180
|
+
|
|
181
|
+
**Phase Summaries:**
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
for p in phase_summaries:
|
|
185
|
+
prompt += f"\n**Phase {p['phase_number']} ({p['start_date']} to {p['end_date']})**\n"
|
|
186
|
+
prompt += f"- LOC Δ: {p['loc_delta']:+,d} ({p['loc_delta_percent']:+.1f}%)\n"
|
|
187
|
+
prompt += f"- Summary: {p['summary']}\n"
|
|
188
|
+
|
|
189
|
+
prompt += """
|
|
190
|
+
**Your Task:**
|
|
191
|
+
Write a concise executive summary (2-3 paragraphs) that:
|
|
192
|
+
1. Provides a high-level overview of the repository's evolution
|
|
193
|
+
2. Highlights the major milestones and transformations
|
|
194
|
+
3. Identifies key themes across the entire history
|
|
195
|
+
4. Summarizes the overall trajectory (growth, maturity, focus areas)
|
|
196
|
+
|
|
197
|
+
Keep it business-friendly and accessible to non-technical readers while maintaining technical accuracy.
|
|
198
|
+
|
|
199
|
+
Write the executive summary now:"""
|
|
200
|
+
|
|
201
|
+
return prompt
|
|
202
|
+
|
|
203
|
+
def _build_timeline_prompt(self, phase_summaries: List[Dict[str, Any]],
|
|
204
|
+
repo_name: Optional[str]) -> str:
|
|
205
|
+
"""Build prompt for timeline generation."""
|
|
206
|
+
repo_title = repo_name or "Repository"
|
|
207
|
+
|
|
208
|
+
prompt = f"""Create a chronological timeline of {repo_title}'s evolution with clear headings for each phase.
|
|
209
|
+
|
|
210
|
+
**Phase Summaries:**
|
|
211
|
+
"""
|
|
212
|
+
|
|
213
|
+
for p in phase_summaries:
|
|
214
|
+
prompt += f"\n**Phase {p['phase_number']} ({p['start_date']} to {p['end_date']})**\n"
|
|
215
|
+
prompt += f"- Commits: {p['commit_count']}\n"
|
|
216
|
+
prompt += f"- LOC Δ: {p['loc_delta']:+,d} ({p['loc_delta_percent']:+.1f}%)\n"
|
|
217
|
+
prompt += f"- Authors: {', '.join(p['authors'])}\n"
|
|
218
|
+
prompt += f"- Summary: {p['summary']}\n"
|
|
219
|
+
|
|
220
|
+
prompt += """
|
|
221
|
+
**Your Task:**
|
|
222
|
+
Create a timeline in markdown format with:
|
|
223
|
+
1. A descriptive heading for each phase (e.g., "Early Prototyping", "Major Refactoring", "Stabilization")
|
|
224
|
+
2. Date range
|
|
225
|
+
3. Key highlights in bullet points
|
|
226
|
+
4. Major changes and decisions
|
|
227
|
+
|
|
228
|
+
Format example:
|
|
229
|
+
## Phase 1: Early Prototyping (Jan - Mar 2018)
|
|
230
|
+
- Initial commit with basic structure
|
|
231
|
+
- Rapid experimentation with...
|
|
232
|
+
- ...
|
|
233
|
+
|
|
234
|
+
Write the timeline now:"""
|
|
235
|
+
|
|
236
|
+
return prompt
|
|
237
|
+
|
|
238
|
+
def _build_technical_evolution_prompt(self, phase_summaries: List[Dict[str, Any]],
|
|
239
|
+
repo_name: Optional[str]) -> str:
|
|
240
|
+
"""Build prompt for technical evolution."""
|
|
241
|
+
repo_title = repo_name or "this codebase"
|
|
242
|
+
|
|
243
|
+
prompt = f"""Analyze the technical and architectural evolution of {repo_title}.
|
|
244
|
+
|
|
245
|
+
**Phase Summaries:**
|
|
246
|
+
"""
|
|
247
|
+
|
|
248
|
+
for p in phase_summaries:
|
|
249
|
+
prompt += f"\n**Phase {p['phase_number']}** ({p['start_date']} to {p['end_date']})\n"
|
|
250
|
+
prompt += f"{p['summary']}\n"
|
|
251
|
+
|
|
252
|
+
prompt += """
|
|
253
|
+
**Your Task:**
|
|
254
|
+
Write a technical retrospective that:
|
|
255
|
+
1. Traces the architectural evolution across phases
|
|
256
|
+
2. Identifies major technical decisions and their motivations
|
|
257
|
+
3. Highlights refactorings and their impact
|
|
258
|
+
4. Discusses technology choices and migrations
|
|
259
|
+
5. Notes patterns in how the codebase matured
|
|
260
|
+
|
|
261
|
+
Write from a senior engineer's perspective, focusing on the technical journey.
|
|
262
|
+
|
|
263
|
+
Write the technical evolution now:"""
|
|
264
|
+
|
|
265
|
+
return prompt
|
|
266
|
+
|
|
267
|
+
def _build_deletion_story_prompt(self, phase_summaries: List[Dict[str, Any]],
|
|
268
|
+
repo_name: Optional[str]) -> str:
|
|
269
|
+
"""Build prompt for deletion story."""
|
|
270
|
+
# Find phases with significant deletions
|
|
271
|
+
deletion_phases = [p for p in phase_summaries if p['has_large_deletion']]
|
|
272
|
+
|
|
273
|
+
prompt = f"""Tell the story of what was removed from the codebase and why.
|
|
274
|
+
|
|
275
|
+
**All Phases:**
|
|
276
|
+
"""
|
|
277
|
+
|
|
278
|
+
for p in phase_summaries:
|
|
279
|
+
prompt += f"\nPhase {p['phase_number']} ({p['start_date']} to {p['end_date']})\n"
|
|
280
|
+
prompt += f"- Deletions: -{p['total_deletions']:,} lines\n"
|
|
281
|
+
prompt += f"- Large Deletion: {p['has_large_deletion']}\n"
|
|
282
|
+
prompt += f"- Summary: {p['summary']}\n"
|
|
283
|
+
|
|
284
|
+
prompt += """
|
|
285
|
+
**Your Task:**
|
|
286
|
+
Write a narrative about the deletion and cleanup efforts:
|
|
287
|
+
1. What major components were removed?
|
|
288
|
+
2. Why were they removed? (deprecated, replaced, experimental, etc.)
|
|
289
|
+
3. How did these deletions improve the codebase?
|
|
290
|
+
4. What does this tell us about the project's evolution?
|
|
291
|
+
|
|
292
|
+
Focus on the story of simplification, refactoring, and evolution through removal.
|
|
293
|
+
|
|
294
|
+
Write the deletion story now:"""
|
|
295
|
+
|
|
296
|
+
return prompt
|
|
297
|
+
|
|
298
|
+
def _build_full_narrative_prompt(self, phase_summaries: List[Dict[str, Any]],
|
|
299
|
+
repo_name: Optional[str]) -> str:
|
|
300
|
+
"""Build prompt for full narrative."""
|
|
301
|
+
repo_title = repo_name or "this repository"
|
|
302
|
+
|
|
303
|
+
prompt = f"""Write a comprehensive narrative of {repo_title}'s evolution.
|
|
304
|
+
|
|
305
|
+
**Phase Summaries:**
|
|
306
|
+
"""
|
|
307
|
+
|
|
308
|
+
for p in phase_summaries:
|
|
309
|
+
prompt += f"\n**Phase {p['phase_number']} ({p['start_date']} to {p['end_date']})**\n"
|
|
310
|
+
prompt += f"- Commits: {p['commit_count']}, LOC Δ: {p['loc_delta']:+,d}\n"
|
|
311
|
+
prompt += f"- Authors: {', '.join(p['authors'])}\n"
|
|
312
|
+
prompt += f"{p['summary']}\n"
|
|
313
|
+
|
|
314
|
+
prompt += """
|
|
315
|
+
**Your Task:**
|
|
316
|
+
Write a complete, detailed narrative (multiple paragraphs) that:
|
|
317
|
+
1. Tells the full story of the repository from beginning to end
|
|
318
|
+
2. Weaves together all phases into a coherent narrative
|
|
319
|
+
3. Highlights the evolution, challenges, and successes
|
|
320
|
+
4. Maintains chronological flow while identifying themes
|
|
321
|
+
5. Makes connections between phases
|
|
322
|
+
6. Provides both technical depth and big-picture perspective
|
|
323
|
+
|
|
324
|
+
This should read like a well-crafted story with a beginning, middle, and current state.
|
|
325
|
+
|
|
326
|
+
Write the full narrative now:"""
|
|
327
|
+
|
|
328
|
+
return prompt
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def generate_story(phases: List[Phase],
|
|
332
|
+
repo_name: Optional[str] = None,
|
|
333
|
+
backend: Optional[str] = None,
|
|
334
|
+
model: Optional[str] = None,
|
|
335
|
+
api_key: Optional[str] = None,
|
|
336
|
+
**kwargs) -> Dict[str, str]:
|
|
337
|
+
"""
|
|
338
|
+
Generate global repository story.
|
|
339
|
+
|
|
340
|
+
Args:
|
|
341
|
+
phases: List of Phase objects with summaries
|
|
342
|
+
repo_name: Optional repository name
|
|
343
|
+
backend: LLM backend ('anthropic', 'openai', 'ollama')
|
|
344
|
+
model: Model identifier (uses backend defaults if not specified)
|
|
345
|
+
api_key: API key for the backend (if required)
|
|
346
|
+
**kwargs: Additional backend parameters
|
|
347
|
+
|
|
348
|
+
Returns:
|
|
349
|
+
Dict with story sections
|
|
350
|
+
"""
|
|
351
|
+
storyteller = StoryTeller(backend=backend, model=model, api_key=api_key, **kwargs)
|
|
352
|
+
return storyteller.generate_global_story(phases, repo_name)
|
gitview/summarizer.py
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"""LLM-based phase summarization."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from typing import List, Dict, Any, Optional
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from .chunker import Phase
|
|
9
|
+
from .backends import LLMRouter, LLMMessage
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PhaseSummarizer:
|
|
13
|
+
"""Summarize git history phases using LLM."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, backend: Optional[str] = None, model: Optional[str] = None,
|
|
16
|
+
api_key: Optional[str] = None, **kwargs):
|
|
17
|
+
"""
|
|
18
|
+
Initialize summarizer with LLM backend.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
backend: LLM backend ('anthropic', 'openai', 'ollama')
|
|
22
|
+
model: Model identifier (uses backend defaults if not specified)
|
|
23
|
+
api_key: API key for the backend (if required)
|
|
24
|
+
**kwargs: Additional backend parameters
|
|
25
|
+
"""
|
|
26
|
+
self.router = LLMRouter(backend=backend, model=model, api_key=api_key, **kwargs)
|
|
27
|
+
self.model = self.router.model
|
|
28
|
+
|
|
29
|
+
def summarize_phase(self, phase: Phase, context: Optional[str] = None) -> str:
|
|
30
|
+
"""
|
|
31
|
+
Generate a narrative summary for a single phase.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
phase: Phase object to summarize
|
|
35
|
+
context: Optional context from previous phases
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Narrative summary string
|
|
39
|
+
"""
|
|
40
|
+
# Prepare phase data for LLM
|
|
41
|
+
phase_data = self._prepare_phase_data(phase)
|
|
42
|
+
|
|
43
|
+
# Build prompt
|
|
44
|
+
prompt = self._build_phase_prompt(phase_data, context)
|
|
45
|
+
|
|
46
|
+
# Call LLM backend
|
|
47
|
+
messages = [LLMMessage(role="user", content=prompt)]
|
|
48
|
+
response = self.router.generate(messages, max_tokens=2000)
|
|
49
|
+
|
|
50
|
+
return response.content.strip()
|
|
51
|
+
|
|
52
|
+
def summarize_all_phases(self, phases: List[Phase],
|
|
53
|
+
output_dir: Optional[str] = None) -> List[Phase]:
|
|
54
|
+
"""
|
|
55
|
+
Summarize all phases with context from previous phases.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
phases: List of Phase objects
|
|
59
|
+
output_dir: Optional directory to save updated phases
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
List of Phase objects with summaries filled in
|
|
63
|
+
"""
|
|
64
|
+
previous_summaries = []
|
|
65
|
+
|
|
66
|
+
for i, phase in enumerate(phases):
|
|
67
|
+
print(f"Summarizing phase {phase.phase_number}/{len(phases)}...")
|
|
68
|
+
|
|
69
|
+
# Build context from previous phases
|
|
70
|
+
context = self._build_context(previous_summaries)
|
|
71
|
+
|
|
72
|
+
# Generate summary
|
|
73
|
+
summary = self.summarize_phase(phase, context)
|
|
74
|
+
phase.summary = summary
|
|
75
|
+
|
|
76
|
+
# Store for next iteration
|
|
77
|
+
previous_summaries.append({
|
|
78
|
+
'phase_number': phase.phase_number,
|
|
79
|
+
'summary': summary,
|
|
80
|
+
'loc_delta': phase.loc_delta,
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
# Save updated phase if output_dir provided
|
|
84
|
+
if output_dir:
|
|
85
|
+
self._save_phase_with_summary(phase, output_dir)
|
|
86
|
+
|
|
87
|
+
return phases
|
|
88
|
+
|
|
89
|
+
def _prepare_phase_data(self, phase: Phase) -> Dict[str, Any]:
|
|
90
|
+
"""Prepare phase data for LLM prompt."""
|
|
91
|
+
# Get commit details
|
|
92
|
+
commits_summary = []
|
|
93
|
+
for commit in phase.commits[:20]: # Limit to first 20 commits
|
|
94
|
+
commits_summary.append({
|
|
95
|
+
'hash': commit.short_hash,
|
|
96
|
+
'date': commit.timestamp[:10], # Just the date
|
|
97
|
+
'author': commit.author,
|
|
98
|
+
'message': commit.commit_subject,
|
|
99
|
+
'insertions': commit.insertions,
|
|
100
|
+
'deletions': commit.deletions,
|
|
101
|
+
'files_changed': commit.files_changed,
|
|
102
|
+
'is_refactor': commit.is_refactor,
|
|
103
|
+
'is_large_deletion': commit.is_large_deletion,
|
|
104
|
+
'is_large_addition': commit.is_large_addition,
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
# Get significant commits (large changes, refactors)
|
|
108
|
+
significant_commits = []
|
|
109
|
+
for commit in phase.commits:
|
|
110
|
+
if commit.is_large_deletion or commit.is_large_addition or commit.is_refactor:
|
|
111
|
+
significant_commits.append({
|
|
112
|
+
'hash': commit.short_hash,
|
|
113
|
+
'message': commit.commit_message,
|
|
114
|
+
'insertions': commit.insertions,
|
|
115
|
+
'deletions': commit.deletions,
|
|
116
|
+
'is_refactor': commit.is_refactor,
|
|
117
|
+
'is_large_deletion': commit.is_large_deletion,
|
|
118
|
+
'is_large_addition': commit.is_large_addition,
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
# Get README changes
|
|
122
|
+
readme_changes = []
|
|
123
|
+
for i, commit in enumerate(phase.commits):
|
|
124
|
+
if commit.readme_exists and commit.readme_excerpt:
|
|
125
|
+
if i == 0 or i == len(phase.commits) - 1:
|
|
126
|
+
readme_changes.append({
|
|
127
|
+
'hash': commit.short_hash,
|
|
128
|
+
'excerpt': commit.readme_excerpt,
|
|
129
|
+
'position': 'start' if i == 0 else 'end'
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
# Get comment samples
|
|
133
|
+
comment_samples = []
|
|
134
|
+
for commit in phase.commits:
|
|
135
|
+
if commit.comment_samples:
|
|
136
|
+
comment_samples.extend(commit.comment_samples[:2])
|
|
137
|
+
comment_samples = comment_samples[:5] # Limit total
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
'phase_number': phase.phase_number,
|
|
141
|
+
'start_date': phase.start_date[:10],
|
|
142
|
+
'end_date': phase.end_date[:10],
|
|
143
|
+
'commit_count': phase.commit_count,
|
|
144
|
+
'loc_start': phase.loc_start,
|
|
145
|
+
'loc_end': phase.loc_end,
|
|
146
|
+
'loc_delta': phase.loc_delta,
|
|
147
|
+
'loc_delta_percent': phase.loc_delta_percent,
|
|
148
|
+
'total_insertions': phase.total_insertions,
|
|
149
|
+
'total_deletions': phase.total_deletions,
|
|
150
|
+
'languages_start': phase.languages_start,
|
|
151
|
+
'languages_end': phase.languages_end,
|
|
152
|
+
'authors': phase.authors,
|
|
153
|
+
'primary_author': phase.primary_author,
|
|
154
|
+
'has_large_deletion': phase.has_large_deletion,
|
|
155
|
+
'has_large_addition': phase.has_large_addition,
|
|
156
|
+
'has_refactor': phase.has_refactor,
|
|
157
|
+
'readme_changed': phase.readme_changed,
|
|
158
|
+
'commits': commits_summary,
|
|
159
|
+
'significant_commits': significant_commits,
|
|
160
|
+
'readme_changes': readme_changes,
|
|
161
|
+
'comment_samples': comment_samples,
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
def _build_phase_prompt(self, phase_data: Dict[str, Any],
|
|
165
|
+
context: Optional[str] = None) -> str:
|
|
166
|
+
"""Build prompt for phase summarization."""
|
|
167
|
+
prompt = f"""You are analyzing a phase in a git repository's history. Your task is to write a concise narrative summary of what happened during this phase.
|
|
168
|
+
|
|
169
|
+
**Phase Overview:**
|
|
170
|
+
- Phase Number: {phase_data['phase_number']}
|
|
171
|
+
- Time Period: {phase_data['start_date']} to {phase_data['end_date']}
|
|
172
|
+
- Commits: {phase_data['commit_count']}
|
|
173
|
+
- LOC Change: {phase_data['loc_delta']:+,d} ({phase_data['loc_delta_percent']:+.1f}%)
|
|
174
|
+
- Start: {phase_data['loc_start']:,} LOC
|
|
175
|
+
- End: {phase_data['loc_end']:,} LOC
|
|
176
|
+
- Total Changes: +{phase_data['total_insertions']:,} / -{phase_data['total_deletions']:,} lines
|
|
177
|
+
- Authors: {', '.join(phase_data['authors'])}
|
|
178
|
+
- Primary Author: {phase_data['primary_author']}
|
|
179
|
+
|
|
180
|
+
**Language Breakdown:**
|
|
181
|
+
- Start: {phase_data['languages_start']}
|
|
182
|
+
- End: {phase_data['languages_end']}
|
|
183
|
+
|
|
184
|
+
**Major Events:**
|
|
185
|
+
- Large Deletion: {phase_data['has_large_deletion']}
|
|
186
|
+
- Large Addition: {phase_data['has_large_addition']}
|
|
187
|
+
- Refactoring: {phase_data['has_refactor']}
|
|
188
|
+
- README Changed: {phase_data['readme_changed']}
|
|
189
|
+
|
|
190
|
+
**Commits Summary:**
|
|
191
|
+
{json.dumps(phase_data['commits'], indent=2)}
|
|
192
|
+
|
|
193
|
+
**Significant Commits (Large Changes/Refactors):**
|
|
194
|
+
{json.dumps(phase_data['significant_commits'], indent=2)}
|
|
195
|
+
|
|
196
|
+
**README Changes:**
|
|
197
|
+
{json.dumps(phase_data['readme_changes'], indent=2)}
|
|
198
|
+
|
|
199
|
+
**Comment Samples:**
|
|
200
|
+
{json.dumps(phase_data['comment_samples'], indent=2)}
|
|
201
|
+
"""
|
|
202
|
+
|
|
203
|
+
if context:
|
|
204
|
+
prompt += f"\n**Context from Previous Phases:**\n{context}\n"
|
|
205
|
+
|
|
206
|
+
prompt += """
|
|
207
|
+
**Your Task:**
|
|
208
|
+
Write a concise narrative summary (3-5 paragraphs) that:
|
|
209
|
+
|
|
210
|
+
1. Describes the main activities during this phase
|
|
211
|
+
2. Explains major code additions, deletions, migrations, and cleanups
|
|
212
|
+
3. Highlights how the README or documentation evolved
|
|
213
|
+
4. Identifies themes from commit messages and comments (TODOs, deprecations, commentary)
|
|
214
|
+
5. Explains the intent behind large diffs or refactorings
|
|
215
|
+
6. Notes any significant architectural or technical decisions
|
|
216
|
+
7. Maintains chronological flow while being concise
|
|
217
|
+
|
|
218
|
+
Focus on the "why" and "what changed" rather than just listing commits. Make it read like a story of the codebase's evolution.
|
|
219
|
+
|
|
220
|
+
Write the summary now:"""
|
|
221
|
+
|
|
222
|
+
return prompt
|
|
223
|
+
|
|
224
|
+
def _build_context(self, previous_summaries: List[Dict[str, Any]]) -> str:
|
|
225
|
+
"""Build context string from previous phase summaries."""
|
|
226
|
+
if not previous_summaries:
|
|
227
|
+
return ""
|
|
228
|
+
|
|
229
|
+
context_parts = []
|
|
230
|
+
for summary_info in previous_summaries[-3:]: # Last 3 phases
|
|
231
|
+
context_parts.append(
|
|
232
|
+
f"Phase {summary_info['phase_number']} "
|
|
233
|
+
f"(LOC Δ: {summary_info['loc_delta']:+,d}): "
|
|
234
|
+
f"{summary_info['summary'][:200]}..."
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
return "\n\n".join(context_parts)
|
|
238
|
+
|
|
239
|
+
def _save_phase_with_summary(self, phase: Phase, output_dir: str):
|
|
240
|
+
"""Save phase with updated summary."""
|
|
241
|
+
output_path = Path(output_dir)
|
|
242
|
+
output_path.mkdir(parents=True, exist_ok=True)
|
|
243
|
+
|
|
244
|
+
phase_file = output_path / f"phase_{phase.phase_number:02d}.json"
|
|
245
|
+
with open(phase_file, 'w') as f:
|
|
246
|
+
json.dump(phase.to_dict(), f, indent=2)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def summarize_phases(phases: List[Phase],
|
|
250
|
+
output_dir: str = "output/phases",
|
|
251
|
+
backend: Optional[str] = None,
|
|
252
|
+
model: Optional[str] = None,
|
|
253
|
+
api_key: Optional[str] = None,
|
|
254
|
+
**kwargs) -> List[Phase]:
|
|
255
|
+
"""
|
|
256
|
+
Summarize all phases using LLM backend.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
phases: List of Phase objects
|
|
260
|
+
output_dir: Directory to save updated phases
|
|
261
|
+
backend: LLM backend ('anthropic', 'openai', 'ollama')
|
|
262
|
+
model: Model identifier (uses backend defaults if not specified)
|
|
263
|
+
api_key: API key for the backend (if required)
|
|
264
|
+
**kwargs: Additional backend parameters
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
List of Phase objects with summaries
|
|
268
|
+
"""
|
|
269
|
+
summarizer = PhaseSummarizer(backend=backend, model=model, api_key=api_key, **kwargs)
|
|
270
|
+
return summarizer.summarize_all_phases(phases, output_dir)
|