gitflow-analytics 1.0.1__py3-none-any.whl → 1.3.6__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 (119) hide show
  1. gitflow_analytics/__init__.py +11 -11
  2. gitflow_analytics/_version.py +2 -2
  3. gitflow_analytics/classification/__init__.py +31 -0
  4. gitflow_analytics/classification/batch_classifier.py +752 -0
  5. gitflow_analytics/classification/classifier.py +464 -0
  6. gitflow_analytics/classification/feature_extractor.py +725 -0
  7. gitflow_analytics/classification/linguist_analyzer.py +574 -0
  8. gitflow_analytics/classification/model.py +455 -0
  9. gitflow_analytics/cli.py +4490 -378
  10. gitflow_analytics/cli_rich.py +503 -0
  11. gitflow_analytics/config/__init__.py +43 -0
  12. gitflow_analytics/config/errors.py +261 -0
  13. gitflow_analytics/config/loader.py +904 -0
  14. gitflow_analytics/config/profiles.py +264 -0
  15. gitflow_analytics/config/repository.py +124 -0
  16. gitflow_analytics/config/schema.py +441 -0
  17. gitflow_analytics/config/validator.py +154 -0
  18. gitflow_analytics/config.py +44 -398
  19. gitflow_analytics/core/analyzer.py +1320 -172
  20. gitflow_analytics/core/branch_mapper.py +132 -132
  21. gitflow_analytics/core/cache.py +1554 -175
  22. gitflow_analytics/core/data_fetcher.py +1193 -0
  23. gitflow_analytics/core/identity.py +571 -185
  24. gitflow_analytics/core/metrics_storage.py +526 -0
  25. gitflow_analytics/core/progress.py +372 -0
  26. gitflow_analytics/core/schema_version.py +269 -0
  27. gitflow_analytics/extractors/base.py +13 -11
  28. gitflow_analytics/extractors/ml_tickets.py +1100 -0
  29. gitflow_analytics/extractors/story_points.py +77 -59
  30. gitflow_analytics/extractors/tickets.py +841 -89
  31. gitflow_analytics/identity_llm/__init__.py +6 -0
  32. gitflow_analytics/identity_llm/analysis_pass.py +231 -0
  33. gitflow_analytics/identity_llm/analyzer.py +464 -0
  34. gitflow_analytics/identity_llm/models.py +76 -0
  35. gitflow_analytics/integrations/github_integration.py +258 -87
  36. gitflow_analytics/integrations/jira_integration.py +572 -123
  37. gitflow_analytics/integrations/orchestrator.py +206 -82
  38. gitflow_analytics/metrics/activity_scoring.py +322 -0
  39. gitflow_analytics/metrics/branch_health.py +470 -0
  40. gitflow_analytics/metrics/dora.py +542 -179
  41. gitflow_analytics/models/database.py +986 -59
  42. gitflow_analytics/pm_framework/__init__.py +115 -0
  43. gitflow_analytics/pm_framework/adapters/__init__.py +50 -0
  44. gitflow_analytics/pm_framework/adapters/jira_adapter.py +1845 -0
  45. gitflow_analytics/pm_framework/base.py +406 -0
  46. gitflow_analytics/pm_framework/models.py +211 -0
  47. gitflow_analytics/pm_framework/orchestrator.py +652 -0
  48. gitflow_analytics/pm_framework/registry.py +333 -0
  49. gitflow_analytics/qualitative/__init__.py +29 -0
  50. gitflow_analytics/qualitative/chatgpt_analyzer.py +259 -0
  51. gitflow_analytics/qualitative/classifiers/__init__.py +13 -0
  52. gitflow_analytics/qualitative/classifiers/change_type.py +742 -0
  53. gitflow_analytics/qualitative/classifiers/domain_classifier.py +506 -0
  54. gitflow_analytics/qualitative/classifiers/intent_analyzer.py +535 -0
  55. gitflow_analytics/qualitative/classifiers/llm/__init__.py +35 -0
  56. gitflow_analytics/qualitative/classifiers/llm/base.py +193 -0
  57. gitflow_analytics/qualitative/classifiers/llm/batch_processor.py +383 -0
  58. gitflow_analytics/qualitative/classifiers/llm/cache.py +479 -0
  59. gitflow_analytics/qualitative/classifiers/llm/cost_tracker.py +435 -0
  60. gitflow_analytics/qualitative/classifiers/llm/openai_client.py +403 -0
  61. gitflow_analytics/qualitative/classifiers/llm/prompts.py +373 -0
  62. gitflow_analytics/qualitative/classifiers/llm/response_parser.py +287 -0
  63. gitflow_analytics/qualitative/classifiers/llm_commit_classifier.py +607 -0
  64. gitflow_analytics/qualitative/classifiers/risk_analyzer.py +438 -0
  65. gitflow_analytics/qualitative/core/__init__.py +13 -0
  66. gitflow_analytics/qualitative/core/llm_fallback.py +657 -0
  67. gitflow_analytics/qualitative/core/nlp_engine.py +382 -0
  68. gitflow_analytics/qualitative/core/pattern_cache.py +479 -0
  69. gitflow_analytics/qualitative/core/processor.py +673 -0
  70. gitflow_analytics/qualitative/enhanced_analyzer.py +2236 -0
  71. gitflow_analytics/qualitative/example_enhanced_usage.py +420 -0
  72. gitflow_analytics/qualitative/models/__init__.py +25 -0
  73. gitflow_analytics/qualitative/models/schemas.py +306 -0
  74. gitflow_analytics/qualitative/utils/__init__.py +13 -0
  75. gitflow_analytics/qualitative/utils/batch_processor.py +339 -0
  76. gitflow_analytics/qualitative/utils/cost_tracker.py +345 -0
  77. gitflow_analytics/qualitative/utils/metrics.py +361 -0
  78. gitflow_analytics/qualitative/utils/text_processing.py +285 -0
  79. gitflow_analytics/reports/__init__.py +100 -0
  80. gitflow_analytics/reports/analytics_writer.py +550 -18
  81. gitflow_analytics/reports/base.py +648 -0
  82. gitflow_analytics/reports/branch_health_writer.py +322 -0
  83. gitflow_analytics/reports/classification_writer.py +924 -0
  84. gitflow_analytics/reports/cli_integration.py +427 -0
  85. gitflow_analytics/reports/csv_writer.py +1700 -216
  86. gitflow_analytics/reports/data_models.py +504 -0
  87. gitflow_analytics/reports/database_report_generator.py +427 -0
  88. gitflow_analytics/reports/example_usage.py +344 -0
  89. gitflow_analytics/reports/factory.py +499 -0
  90. gitflow_analytics/reports/formatters.py +698 -0
  91. gitflow_analytics/reports/html_generator.py +1116 -0
  92. gitflow_analytics/reports/interfaces.py +489 -0
  93. gitflow_analytics/reports/json_exporter.py +2770 -0
  94. gitflow_analytics/reports/narrative_writer.py +2289 -158
  95. gitflow_analytics/reports/story_point_correlation.py +1144 -0
  96. gitflow_analytics/reports/weekly_trends_writer.py +389 -0
  97. gitflow_analytics/training/__init__.py +5 -0
  98. gitflow_analytics/training/model_loader.py +377 -0
  99. gitflow_analytics/training/pipeline.py +550 -0
  100. gitflow_analytics/tui/__init__.py +5 -0
  101. gitflow_analytics/tui/app.py +724 -0
  102. gitflow_analytics/tui/screens/__init__.py +8 -0
  103. gitflow_analytics/tui/screens/analysis_progress_screen.py +496 -0
  104. gitflow_analytics/tui/screens/configuration_screen.py +523 -0
  105. gitflow_analytics/tui/screens/loading_screen.py +348 -0
  106. gitflow_analytics/tui/screens/main_screen.py +321 -0
  107. gitflow_analytics/tui/screens/results_screen.py +722 -0
  108. gitflow_analytics/tui/widgets/__init__.py +7 -0
  109. gitflow_analytics/tui/widgets/data_table.py +255 -0
  110. gitflow_analytics/tui/widgets/export_modal.py +301 -0
  111. gitflow_analytics/tui/widgets/progress_widget.py +187 -0
  112. gitflow_analytics-1.3.6.dist-info/METADATA +1015 -0
  113. gitflow_analytics-1.3.6.dist-info/RECORD +122 -0
  114. gitflow_analytics-1.0.1.dist-info/METADATA +0 -463
  115. gitflow_analytics-1.0.1.dist-info/RECORD +0 -31
  116. {gitflow_analytics-1.0.1.dist-info → gitflow_analytics-1.3.6.dist-info}/WHEEL +0 -0
  117. {gitflow_analytics-1.0.1.dist-info → gitflow_analytics-1.3.6.dist-info}/entry_points.txt +0 -0
  118. {gitflow_analytics-1.0.1.dist-info → gitflow_analytics-1.3.6.dist-info}/licenses/LICENSE +0 -0
  119. {gitflow_analytics-1.0.1.dist-info → gitflow_analytics-1.3.6.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,722 @@
1
+ """Results screen for GitFlow Analytics TUI."""
2
+
3
+ import json
4
+ from datetime import datetime
5
+ from pathlib import Path
6
+ from typing import Any, Optional
7
+
8
+ from rich.table import Table
9
+ from textual.binding import Binding
10
+ from textual.containers import Container, Horizontal, ScrollableContainer, Vertical
11
+ from textual.screen import Screen
12
+ from textual.widgets import Button, Footer, Header, Label, Rule, Static, TabbedContent, TabPane
13
+
14
+ from gitflow_analytics.config import Config
15
+
16
+ from ..widgets.data_table import EnhancedDataTable
17
+ from ..widgets.export_modal import ExportModal
18
+
19
+
20
+ class ResultsScreen(Screen):
21
+ """
22
+ Screen displaying comprehensive analysis results with interactive exploration.
23
+
24
+ WHY: Analysis results are complex and multi-dimensional, requiring an
25
+ interactive interface that allows users to explore different aspects of
26
+ the data. Tabbed layout organizes information logically while providing
27
+ powerful data exploration capabilities.
28
+
29
+ DESIGN DECISION: Uses tabbed interface to separate different result categories
30
+ while providing consistent export functionality across all views. Interactive
31
+ tables allow users to sort, filter, and drill down into specific data points.
32
+ """
33
+
34
+ BINDINGS = [
35
+ Binding("escape", "back", "Back to Main"),
36
+ Binding("ctrl+s", "export", "Export Results"),
37
+ Binding("ctrl+f", "filter", "Filter Data"),
38
+ Binding("r", "refresh", "Refresh View"),
39
+ Binding("ctrl+e", "export_current", "Export Current View"),
40
+ ]
41
+
42
+ def __init__(
43
+ self,
44
+ commits: list[dict],
45
+ prs: list[dict],
46
+ developers: list[dict],
47
+ config: Config,
48
+ *,
49
+ name: Optional[str] = None,
50
+ id: Optional[str] = None,
51
+ ) -> None:
52
+ super().__init__(name=name, id=id)
53
+ self.commits = commits
54
+ self.prs = prs
55
+ self.developers = developers
56
+ self.config = config
57
+ self.current_tab = "summary"
58
+
59
+ def compose(self):
60
+ """Compose the results screen."""
61
+ yield Header()
62
+
63
+ with Container(id="results-container"):
64
+ yield Label("GitFlow Analytics - Results", classes="screen-title")
65
+
66
+ with TabbedContent(initial="summary"):
67
+ # Summary Tab
68
+ with TabPane("Summary", id="summary"):
69
+ yield self._create_summary_panel()
70
+
71
+ # Developers Tab
72
+ with TabPane("Developers", id="developers"):
73
+ yield self._create_developers_panel()
74
+
75
+ # Commits Tab
76
+ with TabPane("Commits", id="commits"):
77
+ yield self._create_commits_panel()
78
+
79
+ # Pull Requests Tab (if available)
80
+ if self.prs:
81
+ with TabPane("Pull Requests", id="pull-requests"):
82
+ yield self._create_prs_panel()
83
+
84
+ # Qualitative Insights Tab (if available)
85
+ if self._has_qualitative_data():
86
+ with TabPane("Qualitative Insights", id="qualitative"):
87
+ yield self._create_qualitative_panel()
88
+
89
+ # Export Tab
90
+ with TabPane("Export", id="export"):
91
+ yield self._create_export_panel()
92
+
93
+ yield Footer()
94
+
95
+ def _create_summary_panel(self) -> ScrollableContainer:
96
+ """
97
+ Create comprehensive summary statistics panel.
98
+
99
+ WHY: Provides high-level overview of all analysis results in a single view,
100
+ allowing users to quickly understand the overall scope and key metrics
101
+ without diving into detailed data tables.
102
+ """
103
+ container = ScrollableContainer()
104
+
105
+ # Key metrics section
106
+ container.mount(Label("Analysis Summary", classes="section-title"))
107
+
108
+ # Create summary table
109
+ summary_table = Table(show_header=False, show_edge=False, pad_edge=False)
110
+ summary_table.add_column("Metric", style="bold cyan", width=25)
111
+ summary_table.add_column("Value", style="green", width=15)
112
+ summary_table.add_column("Details", style="dim", width=40)
113
+
114
+ # Calculate key metrics
115
+ total_commits = len(self.commits)
116
+ total_prs = len(self.prs)
117
+ total_developers = len(self.developers)
118
+
119
+ # Time range
120
+ if self.commits:
121
+ dates = [c.get("timestamp") for c in self.commits if c.get("timestamp")]
122
+ if dates:
123
+ min_date = min(dates).strftime("%Y-%m-%d")
124
+ max_date = max(dates).strftime("%Y-%m-%d")
125
+ date_range = f"{min_date} to {max_date}"
126
+ else:
127
+ date_range = "Unknown"
128
+ else:
129
+ date_range = "No data"
130
+
131
+ # Story points
132
+ total_story_points = sum(c.get("story_points", 0) or 0 for c in self.commits)
133
+
134
+ # Ticket coverage
135
+ commits_with_tickets = sum(1 for c in self.commits if c.get("ticket_references"))
136
+ ticket_coverage = (commits_with_tickets / total_commits * 100) if total_commits > 0 else 0
137
+
138
+ # Add metrics to table
139
+ summary_table.add_row(
140
+ "Total Commits", f"{total_commits:,}", "All commits in analysis period"
141
+ )
142
+ summary_table.add_row("Total Pull Requests", f"{total_prs:,}", "Detected pull requests")
143
+ summary_table.add_row(
144
+ "Active Developers", f"{total_developers:,}", "Unique developer identities"
145
+ )
146
+ summary_table.add_row("Analysis Period", date_range, "Date range of analyzed commits")
147
+ summary_table.add_row(
148
+ "Story Points", f"{total_story_points:,}", "Total story points completed"
149
+ )
150
+ summary_table.add_row(
151
+ "Ticket Coverage", f"{ticket_coverage:.1f}%", "Commits with ticket references"
152
+ )
153
+
154
+ from rich.console import Console
155
+ from rich.panel import Panel
156
+
157
+ Console()
158
+
159
+ container.mount(Static(Panel(summary_table, title="Key Metrics", border_style="blue")))
160
+
161
+ # Top contributors section
162
+ container.mount(Rule())
163
+ container.mount(Label("Top Contributors", classes="section-title"))
164
+
165
+ if self.developers:
166
+ top_devs = sorted(
167
+ self.developers, key=lambda d: d.get("total_commits", 0), reverse=True
168
+ )[:10]
169
+
170
+ contrib_table = Table(show_header=True, header_style="bold magenta")
171
+ contrib_table.add_column("Developer", width=25)
172
+ contrib_table.add_column("Commits", justify="right", width=10)
173
+ contrib_table.add_column("Story Points", justify="right", width=12)
174
+ contrib_table.add_column("Avg Points/Commit", justify="right", width=15)
175
+
176
+ for dev in top_devs:
177
+ commits = dev.get("total_commits", 0)
178
+ points = dev.get("total_story_points", 0)
179
+ avg_points = points / commits if commits > 0 else 0
180
+
181
+ contrib_table.add_row(
182
+ dev.get("primary_name", "Unknown")[:23],
183
+ f"{commits:,}",
184
+ f"{points:,}",
185
+ f"{avg_points:.1f}",
186
+ )
187
+
188
+ container.mount(
189
+ Static(Panel(contrib_table, title="Developer Activity", border_style="green"))
190
+ )
191
+
192
+ # Qualitative insights summary (if available)
193
+ if self._has_qualitative_data():
194
+ container.mount(Rule())
195
+ container.mount(Label("Qualitative Analysis Summary", classes="section-title"))
196
+ container.mount(Static(self._create_qualitative_summary()))
197
+
198
+ return container
199
+
200
+ def _create_developers_panel(self) -> Container:
201
+ """Create interactive developers data panel."""
202
+ container = Container()
203
+
204
+ container.mount(Label("Developer Statistics", classes="section-title"))
205
+ container.mount(
206
+ Static(
207
+ f"Showing {len(self.developers)} unique developers. Click column headers to sort.",
208
+ classes="help-text",
209
+ )
210
+ )
211
+
212
+ # Create enhanced data table
213
+ developers_table = EnhancedDataTable(data=self.developers, id="developers-table")
214
+
215
+ container.mount(developers_table)
216
+
217
+ # Action buttons
218
+ with container.mount(Horizontal(classes="action-bar")):
219
+ yield Button("Export Developers", id="export-developers")
220
+ yield Button("Show Identity Details", id="show-identities")
221
+
222
+ return container
223
+
224
+ def _create_commits_panel(self) -> Container:
225
+ """Create interactive commits data panel."""
226
+ container = Container()
227
+
228
+ container.mount(Label("Commit Analysis", classes="section-title"))
229
+ container.mount(
230
+ Static(
231
+ f"Showing {len(self.commits)} commits. Use filters to explore specific data.",
232
+ classes="help-text",
233
+ )
234
+ )
235
+
236
+ # Prepare commits data for table display
237
+ commits_data = []
238
+ for commit in self.commits[:1000]: # Limit to 1000 for performance
239
+ commit_row = {
240
+ "date": (
241
+ commit.get("timestamp", "").strftime("%Y-%m-%d")
242
+ if commit.get("timestamp")
243
+ else ""
244
+ ),
245
+ "author": commit.get("author_name", ""),
246
+ "message": (
247
+ commit.get("message", "")[:80] + "..."
248
+ if len(commit.get("message", "")) > 80
249
+ else commit.get("message", "")
250
+ ),
251
+ "files_changed": commit.get("files_changed_count", 0),
252
+ "insertions": commit.get("insertions", 0),
253
+ "deletions": commit.get("deletions", 0),
254
+ "story_points": commit.get("story_points", 0),
255
+ "project_key": commit.get("project_key", ""),
256
+ "change_type": commit.get("change_type", "unknown"),
257
+ "risk_level": commit.get("risk_level", "unknown"),
258
+ }
259
+ commits_data.append(commit_row)
260
+
261
+ commits_table = EnhancedDataTable(data=commits_data, id="commits-table")
262
+
263
+ container.mount(commits_table)
264
+
265
+ # Action buttons
266
+ with container.mount(Horizontal(classes="action-bar")):
267
+ yield Button("Export Commits", id="export-commits")
268
+ yield Button("Filter by Author", id="filter-author")
269
+ yield Button("Filter by Project", id="filter-project")
270
+
271
+ return container
272
+
273
+ def _create_prs_panel(self) -> Container:
274
+ """Create pull requests analysis panel."""
275
+ container = Container()
276
+
277
+ container.mount(Label("Pull Request Analysis", classes="section-title"))
278
+ container.mount(
279
+ Static(
280
+ f"Showing {len(self.prs)} pull requests with metrics and timing data.",
281
+ classes="help-text",
282
+ )
283
+ )
284
+
285
+ # Prepare PR data for table
286
+ prs_data = []
287
+ for pr in self.prs:
288
+ pr_row = {
289
+ "title": (
290
+ pr.get("title", "")[:60] + "..."
291
+ if len(pr.get("title", "")) > 60
292
+ else pr.get("title", "")
293
+ ),
294
+ "author": pr.get("author", ""),
295
+ "state": pr.get("state", ""),
296
+ "created_date": (
297
+ pr.get("created_at", "").strftime("%Y-%m-%d") if pr.get("created_at") else ""
298
+ ),
299
+ "merged_date": (
300
+ pr.get("merged_at", "").strftime("%Y-%m-%d") if pr.get("merged_at") else ""
301
+ ),
302
+ "commits": pr.get("commits_count", 0),
303
+ "changed_files": pr.get("changed_files", 0),
304
+ "additions": pr.get("additions", 0),
305
+ "deletions": pr.get("deletions", 0),
306
+ }
307
+ prs_data.append(pr_row)
308
+
309
+ prs_table = EnhancedDataTable(data=prs_data, id="prs-table")
310
+
311
+ container.mount(prs_table)
312
+
313
+ # Action buttons
314
+ with container.mount(Horizontal(classes="action-bar")):
315
+ yield Button("Export PRs", id="export-prs")
316
+ yield Button("Show PR Metrics", id="show-pr-metrics")
317
+
318
+ return container
319
+
320
+ def _create_qualitative_panel(self) -> ScrollableContainer:
321
+ """Create qualitative insights panel."""
322
+ container = ScrollableContainer()
323
+
324
+ container.mount(Label("Qualitative Analysis Results", classes="section-title"))
325
+
326
+ if not self._has_qualitative_data():
327
+ container.mount(
328
+ Static("No qualitative analysis data available.", classes="info-message")
329
+ )
330
+ container.mount(
331
+ Static("Run analysis with qualitative processing enabled to see insights here.")
332
+ )
333
+ return container
334
+
335
+ # Analyze qualitative data distributions
336
+ change_types = {}
337
+ risk_levels = {}
338
+ domains = {}
339
+ confidence_scores = []
340
+
341
+ for commit in self.commits:
342
+ if "change_type" in commit:
343
+ change_type = commit.get("change_type", "unknown")
344
+ change_types[change_type] = change_types.get(change_type, 0) + 1
345
+
346
+ risk_level = commit.get("risk_level", "unknown")
347
+ risk_levels[risk_level] = risk_levels.get(risk_level, 0) + 1
348
+
349
+ domain = commit.get("business_domain", "unknown")
350
+ domains[domain] = domains.get(domain, 0) + 1
351
+
352
+ if "confidence_score" in commit:
353
+ confidence_scores.append(commit["confidence_score"])
354
+
355
+ # Change types distribution
356
+ container.mount(Label("Change Type Distribution", classes="subsection-title"))
357
+
358
+ change_table = Table(show_header=True, header_style="bold cyan")
359
+ change_table.add_column("Change Type", width=20)
360
+ change_table.add_column("Count", justify="right", width=10)
361
+ change_table.add_column("Percentage", justify="right", width=12)
362
+
363
+ total_commits = len(self.commits)
364
+ for change_type, count in sorted(change_types.items(), key=lambda x: x[1], reverse=True):
365
+ pct = (count / total_commits) * 100
366
+ change_table.add_row(change_type.title(), f"{count:,}", f"{pct:.1f}%")
367
+
368
+ from rich.panel import Panel
369
+
370
+ container.mount(Static(Panel(change_table, title="Change Types", border_style="cyan")))
371
+
372
+ # Risk levels distribution
373
+ container.mount(Rule())
374
+ container.mount(Label("Risk Level Distribution", classes="subsection-title"))
375
+
376
+ risk_table = Table(show_header=True, header_style="bold red")
377
+ risk_table.add_column("Risk Level", width=20)
378
+ risk_table.add_column("Count", justify="right", width=10)
379
+ risk_table.add_column("Percentage", justify="right", width=12)
380
+
381
+ for risk_level, count in sorted(risk_levels.items(), key=lambda x: x[1], reverse=True):
382
+ pct = (count / total_commits) * 100
383
+ risk_table.add_row(risk_level.title(), f"{count:,}", f"{pct:.1f}%")
384
+
385
+ container.mount(Static(Panel(risk_table, title="Risk Levels", border_style="red")))
386
+
387
+ # Business domains
388
+ container.mount(Rule())
389
+ container.mount(Label("Business Domain Activity", classes="subsection-title"))
390
+
391
+ domain_table = Table(show_header=True, header_style="bold green")
392
+ domain_table.add_column("Business Domain", width=25)
393
+ domain_table.add_column("Count", justify="right", width=10)
394
+ domain_table.add_column("Percentage", justify="right", width=12)
395
+
396
+ for domain, count in sorted(domains.items(), key=lambda x: x[1], reverse=True):
397
+ pct = (count / total_commits) * 100
398
+ domain_table.add_row(domain.title(), f"{count:,}", f"{pct:.1f}%")
399
+
400
+ container.mount(Static(Panel(domain_table, title="Business Domains", border_style="green")))
401
+
402
+ # Confidence score statistics
403
+ if confidence_scores:
404
+ container.mount(Rule())
405
+ container.mount(Label("Analysis Confidence", classes="subsection-title"))
406
+
407
+ avg_confidence = sum(confidence_scores) / len(confidence_scores)
408
+ min_confidence = min(confidence_scores)
409
+ max_confidence = max(confidence_scores)
410
+
411
+ confidence_text = f"""Average Confidence: {avg_confidence:.2f}
412
+ Minimum Confidence: {min_confidence:.2f}
413
+ Maximum Confidence: {max_confidence:.2f}
414
+ Total Analyzed: {len(confidence_scores):,} commits"""
415
+
416
+ container.mount(
417
+ Static(Panel(confidence_text, title="Confidence Statistics", border_style="yellow"))
418
+ )
419
+
420
+ return container
421
+
422
+ def _create_export_panel(self) -> Container:
423
+ """Create export options panel."""
424
+ container = Container()
425
+
426
+ container.mount(Label("Export Analysis Results", classes="section-title"))
427
+ container.mount(
428
+ Static(
429
+ "Export your analysis results in various formats for further analysis or reporting.",
430
+ classes="help-text",
431
+ )
432
+ )
433
+
434
+ # Export options
435
+ with container.mount(Vertical(id="export-options")):
436
+ yield Button(
437
+ "📄 Export Summary Report (CSV)", variant="primary", id="export-summary-csv"
438
+ )
439
+ yield Button("👥 Export Developer Statistics (CSV)", id="export-developers-csv")
440
+ yield Button("📝 Export Commit Details (CSV)", id="export-commits-csv")
441
+
442
+ if self.prs:
443
+ yield Button("🔀 Export Pull Requests (CSV)", id="export-prs-csv")
444
+
445
+ if self._has_qualitative_data():
446
+ yield Button("🧠 Export Qualitative Insights (CSV)", id="export-qualitative-csv")
447
+
448
+ yield Rule()
449
+ yield Button("📊 Export Complete Dataset (JSON)", id="export-json")
450
+ yield Button("📋 Generate Markdown Report", id="export-markdown")
451
+
452
+ # Export status
453
+ container.mount(Rule())
454
+ container.mount(Static("", id="export-status"))
455
+
456
+ return container
457
+
458
+ def _has_qualitative_data(self) -> bool:
459
+ """Check if qualitative analysis data is available."""
460
+ return any("change_type" in commit for commit in self.commits)
461
+
462
+ def _create_qualitative_summary(self) -> str:
463
+ """Create a text summary of qualitative insights."""
464
+ if not self._has_qualitative_data():
465
+ return "No qualitative data available"
466
+
467
+ # Count change types and risk levels
468
+ change_types = {}
469
+ risk_levels = {}
470
+
471
+ for commit in self.commits:
472
+ if "change_type" in commit:
473
+ change_type = commit.get("change_type", "unknown")
474
+ change_types[change_type] = change_types.get(change_type, 0) + 1
475
+
476
+ risk_level = commit.get("risk_level", "unknown")
477
+ risk_levels[risk_level] = risk_levels.get(risk_level, 0) + 1
478
+
479
+ # Find most common values
480
+ top_change_type = (
481
+ max(change_types.items(), key=lambda x: x[1]) if change_types else ("unknown", 0)
482
+ )
483
+ top_risk_level = (
484
+ max(risk_levels.items(), key=lambda x: x[1]) if risk_levels else ("unknown", 0)
485
+ )
486
+
487
+ total_analyzed = sum(change_types.values())
488
+
489
+ return f"""Qualitative Analysis Summary:
490
+ • Total commits analyzed: {total_analyzed:,}
491
+ • Most common change type: {top_change_type[0]} ({top_change_type[1]} commits)
492
+ • Most common risk level: {top_risk_level[0]} ({top_risk_level[1]} commits)
493
+ • Coverage: {(total_analyzed/len(self.commits)*100):.1f}% of all commits"""
494
+
495
+ def on_button_pressed(self, event: Button.Pressed) -> None:
496
+ """Handle button press events."""
497
+ export_actions = {
498
+ "export-summary-csv": lambda: self._export_data("summary", "csv"),
499
+ "export-developers-csv": lambda: self._export_data("developers", "csv"),
500
+ "export-commits-csv": lambda: self._export_data("commits", "csv"),
501
+ "export-prs-csv": lambda: self._export_data("prs", "csv"),
502
+ "export-qualitative-csv": lambda: self._export_data("qualitative", "csv"),
503
+ "export-json": lambda: self._export_data("complete", "json"),
504
+ "export-markdown": lambda: self._export_data("report", "markdown"),
505
+ }
506
+
507
+ action = export_actions.get(event.button.id)
508
+ if action:
509
+ action()
510
+ else:
511
+ # Handle other button actions
512
+ if event.button.id == "show-identities":
513
+ self._show_identity_details()
514
+ elif event.button.id == "show-pr-metrics":
515
+ self._show_pr_metrics()
516
+
517
+ def _export_data(self, data_type: str, format_type: str) -> None:
518
+ """
519
+ Export specific data type in specified format.
520
+
521
+ WHY: Provides flexible export functionality that allows users to
522
+ export exactly the data they need in their preferred format.
523
+ """
524
+ try:
525
+ # Determine data and filename based on type
526
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
527
+
528
+ if data_type == "summary":
529
+ data = self._prepare_summary_data()
530
+ elif data_type == "developers":
531
+ data = self.developers
532
+ elif data_type == "commits":
533
+ data = self.commits
534
+ elif data_type == "prs":
535
+ data = self.prs
536
+ elif data_type == "qualitative":
537
+ data = self._prepare_qualitative_data()
538
+ elif data_type == "complete":
539
+ data = {
540
+ "commits": self.commits,
541
+ "prs": self.prs,
542
+ "developers": self.developers,
543
+ "config": self.config.__dict__ if hasattr(self.config, "__dict__") else {},
544
+ }
545
+ else:
546
+ self.notify("Unknown export type", severity="error")
547
+ return
548
+
549
+ # Show export modal
550
+ export_modal = ExportModal(
551
+ available_formats=[format_type.upper()],
552
+ default_path=Path("./reports"),
553
+ data_info={
554
+ "type": data_type,
555
+ "row_count": len(data) if isinstance(data, list) else "N/A",
556
+ "timestamp": timestamp,
557
+ },
558
+ )
559
+
560
+ def handle_export(config):
561
+ if config:
562
+ self._perform_export(data, config, format_type)
563
+
564
+ self.app.push_screen(export_modal, handle_export)
565
+
566
+ except Exception as e:
567
+ self.notify(f"Export preparation failed: {e}", severity="error")
568
+
569
+ def _perform_export(self, data: Any, export_config: dict[str, Any], format_type: str) -> None:
570
+ """Perform the actual export operation."""
571
+ try:
572
+ export_path = export_config["path"]
573
+
574
+ if format_type == "csv":
575
+ self._export_to_csv(data, export_path, export_config)
576
+ elif format_type == "json":
577
+ self._export_to_json(data, export_path, export_config)
578
+ elif format_type == "markdown":
579
+ self._export_to_markdown(data, export_path, export_config)
580
+
581
+ self.notify(f"Successfully exported to {export_path}", severity="success")
582
+
583
+ except Exception as e:
584
+ self.notify(f"Export failed: {e}", severity="error")
585
+
586
+ def _export_to_csv(self, data: list[dict], path: Path, config: dict[str, Any]) -> None:
587
+ """Export data to CSV format."""
588
+ import csv
589
+
590
+ if not data:
591
+ return
592
+
593
+ # Ensure parent directory exists
594
+ path.parent.mkdir(parents=True, exist_ok=True)
595
+
596
+ with open(path, "w", newline="", encoding="utf-8") as csvfile:
597
+ fieldnames = list(data[0].keys())
598
+ writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
599
+
600
+ if config.get("include_headers", True):
601
+ writer.writeheader()
602
+
603
+ for row in data:
604
+ # Anonymize if requested
605
+ if config.get("anonymize", False):
606
+ row = self._anonymize_row(row)
607
+ writer.writerow(row)
608
+
609
+ def _export_to_json(self, data: Any, path: Path, config: dict[str, Any]) -> None:
610
+ """Export data to JSON format."""
611
+ # Ensure parent directory exists
612
+ path.parent.mkdir(parents=True, exist_ok=True)
613
+
614
+ # Anonymize if requested
615
+ if config.get("anonymize", False):
616
+ data = self._anonymize_data(data)
617
+
618
+ with open(path, "w", encoding="utf-8") as jsonfile:
619
+ json.dump(data, jsonfile, indent=2, default=str)
620
+
621
+ def _export_to_markdown(self, data: Any, path: Path, config: dict[str, Any]) -> None:
622
+ """Export data as markdown report."""
623
+ self.notify("Markdown export not yet implemented", severity="info")
624
+ # TODO: Implement markdown report generation
625
+
626
+ def _prepare_summary_data(self) -> list[dict]:
627
+ """Prepare summary statistics for export."""
628
+ return [
629
+ {"metric": "Total Commits", "value": len(self.commits)},
630
+ {"metric": "Total PRs", "value": len(self.prs)},
631
+ {"metric": "Active Developers", "value": len(self.developers)},
632
+ {
633
+ "metric": "Total Story Points",
634
+ "value": sum(c.get("story_points", 0) or 0 for c in self.commits),
635
+ },
636
+ ]
637
+
638
+ def _prepare_qualitative_data(self) -> list[dict]:
639
+ """Prepare qualitative analysis data for export."""
640
+ qualitative_commits = []
641
+ for commit in self.commits:
642
+ if "change_type" in commit:
643
+ qual_commit = {
644
+ "commit_hash": commit.get("hash"),
645
+ "author": commit.get("author_name"),
646
+ "message": commit.get("message"),
647
+ "change_type": commit.get("change_type"),
648
+ "business_domain": commit.get("business_domain"),
649
+ "risk_level": commit.get("risk_level"),
650
+ "confidence_score": commit.get("confidence_score"),
651
+ }
652
+ qualitative_commits.append(qual_commit)
653
+ return qualitative_commits
654
+
655
+ def _anonymize_row(self, row: dict) -> dict:
656
+ """Anonymize sensitive data in a row."""
657
+ # Simple anonymization - replace names with hashed versions
658
+ anonymized = row.copy()
659
+
660
+ # Fields to anonymize
661
+ sensitive_fields = ["author_name", "author_email", "primary_name", "primary_email"]
662
+
663
+ for field in sensitive_fields:
664
+ if field in anonymized and anonymized[field]:
665
+ # Simple hash-based anonymization
666
+ import hashlib
667
+
668
+ hash_value = hashlib.md5(str(anonymized[field]).encode()).hexdigest()[:8]
669
+ anonymized[field] = f"User_{hash_value}"
670
+
671
+ return anonymized
672
+
673
+ def _anonymize_data(self, data: Any) -> Any:
674
+ """Anonymize data structure recursively."""
675
+ if isinstance(data, list):
676
+ return [self._anonymize_data(item) for item in data]
677
+ elif isinstance(data, dict):
678
+ return {key: self._anonymize_data(value) for key, value in data.items()}
679
+ else:
680
+ return data
681
+
682
+ def _show_identity_details(self) -> None:
683
+ """Show detailed developer identity information."""
684
+ self.notify("Identity details view not yet implemented", severity="info")
685
+
686
+ def _show_pr_metrics(self) -> None:
687
+ """Show detailed pull request metrics."""
688
+ self.notify("PR metrics view not yet implemented", severity="info")
689
+
690
+ def action_back(self) -> None:
691
+ """Go back to main screen."""
692
+ self.app.pop_screen()
693
+
694
+ def action_export(self) -> None:
695
+ """Show export options."""
696
+ # Switch to export tab
697
+ tabbed_content = self.query_one(TabbedContent)
698
+ tabbed_content.active = "export"
699
+
700
+ def action_filter(self) -> None:
701
+ """Show filter options for current tab."""
702
+ self.notify("Filtering functionality not yet implemented", severity="info")
703
+
704
+ def action_refresh(self) -> None:
705
+ """Refresh current view."""
706
+ self.refresh()
707
+
708
+ def action_export_current(self) -> None:
709
+ """Export data from currently active tab."""
710
+ tabbed_content = self.query_one(TabbedContent)
711
+ current_tab = tabbed_content.active
712
+
713
+ if current_tab == "developers":
714
+ self._export_data("developers", "csv")
715
+ elif current_tab == "commits":
716
+ self._export_data("commits", "csv")
717
+ elif current_tab == "pull-requests":
718
+ self._export_data("prs", "csv")
719
+ elif current_tab == "qualitative":
720
+ self._export_data("qualitative", "csv")
721
+ else:
722
+ self._export_data("summary", "csv")