gitflow-analytics 1.0.3__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 (116) hide show
  1. gitflow_analytics/_version.py +1 -1
  2. gitflow_analytics/classification/__init__.py +31 -0
  3. gitflow_analytics/classification/batch_classifier.py +752 -0
  4. gitflow_analytics/classification/classifier.py +464 -0
  5. gitflow_analytics/classification/feature_extractor.py +725 -0
  6. gitflow_analytics/classification/linguist_analyzer.py +574 -0
  7. gitflow_analytics/classification/model.py +455 -0
  8. gitflow_analytics/cli.py +4108 -350
  9. gitflow_analytics/cli_rich.py +198 -48
  10. gitflow_analytics/config/__init__.py +43 -0
  11. gitflow_analytics/config/errors.py +261 -0
  12. gitflow_analytics/config/loader.py +904 -0
  13. gitflow_analytics/config/profiles.py +264 -0
  14. gitflow_analytics/config/repository.py +124 -0
  15. gitflow_analytics/config/schema.py +441 -0
  16. gitflow_analytics/config/validator.py +154 -0
  17. gitflow_analytics/config.py +44 -508
  18. gitflow_analytics/core/analyzer.py +1209 -98
  19. gitflow_analytics/core/cache.py +1337 -29
  20. gitflow_analytics/core/data_fetcher.py +1193 -0
  21. gitflow_analytics/core/identity.py +363 -14
  22. gitflow_analytics/core/metrics_storage.py +526 -0
  23. gitflow_analytics/core/progress.py +372 -0
  24. gitflow_analytics/core/schema_version.py +269 -0
  25. gitflow_analytics/extractors/ml_tickets.py +1100 -0
  26. gitflow_analytics/extractors/story_points.py +8 -1
  27. gitflow_analytics/extractors/tickets.py +749 -11
  28. gitflow_analytics/identity_llm/__init__.py +6 -0
  29. gitflow_analytics/identity_llm/analysis_pass.py +231 -0
  30. gitflow_analytics/identity_llm/analyzer.py +464 -0
  31. gitflow_analytics/identity_llm/models.py +76 -0
  32. gitflow_analytics/integrations/github_integration.py +175 -11
  33. gitflow_analytics/integrations/jira_integration.py +461 -24
  34. gitflow_analytics/integrations/orchestrator.py +124 -1
  35. gitflow_analytics/metrics/activity_scoring.py +322 -0
  36. gitflow_analytics/metrics/branch_health.py +470 -0
  37. gitflow_analytics/metrics/dora.py +379 -20
  38. gitflow_analytics/models/database.py +843 -53
  39. gitflow_analytics/pm_framework/__init__.py +115 -0
  40. gitflow_analytics/pm_framework/adapters/__init__.py +50 -0
  41. gitflow_analytics/pm_framework/adapters/jira_adapter.py +1845 -0
  42. gitflow_analytics/pm_framework/base.py +406 -0
  43. gitflow_analytics/pm_framework/models.py +211 -0
  44. gitflow_analytics/pm_framework/orchestrator.py +652 -0
  45. gitflow_analytics/pm_framework/registry.py +333 -0
  46. gitflow_analytics/qualitative/__init__.py +9 -10
  47. gitflow_analytics/qualitative/chatgpt_analyzer.py +259 -0
  48. gitflow_analytics/qualitative/classifiers/__init__.py +3 -3
  49. gitflow_analytics/qualitative/classifiers/change_type.py +518 -244
  50. gitflow_analytics/qualitative/classifiers/domain_classifier.py +272 -165
  51. gitflow_analytics/qualitative/classifiers/intent_analyzer.py +321 -222
  52. gitflow_analytics/qualitative/classifiers/llm/__init__.py +35 -0
  53. gitflow_analytics/qualitative/classifiers/llm/base.py +193 -0
  54. gitflow_analytics/qualitative/classifiers/llm/batch_processor.py +383 -0
  55. gitflow_analytics/qualitative/classifiers/llm/cache.py +479 -0
  56. gitflow_analytics/qualitative/classifiers/llm/cost_tracker.py +435 -0
  57. gitflow_analytics/qualitative/classifiers/llm/openai_client.py +403 -0
  58. gitflow_analytics/qualitative/classifiers/llm/prompts.py +373 -0
  59. gitflow_analytics/qualitative/classifiers/llm/response_parser.py +287 -0
  60. gitflow_analytics/qualitative/classifiers/llm_commit_classifier.py +607 -0
  61. gitflow_analytics/qualitative/classifiers/risk_analyzer.py +215 -189
  62. gitflow_analytics/qualitative/core/__init__.py +4 -4
  63. gitflow_analytics/qualitative/core/llm_fallback.py +239 -235
  64. gitflow_analytics/qualitative/core/nlp_engine.py +157 -148
  65. gitflow_analytics/qualitative/core/pattern_cache.py +214 -192
  66. gitflow_analytics/qualitative/core/processor.py +381 -248
  67. gitflow_analytics/qualitative/enhanced_analyzer.py +2236 -0
  68. gitflow_analytics/qualitative/example_enhanced_usage.py +420 -0
  69. gitflow_analytics/qualitative/models/__init__.py +7 -7
  70. gitflow_analytics/qualitative/models/schemas.py +155 -121
  71. gitflow_analytics/qualitative/utils/__init__.py +4 -4
  72. gitflow_analytics/qualitative/utils/batch_processor.py +136 -123
  73. gitflow_analytics/qualitative/utils/cost_tracker.py +142 -140
  74. gitflow_analytics/qualitative/utils/metrics.py +172 -158
  75. gitflow_analytics/qualitative/utils/text_processing.py +146 -104
  76. gitflow_analytics/reports/__init__.py +100 -0
  77. gitflow_analytics/reports/analytics_writer.py +539 -14
  78. gitflow_analytics/reports/base.py +648 -0
  79. gitflow_analytics/reports/branch_health_writer.py +322 -0
  80. gitflow_analytics/reports/classification_writer.py +924 -0
  81. gitflow_analytics/reports/cli_integration.py +427 -0
  82. gitflow_analytics/reports/csv_writer.py +1676 -212
  83. gitflow_analytics/reports/data_models.py +504 -0
  84. gitflow_analytics/reports/database_report_generator.py +427 -0
  85. gitflow_analytics/reports/example_usage.py +344 -0
  86. gitflow_analytics/reports/factory.py +499 -0
  87. gitflow_analytics/reports/formatters.py +698 -0
  88. gitflow_analytics/reports/html_generator.py +1116 -0
  89. gitflow_analytics/reports/interfaces.py +489 -0
  90. gitflow_analytics/reports/json_exporter.py +2770 -0
  91. gitflow_analytics/reports/narrative_writer.py +2287 -158
  92. gitflow_analytics/reports/story_point_correlation.py +1144 -0
  93. gitflow_analytics/reports/weekly_trends_writer.py +389 -0
  94. gitflow_analytics/training/__init__.py +5 -0
  95. gitflow_analytics/training/model_loader.py +377 -0
  96. gitflow_analytics/training/pipeline.py +550 -0
  97. gitflow_analytics/tui/__init__.py +1 -1
  98. gitflow_analytics/tui/app.py +129 -126
  99. gitflow_analytics/tui/screens/__init__.py +3 -3
  100. gitflow_analytics/tui/screens/analysis_progress_screen.py +188 -179
  101. gitflow_analytics/tui/screens/configuration_screen.py +154 -178
  102. gitflow_analytics/tui/screens/loading_screen.py +100 -110
  103. gitflow_analytics/tui/screens/main_screen.py +89 -72
  104. gitflow_analytics/tui/screens/results_screen.py +305 -281
  105. gitflow_analytics/tui/widgets/__init__.py +2 -2
  106. gitflow_analytics/tui/widgets/data_table.py +67 -69
  107. gitflow_analytics/tui/widgets/export_modal.py +76 -76
  108. gitflow_analytics/tui/widgets/progress_widget.py +41 -46
  109. gitflow_analytics-1.3.6.dist-info/METADATA +1015 -0
  110. gitflow_analytics-1.3.6.dist-info/RECORD +122 -0
  111. gitflow_analytics-1.0.3.dist-info/METADATA +0 -490
  112. gitflow_analytics-1.0.3.dist-info/RECORD +0 -62
  113. {gitflow_analytics-1.0.3.dist-info → gitflow_analytics-1.3.6.dist-info}/WHEEL +0 -0
  114. {gitflow_analytics-1.0.3.dist-info → gitflow_analytics-1.3.6.dist-info}/entry_points.txt +0 -0
  115. {gitflow_analytics-1.0.3.dist-info → gitflow_analytics-1.3.6.dist-info}/licenses/LICENSE +0 -0
  116. {gitflow_analytics-1.0.3.dist-info → gitflow_analytics-1.3.6.dist-info}/top_level.txt +0 -0
@@ -1,24 +1,25 @@
1
1
  """Intent analyzer for extracting developer intent and urgency from commits."""
2
2
 
3
+ import importlib.util
3
4
  import logging
4
5
  import re
5
- from typing import Dict, List, Any, Set
6
6
  from collections import defaultdict
7
+ from typing import Any
7
8
 
8
9
  from ..models.schemas import IntentConfig
9
10
 
10
- try:
11
- import spacy
11
+ # Check if spacy is available without importing it
12
+ SPACY_AVAILABLE = importlib.util.find_spec("spacy") is not None
13
+
14
+ if SPACY_AVAILABLE:
12
15
  from spacy.tokens import Doc
13
- SPACY_AVAILABLE = True
14
- except ImportError:
15
- SPACY_AVAILABLE = False
16
+ else:
16
17
  Doc = Any
17
18
 
18
19
 
19
20
  class IntentAnalyzer:
20
21
  """Analyze commit messages to extract developer intent and urgency signals.
21
-
22
+
22
23
  This analyzer identifies:
23
24
  - Urgency level (critical, important, routine)
24
25
  - Intent confidence (how clear the intent is)
@@ -26,124 +27,236 @@ class IntentAnalyzer:
26
27
  - Planning signals (TODO, FIXME, temporary fixes)
27
28
  - Collaboration signals (pair programming, code review)
28
29
  """
29
-
30
+
30
31
  def __init__(self, config: IntentConfig):
31
32
  """Initialize intent analyzer.
32
-
33
+
33
34
  Args:
34
35
  config: Configuration for intent analysis
35
36
  """
36
37
  self.config = config
37
38
  self.logger = logging.getLogger(__name__)
38
-
39
+
39
40
  # Urgency keyword patterns from config
40
41
  self.urgency_keywords = config.urgency_keywords
41
-
42
+
42
43
  # Confidence indicators
43
44
  self.confidence_indicators = {
44
- 'high_confidence': {
45
- 'definitely', 'clearly', 'obviously', 'certainly', 'confirmed',
46
- 'verified', 'tested', 'working', 'complete', 'finished',
47
- 'implement', 'solution', 'resolve'
45
+ "high_confidence": {
46
+ "definitely",
47
+ "clearly",
48
+ "obviously",
49
+ "certainly",
50
+ "confirmed",
51
+ "verified",
52
+ "tested",
53
+ "working",
54
+ "complete",
55
+ "finished",
56
+ "implement",
57
+ "solution",
58
+ "resolve",
48
59
  },
49
- 'low_confidence': {
50
- 'maybe', 'perhaps', 'possibly', 'might', 'could', 'should',
51
- 'try', 'attempt', 'experiment', 'test', 'temporary', 'quick',
52
- 'hack', 'workaround', 'temp'
60
+ "low_confidence": {
61
+ "maybe",
62
+ "perhaps",
63
+ "possibly",
64
+ "might",
65
+ "could",
66
+ "should",
67
+ "try",
68
+ "attempt",
69
+ "experiment",
70
+ "test",
71
+ "temporary",
72
+ "quick",
73
+ "hack",
74
+ "workaround",
75
+ "temp",
76
+ },
77
+ "uncertain": {
78
+ "not sure",
79
+ "unclear",
80
+ "confusing",
81
+ "weird",
82
+ "strange",
83
+ "unexpected",
84
+ "unsure",
85
+ "investigation",
86
+ "debug",
87
+ "investigate",
53
88
  },
54
- 'uncertain': {
55
- 'not sure', 'unclear', 'confusing', 'weird', 'strange',
56
- 'unexpected', 'unsure', 'investigation', 'debug', 'investigate'
57
- }
58
89
  }
59
-
90
+
60
91
  # Emotional tone indicators
61
92
  self.tone_indicators = {
62
- 'frustrated': {
63
- 'annoying', 'frustrating', 'stupid', 'broken', 'terrible',
64
- 'awful', 'hate', 'annoyed', 'ugh', 'argh', 'damn', 'wtf'
93
+ "frustrated": {
94
+ "annoying",
95
+ "frustrating",
96
+ "stupid",
97
+ "broken",
98
+ "terrible",
99
+ "awful",
100
+ "hate",
101
+ "annoyed",
102
+ "ugh",
103
+ "argh",
104
+ "damn",
105
+ "wtf",
65
106
  },
66
- 'confident': {
67
- 'great', 'excellent', 'perfect', 'awesome', 'clean', 'elegant',
68
- 'nice', 'good', 'better', 'improved', 'optimized'
107
+ "confident": {
108
+ "great",
109
+ "excellent",
110
+ "perfect",
111
+ "awesome",
112
+ "clean",
113
+ "elegant",
114
+ "nice",
115
+ "good",
116
+ "better",
117
+ "improved",
118
+ "optimized",
119
+ },
120
+ "cautious": {
121
+ "careful",
122
+ "cautious",
123
+ "gentle",
124
+ "safe",
125
+ "conservative",
126
+ "minimal",
127
+ "small",
128
+ "incremental",
129
+ "gradual",
69
130
  },
70
- 'cautious': {
71
- 'careful', 'cautious', 'gentle', 'safe', 'conservative',
72
- 'minimal', 'small', 'incremental', 'gradual'
73
- }
74
131
  }
75
-
132
+
76
133
  # Planning and TODO indicators
77
134
  self.planning_indicators = {
78
- 'todo': {
79
- 'todo', 'fixme', 'hack', 'temporary', 'temp', 'later',
80
- 'placeholder', 'stub', 'incomplete', 'wip'
135
+ "todo": {
136
+ "todo",
137
+ "fixme",
138
+ "hack",
139
+ "temporary",
140
+ "temp",
141
+ "later",
142
+ "placeholder",
143
+ "stub",
144
+ "incomplete",
145
+ "wip",
81
146
  },
82
- 'future_work': {
83
- 'future', 'later', 'eventually', 'someday', 'next',
84
- 'upcoming', 'planned', 'roadmap'
147
+ "future_work": {
148
+ "future",
149
+ "later",
150
+ "eventually",
151
+ "someday",
152
+ "next",
153
+ "upcoming",
154
+ "planned",
155
+ "roadmap",
156
+ },
157
+ "immediate": {
158
+ "now",
159
+ "immediate",
160
+ "urgent",
161
+ "asap",
162
+ "quickly",
163
+ "fast",
164
+ "emergency",
165
+ "critical",
166
+ "hotfix",
85
167
  },
86
- 'immediate': {
87
- 'now', 'immediate', 'urgent', 'asap', 'quickly', 'fast',
88
- 'emergency', 'critical', 'hotfix'
89
- }
90
168
  }
91
-
169
+
92
170
  # Collaboration indicators
93
171
  self.collaboration_indicators = {
94
- 'pair_programming': {
95
- 'pair', 'pairing', 'together', 'with', 'collaborative',
96
- 'co-authored', 'mob', 'mobbing'
172
+ "pair_programming": {
173
+ "pair",
174
+ "pairing",
175
+ "together",
176
+ "with",
177
+ "collaborative",
178
+ "co-authored",
179
+ "mob",
180
+ "mobbing",
97
181
  },
98
- 'code_review': {
99
- 'review', 'feedback', 'suggestion', 'requested', 'comment',
100
- 'pr', 'pull request', 'merge request'
182
+ "code_review": {
183
+ "review",
184
+ "feedback",
185
+ "suggestion",
186
+ "requested",
187
+ "comment",
188
+ "pr",
189
+ "pull request",
190
+ "merge request",
191
+ },
192
+ "help_seeking": {
193
+ "help",
194
+ "assistance",
195
+ "advice",
196
+ "guidance",
197
+ "input",
198
+ "thoughts",
199
+ "opinions",
200
+ "feedback",
101
201
  },
102
- 'help_seeking': {
103
- 'help', 'assistance', 'advice', 'guidance', 'input',
104
- 'thoughts', 'opinions', 'feedback'
105
- }
106
202
  }
107
-
203
+
108
204
  # Technical complexity indicators
109
205
  self.complexity_indicators = {
110
- 'simple': {
111
- 'simple', 'easy', 'quick', 'minor', 'small', 'tiny',
112
- 'straightforward', 'basic'
206
+ "simple": {
207
+ "simple",
208
+ "easy",
209
+ "quick",
210
+ "minor",
211
+ "small",
212
+ "tiny",
213
+ "straightforward",
214
+ "basic",
113
215
  },
114
- 'complex': {
115
- 'complex', 'complicated', 'difficult', 'challenging',
116
- 'major', 'significant', 'substantial', 'extensive'
216
+ "complex": {
217
+ "complex",
218
+ "complicated",
219
+ "difficult",
220
+ "challenging",
221
+ "major",
222
+ "significant",
223
+ "substantial",
224
+ "extensive",
225
+ },
226
+ "refactoring": {
227
+ "refactor",
228
+ "restructure",
229
+ "reorganize",
230
+ "cleanup",
231
+ "simplify",
232
+ "optimize",
233
+ "improve",
117
234
  },
118
- 'refactoring': {
119
- 'refactor', 'restructure', 'reorganize', 'cleanup',
120
- 'simplify', 'optimize', 'improve'
121
- }
122
235
  }
123
-
124
- def analyze(self, message: str, doc: Doc) -> Dict[str, Any]:
236
+
237
+ def analyze(self, message: str, doc: Doc) -> dict[str, Any]:
125
238
  """Analyze commit message for intent signals.
126
-
239
+
127
240
  Args:
128
241
  message: Commit message
129
242
  doc: spaCy processed document (may be None)
130
-
243
+
131
244
  Returns:
132
245
  Dictionary with intent analysis results
133
246
  """
134
247
  if not message:
135
248
  return {
136
- 'urgency': 'routine',
137
- 'confidence': 0.0,
138
- 'tone': 'neutral',
139
- 'planning_stage': 'implementation',
140
- 'collaboration_signals': [],
141
- 'complexity': 'moderate',
142
- 'signals': []
249
+ "urgency": "routine",
250
+ "confidence": 0.0,
251
+ "tone": "neutral",
252
+ "planning_stage": "implementation",
253
+ "collaboration_signals": [],
254
+ "complexity": "moderate",
255
+ "signals": [],
143
256
  }
144
-
257
+
145
258
  message_lower = message.lower()
146
-
259
+
147
260
  # Extract all signals
148
261
  urgency = self._analyze_urgency(message_lower)
149
262
  confidence_info = self._analyze_confidence(message_lower, doc)
@@ -151,105 +264,101 @@ class IntentAnalyzer:
151
264
  planning = self._analyze_planning_stage(message_lower)
152
265
  collaboration = self._analyze_collaboration(message_lower)
153
266
  complexity = self._analyze_complexity(message_lower)
154
-
267
+
155
268
  # Collect all detected signals
156
269
  all_signals = []
157
- all_signals.extend(urgency.get('signals', []))
158
- all_signals.extend(confidence_info.get('signals', []))
159
- all_signals.extend(tone.get('signals', []))
160
- all_signals.extend(planning.get('signals', []))
161
- all_signals.extend(collaboration.get('signals', []))
162
- all_signals.extend(complexity.get('signals', []))
163
-
270
+ all_signals.extend(urgency.get("signals", []))
271
+ all_signals.extend(confidence_info.get("signals", []))
272
+ all_signals.extend(tone.get("signals", []))
273
+ all_signals.extend(planning.get("signals", []))
274
+ all_signals.extend(collaboration.get("signals", []))
275
+ all_signals.extend(complexity.get("signals", []))
276
+
164
277
  return {
165
- 'urgency': urgency['level'],
166
- 'confidence': confidence_info['score'],
167
- 'tone': tone['dominant_tone'],
168
- 'planning_stage': planning['stage'],
169
- 'collaboration_signals': collaboration['types'],
170
- 'complexity': complexity['level'],
171
- 'signals': all_signals,
172
- 'detailed_analysis': {
173
- 'urgency_breakdown': urgency,
174
- 'confidence_breakdown': confidence_info,
175
- 'tone_breakdown': tone,
176
- 'planning_breakdown': planning,
177
- 'collaboration_breakdown': collaboration,
178
- 'complexity_breakdown': complexity
179
- }
278
+ "urgency": urgency["level"],
279
+ "confidence": confidence_info["score"],
280
+ "tone": tone["dominant_tone"],
281
+ "planning_stage": planning["stage"],
282
+ "collaboration_signals": collaboration["types"],
283
+ "complexity": complexity["level"],
284
+ "signals": all_signals,
285
+ "detailed_analysis": {
286
+ "urgency_breakdown": urgency,
287
+ "confidence_breakdown": confidence_info,
288
+ "tone_breakdown": tone,
289
+ "planning_breakdown": planning,
290
+ "collaboration_breakdown": collaboration,
291
+ "complexity_breakdown": complexity,
292
+ },
180
293
  }
181
-
182
- def _analyze_urgency(self, message: str) -> Dict[str, Any]:
294
+
295
+ def _analyze_urgency(self, message: str) -> dict[str, Any]:
183
296
  """Analyze urgency level from message content.
184
-
297
+
185
298
  Args:
186
299
  message: Lowercase commit message
187
-
300
+
188
301
  Returns:
189
302
  Dictionary with urgency analysis
190
303
  """
191
304
  signals = []
192
305
  urgency_scores = defaultdict(float)
193
-
306
+
194
307
  # Check configured urgency keywords
195
308
  for urgency_level, keywords in self.urgency_keywords.items():
196
309
  for keyword in keywords:
197
310
  if keyword.lower() in message:
198
311
  signals.append(f"urgency:{urgency_level}:{keyword}")
199
312
  urgency_scores[urgency_level] += 1.0
200
-
313
+
201
314
  # Additional urgency patterns
202
315
  urgent_patterns = [
203
- (r'\b(urgent|critical|emergency|asap|immediate)\b', 'critical', 2.0),
204
- (r'\b(important|priority|needed|required)\b', 'important', 1.5),
205
- (r'\b(hotfix|quickfix|patch)\b', 'critical', 2.0),
206
- (r'\b(breaking|major)\b', 'important', 1.5),
207
- (r'\b(minor|small|tiny)\b', 'routine', 0.5),
316
+ (r"\b(urgent|critical|emergency|asap|immediate)\b", "critical", 2.0),
317
+ (r"\b(important|priority|needed|required)\b", "important", 1.5),
318
+ (r"\b(hotfix|quickfix|patch)\b", "critical", 2.0),
319
+ (r"\b(breaking|major)\b", "important", 1.5),
320
+ (r"\b(minor|small|tiny)\b", "routine", 0.5),
208
321
  ]
209
-
322
+
210
323
  for pattern, level, weight in urgent_patterns:
211
324
  if re.search(pattern, message):
212
325
  signals.append(f"urgency_pattern:{level}:{pattern}")
213
326
  urgency_scores[level] += weight
214
-
327
+
215
328
  # Determine dominant urgency level
216
329
  if urgency_scores:
217
330
  dominant_urgency = max(urgency_scores.keys(), key=lambda k: urgency_scores[k])
218
331
  else:
219
- dominant_urgency = 'routine'
220
-
221
- return {
222
- 'level': dominant_urgency,
223
- 'scores': dict(urgency_scores),
224
- 'signals': signals
225
- }
226
-
227
- def _analyze_confidence(self, message: str, doc: Doc) -> Dict[str, Any]:
332
+ dominant_urgency = "routine"
333
+
334
+ return {"level": dominant_urgency, "scores": dict(urgency_scores), "signals": signals}
335
+
336
+ def _analyze_confidence(self, message: str, doc: Doc) -> dict[str, Any]:
228
337
  """Analyze confidence level in the commit.
229
-
338
+
230
339
  Args:
231
340
  message: Lowercase commit message
232
341
  doc: spaCy processed document
233
-
342
+
234
343
  Returns:
235
344
  Dictionary with confidence analysis
236
345
  """
237
346
  signals = []
238
347
  confidence_score = 0.5 # Start with neutral confidence
239
-
348
+
240
349
  # Check confidence indicators
241
350
  for confidence_type, keywords in self.confidence_indicators.items():
242
351
  matches = sum(1 for keyword in keywords if keyword in message)
243
352
  if matches > 0:
244
353
  signals.append(f"confidence:{confidence_type}:{matches}")
245
-
246
- if confidence_type == 'high_confidence':
354
+
355
+ if confidence_type == "high_confidence":
247
356
  confidence_score += matches * 0.2
248
- elif confidence_type == 'low_confidence':
357
+ elif confidence_type == "low_confidence":
249
358
  confidence_score -= matches * 0.15
250
- elif confidence_type == 'uncertain':
359
+ elif confidence_type == "uncertain":
251
360
  confidence_score -= matches * 0.25
252
-
361
+
253
362
  # Check message structure and completeness
254
363
  if len(message.split()) >= 5: # Detailed message
255
364
  confidence_score += 0.1
@@ -257,180 +366,170 @@ class IntentAnalyzer:
257
366
  elif len(message.split()) <= 2: # Very brief message
258
367
  confidence_score -= 0.1
259
368
  signals.append("confidence:brief_message")
260
-
369
+
261
370
  # Check for question marks (uncertainty)
262
- if '?' in message:
371
+ if "?" in message:
263
372
  confidence_score -= 0.2
264
373
  signals.append("confidence:contains_question")
265
-
374
+
266
375
  # Check for ellipsis or incomplete thoughts
267
- if '...' in message or message.endswith('.'):
376
+ if "..." in message or message.endswith("."):
268
377
  confidence_score -= 0.1
269
378
  signals.append("confidence:incomplete_thought")
270
-
379
+
271
380
  # Normalize confidence score
272
381
  confidence_score = max(0.0, min(1.0, confidence_score))
273
-
382
+
274
383
  return {
275
- 'score': confidence_score,
276
- 'level': 'high' if confidence_score > 0.7 else 'medium' if confidence_score > 0.4 else 'low',
277
- 'signals': signals
384
+ "score": confidence_score,
385
+ "level": (
386
+ "high" if confidence_score > 0.7 else "medium" if confidence_score > 0.4 else "low"
387
+ ),
388
+ "signals": signals,
278
389
  }
279
-
280
- def _analyze_tone(self, message: str) -> Dict[str, Any]:
390
+
391
+ def _analyze_tone(self, message: str) -> dict[str, Any]:
281
392
  """Analyze emotional tone of the commit message.
282
-
393
+
283
394
  Args:
284
395
  message: Lowercase commit message
285
-
396
+
286
397
  Returns:
287
398
  Dictionary with tone analysis
288
399
  """
289
400
  signals = []
290
401
  tone_scores = defaultdict(float)
291
-
402
+
292
403
  # Check tone indicators
293
404
  for tone_type, keywords in self.tone_indicators.items():
294
405
  matches = sum(1 for keyword in keywords if keyword in message)
295
406
  if matches > 0:
296
407
  signals.append(f"tone:{tone_type}:{matches}")
297
408
  tone_scores[tone_type] += matches
298
-
409
+
299
410
  # Check punctuation for tone
300
- if '!' in message:
301
- tone_scores['confident'] += 0.5
411
+ if "!" in message:
412
+ tone_scores["confident"] += 0.5
302
413
  signals.append("tone:exclamation_mark")
303
- elif '...' in message:
304
- tone_scores['cautious'] += 0.5
414
+ elif "..." in message:
415
+ tone_scores["cautious"] += 0.5
305
416
  signals.append("tone:ellipsis")
306
-
417
+
307
418
  # Determine dominant tone
308
419
  if tone_scores:
309
420
  dominant_tone = max(tone_scores.keys(), key=lambda k: tone_scores[k])
310
421
  else:
311
- dominant_tone = 'neutral'
312
-
313
- return {
314
- 'dominant_tone': dominant_tone,
315
- 'scores': dict(tone_scores),
316
- 'signals': signals
317
- }
318
-
319
- def _analyze_planning_stage(self, message: str) -> Dict[str, Any]:
422
+ dominant_tone = "neutral"
423
+
424
+ return {"dominant_tone": dominant_tone, "scores": dict(tone_scores), "signals": signals}
425
+
426
+ def _analyze_planning_stage(self, message: str) -> dict[str, Any]:
320
427
  """Analyze what stage of planning/development this commit represents.
321
-
428
+
322
429
  Args:
323
430
  message: Lowercase commit message
324
-
431
+
325
432
  Returns:
326
433
  Dictionary with planning stage analysis
327
434
  """
328
435
  signals = []
329
436
  stage_scores = defaultdict(float)
330
-
437
+
331
438
  # Check planning indicators
332
439
  for stage_type, keywords in self.planning_indicators.items():
333
440
  matches = sum(1 for keyword in keywords if keyword in message)
334
441
  if matches > 0:
335
442
  signals.append(f"planning:{stage_type}:{matches}")
336
443
  stage_scores[stage_type] += matches
337
-
444
+
338
445
  # Additional stage indicators
339
- if any(word in message for word in ['start', 'initial', 'begin', 'setup']):
340
- stage_scores['initial'] = stage_scores.get('initial', 0) + 1
446
+ if any(word in message for word in ["start", "initial", "begin", "setup"]):
447
+ stage_scores["initial"] = stage_scores.get("initial", 0) + 1
341
448
  signals.append("planning:initial_stage")
342
-
343
- if any(word in message for word in ['complete', 'finish', 'done', 'final']):
344
- stage_scores['completion'] = stage_scores.get('completion', 0) + 1
449
+
450
+ if any(word in message for word in ["complete", "finish", "done", "final"]):
451
+ stage_scores["completion"] = stage_scores.get("completion", 0) + 1
345
452
  signals.append("planning:completion_stage")
346
-
453
+
347
454
  # Determine stage
348
455
  if stage_scores:
349
- if 'immediate' in stage_scores:
350
- stage = 'immediate'
351
- elif 'todo' in stage_scores:
352
- stage = 'planning'
353
- elif 'future_work' in stage_scores:
354
- stage = 'future_planning'
355
- elif 'completion' in stage_scores:
356
- stage = 'completion'
357
- elif 'initial' in stage_scores:
358
- stage = 'initiation'
456
+ if "immediate" in stage_scores:
457
+ stage = "immediate"
458
+ elif "todo" in stage_scores:
459
+ stage = "planning"
460
+ elif "future_work" in stage_scores:
461
+ stage = "future_planning"
462
+ elif "completion" in stage_scores:
463
+ stage = "completion"
464
+ elif "initial" in stage_scores:
465
+ stage = "initiation"
359
466
  else:
360
- stage = 'implementation'
467
+ stage = "implementation"
361
468
  else:
362
- stage = 'implementation'
363
-
364
- return {
365
- 'stage': stage,
366
- 'scores': dict(stage_scores),
367
- 'signals': signals
368
- }
369
-
370
- def _analyze_collaboration(self, message: str) -> Dict[str, Any]:
469
+ stage = "implementation"
470
+
471
+ return {"stage": stage, "scores": dict(stage_scores), "signals": signals}
472
+
473
+ def _analyze_collaboration(self, message: str) -> dict[str, Any]:
371
474
  """Analyze collaboration signals in the commit message.
372
-
475
+
373
476
  Args:
374
477
  message: Lowercase commit message
375
-
478
+
376
479
  Returns:
377
480
  Dictionary with collaboration analysis
378
481
  """
379
482
  signals = []
380
483
  collaboration_types = []
381
-
484
+
382
485
  # Check collaboration indicators
383
486
  for collab_type, keywords in self.collaboration_indicators.items():
384
487
  matches = [keyword for keyword in keywords if keyword in message]
385
488
  if matches:
386
489
  signals.extend([f"collaboration:{collab_type}:{match}" for match in matches])
387
490
  collaboration_types.append(collab_type)
388
-
491
+
389
492
  # Check for co-author patterns
390
- if 'co-authored-by:' in message or 'with @' in message:
391
- collaboration_types.append('co_authored')
493
+ if "co-authored-by:" in message or "with @" in message:
494
+ collaboration_types.append("co_authored")
392
495
  signals.append("collaboration:co_authored")
393
-
496
+
394
497
  return {
395
- 'types': collaboration_types,
396
- 'signals': signals,
397
- 'is_collaborative': len(collaboration_types) > 0
498
+ "types": collaboration_types,
499
+ "signals": signals,
500
+ "is_collaborative": len(collaboration_types) > 0,
398
501
  }
399
-
400
- def _analyze_complexity(self, message: str) -> Dict[str, Any]:
502
+
503
+ def _analyze_complexity(self, message: str) -> dict[str, Any]:
401
504
  """Analyze technical complexity signals in the commit message.
402
-
505
+
403
506
  Args:
404
507
  message: Lowercase commit message
405
-
508
+
406
509
  Returns:
407
510
  Dictionary with complexity analysis
408
511
  """
409
512
  signals = []
410
513
  complexity_scores = defaultdict(float)
411
-
514
+
412
515
  # Check complexity indicators
413
516
  for complexity_type, keywords in self.complexity_indicators.items():
414
517
  matches = sum(1 for keyword in keywords if keyword in message)
415
518
  if matches > 0:
416
519
  signals.append(f"complexity:{complexity_type}:{matches}")
417
520
  complexity_scores[complexity_type] += matches
418
-
521
+
419
522
  # Determine complexity level
420
523
  if complexity_scores:
421
- if complexity_scores.get('complex', 0) > complexity_scores.get('simple', 0):
422
- level = 'complex'
423
- elif complexity_scores.get('simple', 0) > 0:
424
- level = 'simple'
425
- elif complexity_scores.get('refactoring', 0) > 0:
426
- level = 'moderate' # Refactoring is usually moderate complexity
524
+ if complexity_scores.get("complex", 0) > complexity_scores.get("simple", 0):
525
+ level = "complex"
526
+ elif complexity_scores.get("simple", 0) > 0:
527
+ level = "simple"
528
+ elif complexity_scores.get("refactoring", 0) > 0:
529
+ level = "moderate" # Refactoring is usually moderate complexity
427
530
  else:
428
- level = 'moderate'
531
+ level = "moderate"
429
532
  else:
430
- level = 'moderate'
431
-
432
- return {
433
- 'level': level,
434
- 'scores': dict(complexity_scores),
435
- 'signals': signals
436
- }
533
+ level = "moderate"
534
+
535
+ return {"level": level, "scores": dict(complexity_scores), "signals": signals}