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,724 @@
1
+ """Main TUI application for GitFlow Analytics."""
2
+
3
+ import contextlib
4
+ from pathlib import Path
5
+ from typing import Optional
6
+
7
+ from textual.app import App, ComposeResult
8
+ from textual.binding import Binding
9
+
10
+ from gitflow_analytics.config import Config, ConfigLoader
11
+
12
+ from .screens.analysis_progress_screen import AnalysisProgressScreen
13
+ from .screens.configuration_screen import ConfigurationScreen
14
+ from .screens.loading_screen import InitializationLoadingScreen
15
+ from .screens.main_screen import MainScreen
16
+
17
+
18
+ class GitFlowAnalyticsApp(App):
19
+ """
20
+ Main Terminal User Interface application for GitFlow Analytics.
21
+
22
+ WHY: Provides a comprehensive TUI that guides users through the entire
23
+ analytics workflow from configuration to results analysis. Designed to
24
+ be more user-friendly than command-line interface while maintaining
25
+ the power and flexibility of the core analysis engine.
26
+
27
+ DESIGN DECISION: Uses a screen-based navigation model where each major
28
+ workflow step (configuration, analysis, results) has its own dedicated
29
+ screen. This provides clear context separation and allows for complex
30
+ interactions within each workflow step.
31
+ """
32
+
33
+ TITLE = "GitFlow Analytics"
34
+ SUB_TITLE = "Developer Productivity Analysis"
35
+
36
+ CSS = """
37
+ /* Global styles */
38
+ .screen-title {
39
+ text-align: center;
40
+ text-style: bold;
41
+ color: $accent;
42
+ margin: 1;
43
+ padding: 1;
44
+ }
45
+
46
+ .section-title {
47
+ text-style: bold;
48
+ color: $secondary;
49
+ margin: 1 0;
50
+ }
51
+
52
+ .subsection-title {
53
+ text-style: bold;
54
+ color: $warning;
55
+ margin: 1 0 0 0;
56
+ }
57
+
58
+ .help-text {
59
+ color: $text-muted;
60
+ text-style: italic;
61
+ margin: 0 0 1 0;
62
+ }
63
+
64
+ .info-message {
65
+ color: $warning;
66
+ text-style: italic;
67
+ margin: 1 0;
68
+ }
69
+
70
+ /* Panel styles */
71
+ .status-panel {
72
+ border: solid $primary;
73
+ margin: 1;
74
+ padding: 1;
75
+ height: auto;
76
+ }
77
+
78
+ .actions-panel {
79
+ border: solid $secondary;
80
+ margin: 1;
81
+ padding: 1;
82
+ height: auto;
83
+ }
84
+
85
+ .stats-panel {
86
+ border: solid $accent;
87
+ margin: 1;
88
+ padding: 1;
89
+ height: auto;
90
+ }
91
+
92
+ .progress-panel {
93
+ height: 8;
94
+ border: solid $primary;
95
+ margin: 1;
96
+ padding: 1;
97
+ }
98
+
99
+ .log-panel {
100
+ border: solid $accent;
101
+ margin: 1;
102
+ padding: 1;
103
+ min-height: 10;
104
+ }
105
+
106
+ .panel-title {
107
+ text-style: bold;
108
+ color: $primary;
109
+ margin-bottom: 1;
110
+ }
111
+
112
+ /* Form styles */
113
+ .form-row {
114
+ height: 3;
115
+ margin: 1 0;
116
+ }
117
+
118
+ .form-label {
119
+ width: 25;
120
+ padding: 1 0;
121
+ }
122
+
123
+ .form-input {
124
+ width: 1fr;
125
+ }
126
+
127
+ /* Button styles */
128
+ .button-bar {
129
+ dock: bottom;
130
+ height: 3;
131
+ align: center middle;
132
+ margin: 1;
133
+ }
134
+
135
+ .action-bar {
136
+ height: 3;
137
+ align: center middle;
138
+ margin: 1 0;
139
+ }
140
+
141
+ /* Modal styles */
142
+ #config-modal {
143
+ background: $surface;
144
+ border: thick $primary;
145
+ width: 90%;
146
+ height: 85%;
147
+ padding: 1;
148
+ }
149
+
150
+ .modal-title {
151
+ text-align: center;
152
+ text-style: bold;
153
+ color: $primary;
154
+ margin-bottom: 1;
155
+ }
156
+
157
+ /* Validation styles */
158
+ .validation-error {
159
+ color: $error;
160
+ text-style: bold;
161
+ }
162
+
163
+ .validation-success {
164
+ color: $success;
165
+ text-style: bold;
166
+ }
167
+
168
+ /* Table styles */
169
+ EnhancedDataTable {
170
+ height: auto;
171
+ min-height: 20;
172
+ }
173
+
174
+ EnhancedDataTable > .datatable--header {
175
+ background: $primary 10%;
176
+ color: $primary;
177
+ text-style: bold;
178
+ }
179
+
180
+ EnhancedDataTable > .datatable--row-hover {
181
+ background: $accent 20%;
182
+ }
183
+
184
+ EnhancedDataTable > .datatable--row-cursor {
185
+ background: $primary 30%;
186
+ }
187
+
188
+ /* Progress widget styles */
189
+ AnalysisProgressWidget {
190
+ height: auto;
191
+ border: solid $primary;
192
+ margin: 1;
193
+ padding: 1;
194
+ }
195
+
196
+ .progress-title {
197
+ text-style: bold;
198
+ color: $primary;
199
+ margin-bottom: 1;
200
+ }
201
+
202
+ .progress-status {
203
+ color: $text;
204
+ margin-top: 1;
205
+ }
206
+
207
+ .progress-eta {
208
+ color: $accent;
209
+ text-style: italic;
210
+ }
211
+
212
+ /* Loading screen styles */
213
+ #loading-container {
214
+ height: 100%;
215
+ align: center middle;
216
+ }
217
+
218
+ #loading-content {
219
+ width: 80%;
220
+ max-width: 100;
221
+ align: center middle;
222
+ margin: 2;
223
+ }
224
+
225
+ #main-spinner {
226
+ margin: 1;
227
+ align: center middle;
228
+ }
229
+
230
+ #loading-steps {
231
+ margin-top: 1;
232
+ }
233
+
234
+ #loading-steps > Static {
235
+ margin: 0 0 0 2;
236
+ padding: 0;
237
+ }
238
+
239
+ /* Utility classes */
240
+ .hidden {
241
+ display: none;
242
+ }
243
+
244
+ .center {
245
+ text-align: center;
246
+ }
247
+
248
+ .bold {
249
+ text-style: bold;
250
+ }
251
+
252
+ .italic {
253
+ text-style: italic;
254
+ }
255
+
256
+ .error {
257
+ color: $error;
258
+ }
259
+
260
+ .success {
261
+ color: $success;
262
+ }
263
+
264
+ .warning {
265
+ color: $warning;
266
+ }
267
+ """
268
+
269
+ BINDINGS = [
270
+ Binding("ctrl+q", "quit", "Quit", priority=True),
271
+ Binding("ctrl+c", "quit", "Quit", priority=True),
272
+ Binding("f1", "help", "Help"),
273
+ Binding("ctrl+d", "toggle_dark", "Toggle Dark Mode"),
274
+ ]
275
+
276
+ def __init__(self) -> None:
277
+ super().__init__()
278
+ self.config: Optional[Config] = None
279
+ self.config_path: Optional[Path] = None
280
+ self.initialization_complete = False
281
+ self._nlp_engine = None
282
+
283
+ def compose(self) -> ComposeResult:
284
+ """
285
+ Compose the main application. We don't yield screens here during startup
286
+ to avoid screen stack management issues.
287
+
288
+ WHY: Instead of yielding screens in compose, we handle the initial screen
289
+ setup in on_mount to ensure proper screen stack management and avoid
290
+ IndexError when transitioning between screens.
291
+ """
292
+ # Don't yield screens here - handle in on_mount for proper screen management
293
+ # Return empty to avoid NoneType iteration error
294
+ return iter([])
295
+
296
+ def on_mount(self) -> None:
297
+ """
298
+ Handle application startup with loading screen.
299
+
300
+ WHY: Shows loading screen immediately while heavy initialization
301
+ happens in the background, providing better user experience.
302
+ Using push_screen instead of compose to avoid screen stack issues.
303
+ """
304
+ # Set up application title
305
+ self.title = "GitFlow Analytics"
306
+ self.sub_title = "Developer Productivity Analysis"
307
+
308
+ # Start with loading screen during initialization
309
+ if not self.initialization_complete:
310
+ loading_screen = InitializationLoadingScreen(
311
+ config_loader_func=self._load_default_config,
312
+ nlp_init_func=self._initialize_nlp_engine,
313
+ loading_message="Initializing GitFlow Analytics...",
314
+ id="loading-screen",
315
+ )
316
+ self.push_screen(loading_screen)
317
+ else:
318
+ # Show main screen if already initialized
319
+ main_screen = MainScreen(self.config, self.config_path, id="main-screen")
320
+ self.push_screen(main_screen)
321
+
322
+ def _load_default_config(self) -> Optional[tuple]:
323
+ """
324
+ Attempt to load configuration from default locations.
325
+
326
+ WHY: Provides automatic configuration discovery to reduce setup
327
+ friction for users who have configurations in standard locations.
328
+ This version is designed to be called from the loading screen.
329
+
330
+ @return: Tuple of (config, config_path) if found, None otherwise
331
+ """
332
+ default_config_paths = [
333
+ Path("config.yaml"),
334
+ Path("gitflow.yaml"),
335
+ Path(".gitflow/config.yaml"),
336
+ Path("~/.gitflow/config.yaml").expanduser(),
337
+ ]
338
+
339
+ for config_path in default_config_paths:
340
+ if config_path.exists():
341
+ try:
342
+ config = ConfigLoader.load(config_path)
343
+ return (config, config_path)
344
+
345
+ except Exception:
346
+ # Log error but continue trying other paths
347
+ continue
348
+
349
+ return None
350
+
351
+ def _initialize_nlp_engine(self, config: Optional[Config] = None) -> Optional[object]:
352
+ """
353
+ Initialize the NLP engine for qualitative analysis.
354
+
355
+ WHY: spaCy model loading can be slow and should happen during
356
+ the loading screen to provide user feedback. This method handles
357
+ the heavy NLP initialization work.
358
+
359
+ @param config: Configuration object with qualitative settings
360
+ @return: Initialized NLP engine object or None if not needed/failed
361
+ """
362
+ if not config or not getattr(config, "qualitative", None):
363
+ return None
364
+
365
+ if not config.qualitative.enabled:
366
+ return None
367
+
368
+ try:
369
+ # Import here to avoid slow imports at module level
370
+ from gitflow_analytics.qualitative.core.nlp_engine import NLPEngine
371
+
372
+ # Initialize the NLP engine (this loads spaCy models)
373
+ nlp_engine = NLPEngine(config.qualitative.nlp)
374
+
375
+ # Validate the setup
376
+ is_valid, issues = nlp_engine.validate_setup()
377
+ if not is_valid:
378
+ # Return None if validation fails, but don't raise exception
379
+ return None
380
+
381
+ return nlp_engine
382
+
383
+ except ImportError:
384
+ # Qualitative analysis dependencies not available
385
+ return None
386
+ except Exception:
387
+ # Other initialization errors
388
+ return None
389
+
390
+ def on_initialization_loading_screen_initialization_complete(
391
+ self, message: InitializationLoadingScreen.InitializationComplete
392
+ ) -> None:
393
+ """
394
+ Handle completion of initialization loading.
395
+
396
+ WHY: When the loading screen completes initialization, we need to
397
+ update the app state and transition to the main screen. Using pop_screen
398
+ and push_screen for proper screen stack management.
399
+ """
400
+ initialization_data = message.data
401
+
402
+ # Update app state with loaded configuration
403
+ config_result = initialization_data.get("config")
404
+ if config_result:
405
+ self.config, self.config_path = config_result
406
+
407
+ # Store NLP engine for later use
408
+ self._nlp_engine = initialization_data.get("nlp")
409
+
410
+ # Mark initialization as complete
411
+ self.initialization_complete = True
412
+
413
+ # Transition to main screen by popping loading screen and pushing main screen
414
+ with contextlib.suppress(Exception):
415
+ self.pop_screen() # Remove loading screen
416
+
417
+ main_screen = MainScreen(self.config, self.config_path, id="main-screen")
418
+ self.push_screen(main_screen)
419
+
420
+ # Show success notification if config was loaded
421
+ if self.config and self.config_path:
422
+ self.notify(f"Loaded configuration from {self.config_path}", severity="info")
423
+
424
+ def on_initialization_loading_screen_loading_cancelled(
425
+ self, message: InitializationLoadingScreen.LoadingCancelled
426
+ ) -> None:
427
+ """
428
+ Handle cancellation of initialization loading.
429
+
430
+ WHY: If user cancels loading, we should still show the main screen
431
+ but without the loaded configuration. Using pop_screen and push_screen
432
+ for proper screen stack management.
433
+ """
434
+ # Mark initialization as complete (even if cancelled)
435
+ self.initialization_complete = True
436
+
437
+ # Transition to main screen without configuration
438
+ with contextlib.suppress(Exception):
439
+ self.pop_screen() # Remove loading screen
440
+
441
+ main_screen = MainScreen(None, None, id="main-screen")
442
+ self.push_screen(main_screen)
443
+
444
+ self.notify(
445
+ "Initialization cancelled. You can load configuration manually.", severity="warning"
446
+ )
447
+
448
+ def on_main_screen_new_analysis_requested(
449
+ self, message: MainScreen.NewAnalysisRequested
450
+ ) -> None:
451
+ """Handle new analysis request from main screen."""
452
+ if not self.config:
453
+ self.notify("Please load or create a configuration first", severity="error")
454
+ return
455
+
456
+ # Launch analysis progress screen
457
+ analysis_screen = AnalysisProgressScreen(
458
+ config=self.config,
459
+ weeks=12, # TODO: Get from config or user preference
460
+ enable_qualitative=getattr(self.config, "qualitative", None)
461
+ and self.config.qualitative.enabled,
462
+ )
463
+
464
+ self.push_screen(analysis_screen)
465
+
466
+ def on_main_screen_configuration_requested(
467
+ self, message: MainScreen.ConfigurationRequested
468
+ ) -> None:
469
+ """Handle configuration request from main screen."""
470
+ config_screen = ConfigurationScreen(config_path=self.config_path, config=self.config)
471
+
472
+ def handle_config_result(config: Optional[Config]) -> None:
473
+ if config:
474
+ self.config = config
475
+ # TODO: Set config_path if provided
476
+
477
+ # Update main screen
478
+ main_screen = self.query_one("#main-screen", MainScreen)
479
+ main_screen.update_config(self.config, self.config_path)
480
+
481
+ self.notify("Configuration updated successfully", severity="success")
482
+
483
+ self.push_screen(config_screen, handle_config_result)
484
+
485
+ def on_main_screen_cache_status_requested(
486
+ self, message: MainScreen.CacheStatusRequested
487
+ ) -> None:
488
+ """Handle cache status request from main screen."""
489
+ if not self.config:
490
+ self.notify("No configuration loaded", severity="error")
491
+ return
492
+
493
+ try:
494
+ from gitflow_analytics.core.cache import GitAnalysisCache
495
+
496
+ cache = GitAnalysisCache(self.config.cache.directory)
497
+ stats = cache.get_cache_stats()
498
+
499
+ # Calculate cache size
500
+ import os
501
+
502
+ cache_size = 0
503
+ try:
504
+ for root, _dirs, files in os.walk(self.config.cache.directory):
505
+ for f in files:
506
+ cache_size += os.path.getsize(os.path.join(root, f))
507
+ cache_size_mb = cache_size / 1024 / 1024
508
+ except Exception:
509
+ cache_size_mb = 0
510
+
511
+ message_text = f"""Cache Statistics:
512
+ • Location: {self.config.cache.directory}
513
+ • Cached commits: {stats['cached_commits']:,}
514
+ • Cached PRs: {stats['cached_prs']:,}
515
+ • Cached issues: {stats['cached_issues']:,}
516
+ • Stale entries: {stats['stale_commits']:,}
517
+ • Cache size: {cache_size_mb:.1f} MB
518
+ • TTL: {self.config.cache.ttl_hours} hours"""
519
+
520
+ self.notify(message_text, severity="info")
521
+
522
+ except Exception as e:
523
+ self.notify(f"Failed to get cache statistics: {e}", severity="error")
524
+
525
+ def on_main_screen_identity_management_requested(
526
+ self, message: MainScreen.IdentityManagementRequested
527
+ ) -> None:
528
+ """Handle identity management request from main screen."""
529
+ if not self.config:
530
+ self.notify("No configuration loaded", severity="error")
531
+ return
532
+
533
+ try:
534
+ from gitflow_analytics.core.identity import DeveloperIdentityResolver
535
+
536
+ identity_resolver = DeveloperIdentityResolver(
537
+ self.config.cache.directory / "identities.db"
538
+ )
539
+
540
+ developers = identity_resolver.get_developer_stats()
541
+
542
+ if not developers:
543
+ self.notify("No developer identities found. Run analysis first.", severity="info")
544
+ return
545
+
546
+ # Show top developers
547
+ top_devs = sorted(developers, key=lambda d: d["total_commits"], reverse=True)[:10]
548
+
549
+ dev_list = []
550
+ for dev in top_devs:
551
+ dev_list.append(
552
+ f"• {dev['primary_name']}: {dev['total_commits']} commits, {dev['alias_count']} aliases"
553
+ )
554
+
555
+ message_text = f"""Developer Identity Statistics:
556
+ • Total unique developers: {len(developers)}
557
+ • Manual mappings: {len(self.config.analysis.manual_identity_mappings) if self.config.analysis.manual_identity_mappings else 0}
558
+
559
+ Top Contributors:
560
+ {chr(10).join(dev_list)}
561
+
562
+ Use the CLI 'merge-identity' command to merge duplicate identities."""
563
+
564
+ self.notify(message_text, severity="info")
565
+
566
+ except Exception as e:
567
+ self.notify(f"Failed to get identity information: {e}", severity="error")
568
+
569
+ def on_main_screen_help_requested(self, message: MainScreen.HelpRequested) -> None:
570
+ """Handle help request from main screen."""
571
+ help_text = """GitFlow Analytics - Terminal UI Help
572
+
573
+ 🚀 Getting Started:
574
+ 1. Load or create a configuration file (API keys, repositories)
575
+ 2. Configure analysis settings (time period, options)
576
+ 3. Run analysis to process your repositories
577
+ 4. Explore results and export reports
578
+
579
+ ⌨️ Key Bindings:
580
+ • Ctrl+Q / Ctrl+C: Quit application
581
+ • F1: Show this help
582
+ • Ctrl+D: Toggle dark/light mode
583
+ • Escape: Go back/cancel current action
584
+
585
+ 📁 Configuration:
586
+ • GitHub Personal Access Token required
587
+ • OpenRouter API key for qualitative analysis
588
+ • JIRA optional for enhanced ticket tracking
589
+
590
+ 🔧 Analysis Features:
591
+ • Git commit analysis with developer identity resolution
592
+ • Pull request metrics and timing analysis
593
+ • Qualitative analysis using AI for commit categorization
594
+ • DORA metrics calculation
595
+ • Story point tracking from commit messages
596
+
597
+ 📊 Export Options:
598
+ • CSV reports for spreadsheet analysis
599
+ • JSON export for API integration
600
+ • Markdown reports for documentation
601
+
602
+ 💡 Tips:
603
+ • Use organization auto-discovery for multiple repositories
604
+ • Enable qualitative analysis for deeper insights
605
+ • Review identity mappings for accurate attribution
606
+
607
+ For more information: https://github.com/bobmatnyc/gitflow-analytics"""
608
+
609
+ self.notify(help_text, severity="info")
610
+
611
+ def action_quit(self) -> None:
612
+ """
613
+ Quit the application with confirmation if analysis is running.
614
+
615
+ WHY: Provides safe exit that checks for running operations to
616
+ prevent data loss or corruption from incomplete analysis.
617
+ """
618
+ # Check if analysis is running
619
+ try:
620
+ analysis_screen = self.query("AnalysisProgressScreen")
621
+ if analysis_screen:
622
+ # TODO: Show confirmation dialog
623
+ pass
624
+ except Exception:
625
+ pass
626
+
627
+ self.exit()
628
+
629
+ def action_help(self) -> None:
630
+ """Show application help."""
631
+ # Trigger help from main screen if available
632
+ try:
633
+ main_screen = self.query_one("#main-screen", MainScreen)
634
+ main_screen.action_help()
635
+ except Exception:
636
+ # Fallback to direct help
637
+ self.on_main_screen_help_requested(MainScreen.HelpRequested())
638
+
639
+ def action_toggle_dark(self) -> None:
640
+ """
641
+ Toggle between dark and light themes.
642
+
643
+ WHY: Provides theme flexibility for different user preferences
644
+ and working environments (bright vs dim lighting conditions).
645
+ """
646
+ self.dark = not self.dark
647
+ theme = "dark" if self.dark else "light"
648
+ self.notify(f"Switched to {theme} theme", severity="info")
649
+
650
+ def get_current_config(self) -> Optional[Config]:
651
+ """Get the currently loaded configuration."""
652
+ return self.config
653
+
654
+ def get_current_config_path(self) -> Optional[Path]:
655
+ """Get the path of the currently loaded configuration."""
656
+ return self.config_path
657
+
658
+ def get_nlp_engine(self) -> Optional[object]:
659
+ """
660
+ Get the initialized NLP engine for qualitative analysis.
661
+
662
+ WHY: Provides access to the pre-loaded NLP engine to avoid
663
+ re-initialization overhead during analysis operations.
664
+
665
+ @return: Initialized NLP engine or None if not available
666
+ """
667
+ return self._nlp_engine
668
+
669
+ def update_config(self, config: Config, config_path: Optional[Path] = None) -> None:
670
+ """
671
+ Update the application configuration and refresh relevant screens.
672
+
673
+ WHY: Provides centralized configuration updates that can be called
674
+ from any screen to ensure all parts of the application stay in sync.
675
+ """
676
+ self.config = config
677
+ self.config_path = config_path
678
+
679
+ # Update main screen if visible
680
+ try:
681
+ main_screen = self.query_one("#main-screen", MainScreen)
682
+ main_screen.update_config(config, config_path)
683
+ except Exception:
684
+ pass
685
+
686
+ async def run_analysis_async(
687
+ self, weeks: int = 12, enable_qualitative: bool = True
688
+ ) -> Optional[dict]:
689
+ """
690
+ Run analysis asynchronously and return results.
691
+
692
+ WHY: Provides programmatic access to analysis functionality
693
+ for integration with other systems or automated workflows.
694
+
695
+ @param weeks: Number of weeks to analyze
696
+ @param enable_qualitative: Whether to enable qualitative analysis
697
+ @return: Analysis results dictionary or None if failed
698
+ """
699
+ if not self.config:
700
+ raise ValueError("No configuration loaded")
701
+
702
+ # Create analysis screen
703
+ AnalysisProgressScreen(
704
+ config=self.config, weeks=weeks, enable_qualitative=enable_qualitative
705
+ )
706
+
707
+ # Run analysis (this would need to be implemented properly)
708
+ # For now, return None to indicate not implemented
709
+ return None
710
+
711
+
712
+ def main() -> None:
713
+ """
714
+ Main entry point for the TUI application.
715
+
716
+ WHY: Provides a clean entry point that can be called from the CLI
717
+ or used as a standalone application launcher.
718
+ """
719
+ app = GitFlowAnalyticsApp()
720
+ app.run()
721
+
722
+
723
+ if __name__ == "__main__":
724
+ main()