gitflow-analytics 1.0.0__py3-none-any.whl → 1.0.3__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.
- gitflow_analytics/__init__.py +11 -9
- gitflow_analytics/_version.py +2 -2
- gitflow_analytics/cli.py +691 -243
- gitflow_analytics/cli_rich.py +353 -0
- gitflow_analytics/config.py +389 -96
- gitflow_analytics/core/analyzer.py +175 -78
- gitflow_analytics/core/branch_mapper.py +132 -132
- gitflow_analytics/core/cache.py +242 -173
- gitflow_analytics/core/identity.py +214 -178
- gitflow_analytics/extractors/base.py +13 -11
- gitflow_analytics/extractors/story_points.py +70 -59
- gitflow_analytics/extractors/tickets.py +111 -88
- gitflow_analytics/integrations/github_integration.py +91 -77
- gitflow_analytics/integrations/jira_integration.py +284 -0
- gitflow_analytics/integrations/orchestrator.py +99 -72
- gitflow_analytics/metrics/dora.py +183 -179
- gitflow_analytics/models/database.py +191 -54
- gitflow_analytics/qualitative/__init__.py +30 -0
- gitflow_analytics/qualitative/classifiers/__init__.py +13 -0
- gitflow_analytics/qualitative/classifiers/change_type.py +468 -0
- gitflow_analytics/qualitative/classifiers/domain_classifier.py +399 -0
- gitflow_analytics/qualitative/classifiers/intent_analyzer.py +436 -0
- gitflow_analytics/qualitative/classifiers/risk_analyzer.py +412 -0
- gitflow_analytics/qualitative/core/__init__.py +13 -0
- gitflow_analytics/qualitative/core/llm_fallback.py +653 -0
- gitflow_analytics/qualitative/core/nlp_engine.py +373 -0
- gitflow_analytics/qualitative/core/pattern_cache.py +457 -0
- gitflow_analytics/qualitative/core/processor.py +540 -0
- gitflow_analytics/qualitative/models/__init__.py +25 -0
- gitflow_analytics/qualitative/models/schemas.py +272 -0
- gitflow_analytics/qualitative/utils/__init__.py +13 -0
- gitflow_analytics/qualitative/utils/batch_processor.py +326 -0
- gitflow_analytics/qualitative/utils/cost_tracker.py +343 -0
- gitflow_analytics/qualitative/utils/metrics.py +347 -0
- gitflow_analytics/qualitative/utils/text_processing.py +243 -0
- gitflow_analytics/reports/analytics_writer.py +25 -8
- gitflow_analytics/reports/csv_writer.py +60 -32
- gitflow_analytics/reports/narrative_writer.py +21 -15
- gitflow_analytics/tui/__init__.py +5 -0
- gitflow_analytics/tui/app.py +721 -0
- gitflow_analytics/tui/screens/__init__.py +8 -0
- gitflow_analytics/tui/screens/analysis_progress_screen.py +487 -0
- gitflow_analytics/tui/screens/configuration_screen.py +547 -0
- gitflow_analytics/tui/screens/loading_screen.py +358 -0
- gitflow_analytics/tui/screens/main_screen.py +304 -0
- gitflow_analytics/tui/screens/results_screen.py +698 -0
- gitflow_analytics/tui/widgets/__init__.py +7 -0
- gitflow_analytics/tui/widgets/data_table.py +257 -0
- gitflow_analytics/tui/widgets/export_modal.py +301 -0
- gitflow_analytics/tui/widgets/progress_widget.py +192 -0
- gitflow_analytics-1.0.3.dist-info/METADATA +490 -0
- gitflow_analytics-1.0.3.dist-info/RECORD +62 -0
- gitflow_analytics-1.0.0.dist-info/METADATA +0 -201
- gitflow_analytics-1.0.0.dist-info/RECORD +0 -30
- {gitflow_analytics-1.0.0.dist-info → gitflow_analytics-1.0.3.dist-info}/WHEEL +0 -0
- {gitflow_analytics-1.0.0.dist-info → gitflow_analytics-1.0.3.dist-info}/entry_points.txt +0 -0
- {gitflow_analytics-1.0.0.dist-info → gitflow_analytics-1.0.3.dist-info}/licenses/LICENSE +0 -0
- {gitflow_analytics-1.0.0.dist-info → gitflow_analytics-1.0.3.dist-info}/top_level.txt +0 -0
|
@@ -1,30 +1,31 @@
|
|
|
1
1
|
"""Story point extraction from commits and pull requests."""
|
|
2
|
+
|
|
2
3
|
import re
|
|
3
|
-
from typing import
|
|
4
|
+
from typing import Any, Optional
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
class StoryPointExtractor:
|
|
7
8
|
"""Extract story points from text using configurable patterns."""
|
|
8
|
-
|
|
9
|
-
def __init__(self, patterns: Optional[
|
|
9
|
+
|
|
10
|
+
def __init__(self, patterns: Optional[list[str]] = None):
|
|
10
11
|
"""Initialize with extraction patterns."""
|
|
11
12
|
if patterns is None:
|
|
12
13
|
patterns = [
|
|
13
|
-
r
|
|
14
|
-
r
|
|
15
|
-
r
|
|
16
|
-
r
|
|
17
|
-
r
|
|
18
|
-
r
|
|
14
|
+
r"(?:story\s*points?|sp|pts?)\s*[:=]\s*(\d+)", # SP: 5, story points = 3
|
|
15
|
+
r"\[(\d+)\s*(?:sp|pts?)\]", # [3sp], [5 pts]
|
|
16
|
+
r"#(\d+)sp", # #3sp
|
|
17
|
+
r"estimate:\s*(\d+)", # estimate: 5
|
|
18
|
+
r"\bSP(\d+)\b", # SP5, SP13
|
|
19
|
+
r"points?:\s*(\d+)", # points: 8
|
|
19
20
|
]
|
|
20
|
-
|
|
21
|
+
|
|
21
22
|
self.patterns = [re.compile(pattern, re.IGNORECASE) for pattern in patterns]
|
|
22
|
-
|
|
23
|
+
|
|
23
24
|
def extract_from_text(self, text: str) -> Optional[int]:
|
|
24
25
|
"""Extract story points from text."""
|
|
25
26
|
if not text:
|
|
26
27
|
return None
|
|
27
|
-
|
|
28
|
+
|
|
28
29
|
for pattern in self.patterns:
|
|
29
30
|
match = pattern.search(text)
|
|
30
31
|
if match:
|
|
@@ -35,28 +36,29 @@ class StoryPointExtractor:
|
|
|
35
36
|
return points
|
|
36
37
|
except (ValueError, IndexError):
|
|
37
38
|
continue
|
|
38
|
-
|
|
39
|
+
|
|
39
40
|
return None
|
|
40
|
-
|
|
41
|
-
def extract_from_pr(
|
|
42
|
-
|
|
41
|
+
|
|
42
|
+
def extract_from_pr(
|
|
43
|
+
self, pr_data: dict[str, Any], commit_messages: Optional[list[str]] = None
|
|
44
|
+
) -> Optional[int]:
|
|
43
45
|
"""Extract story points from PR with fallback to commits."""
|
|
44
46
|
# Try PR description first (most authoritative)
|
|
45
|
-
points = self.extract_from_text(pr_data.get(
|
|
47
|
+
points = self.extract_from_text(pr_data.get("description", ""))
|
|
46
48
|
if points:
|
|
47
49
|
return points
|
|
48
|
-
|
|
50
|
+
|
|
49
51
|
# Try PR title
|
|
50
|
-
points = self.extract_from_text(pr_data.get(
|
|
52
|
+
points = self.extract_from_text(pr_data.get("title", ""))
|
|
51
53
|
if points:
|
|
52
54
|
return points
|
|
53
|
-
|
|
55
|
+
|
|
54
56
|
# Try PR body (if different from description)
|
|
55
|
-
if
|
|
56
|
-
points = self.extract_from_text(pr_data[
|
|
57
|
+
if "body" in pr_data:
|
|
58
|
+
points = self.extract_from_text(pr_data["body"])
|
|
57
59
|
if points:
|
|
58
60
|
return points
|
|
59
|
-
|
|
61
|
+
|
|
60
62
|
# Fallback to commit messages
|
|
61
63
|
if commit_messages:
|
|
62
64
|
commit_points = []
|
|
@@ -64,65 +66,74 @@ class StoryPointExtractor:
|
|
|
64
66
|
points = self.extract_from_text(message)
|
|
65
67
|
if points:
|
|
66
68
|
commit_points.append(points)
|
|
67
|
-
|
|
69
|
+
|
|
68
70
|
if commit_points:
|
|
69
71
|
# Use the most common value or max if no consensus
|
|
70
72
|
from collections import Counter
|
|
73
|
+
|
|
71
74
|
point_counts = Counter(commit_points)
|
|
72
75
|
most_common = point_counts.most_common(1)
|
|
73
76
|
if most_common:
|
|
74
77
|
return most_common[0][0]
|
|
75
|
-
|
|
78
|
+
|
|
76
79
|
return None
|
|
77
|
-
|
|
78
|
-
def aggregate_story_points(
|
|
79
|
-
|
|
80
|
+
|
|
81
|
+
def aggregate_story_points(
|
|
82
|
+
self, prs: list[dict[str, Any]], commits: list[dict[str, Any]]
|
|
83
|
+
) -> dict[str, Any]:
|
|
80
84
|
"""Aggregate story points from PRs and commits."""
|
|
81
85
|
# Map commits to PRs
|
|
82
86
|
pr_by_commit = {}
|
|
83
87
|
for pr in prs:
|
|
84
|
-
for commit_hash in pr.get(
|
|
88
|
+
for commit_hash in pr.get("commit_hashes", []):
|
|
85
89
|
pr_by_commit[commit_hash] = pr
|
|
86
|
-
|
|
90
|
+
|
|
87
91
|
# Track which commits are associated with PRs
|
|
88
92
|
pr_commits = set(pr_by_commit.keys())
|
|
89
|
-
|
|
93
|
+
|
|
90
94
|
# Aggregate results
|
|
95
|
+
orphan_commits: list[dict[str, Any]] = []
|
|
96
|
+
unestimated_prs: list[dict[str, Any]] = []
|
|
97
|
+
|
|
91
98
|
results = {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
99
|
+
"total_story_points": 0,
|
|
100
|
+
"pr_story_points": 0,
|
|
101
|
+
"commit_story_points": 0,
|
|
102
|
+
"orphan_commits": orphan_commits, # Commits without PRs
|
|
103
|
+
"unestimated_prs": unestimated_prs, # PRs without story points
|
|
97
104
|
}
|
|
98
|
-
|
|
105
|
+
|
|
99
106
|
# Process PRs
|
|
100
107
|
for pr in prs:
|
|
101
|
-
pr_points = pr.get(
|
|
108
|
+
pr_points = pr.get("story_points", 0)
|
|
102
109
|
if pr_points:
|
|
103
|
-
results[
|
|
104
|
-
results[
|
|
110
|
+
results["pr_story_points"] += pr_points
|
|
111
|
+
results["total_story_points"] += pr_points
|
|
105
112
|
else:
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
113
|
+
unestimated_prs.append(
|
|
114
|
+
{"number": pr.get("number", 0), "title": pr.get("title", "")}
|
|
115
|
+
)
|
|
116
|
+
|
|
111
117
|
# Process commits not in PRs
|
|
112
118
|
for commit in commits:
|
|
113
|
-
|
|
114
|
-
|
|
119
|
+
commit_hash = commit.get("hash", "")
|
|
120
|
+
if commit_hash not in pr_commits:
|
|
121
|
+
commit_points = commit.get("story_points", 0)
|
|
115
122
|
if commit_points:
|
|
116
|
-
results[
|
|
117
|
-
results[
|
|
118
|
-
|
|
123
|
+
results["commit_story_points"] += commit_points
|
|
124
|
+
results["total_story_points"] += commit_points
|
|
125
|
+
|
|
119
126
|
# Track significant orphan commits
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
127
|
+
files_changed = commit.get("files_changed", 0)
|
|
128
|
+
insertions = commit.get("insertions", 0)
|
|
129
|
+
if files_changed > 5 or insertions > 100:
|
|
130
|
+
orphan_commits.append(
|
|
131
|
+
{
|
|
132
|
+
"hash": commit.get("hash", "")[:7],
|
|
133
|
+
"message": commit.get("message", "").split("\n")[0][:80],
|
|
134
|
+
"story_points": commit_points,
|
|
135
|
+
"files_changed": files_changed,
|
|
136
|
+
}
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
return results
|
|
@@ -1,157 +1,180 @@
|
|
|
1
1
|
"""Ticket reference extraction for multiple platforms."""
|
|
2
|
+
|
|
2
3
|
import re
|
|
3
|
-
from typing import List, Dict, Any, Set
|
|
4
4
|
from collections import defaultdict
|
|
5
|
+
from typing import Any, Optional, cast
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
class TicketExtractor:
|
|
8
9
|
"""Extract ticket references from various issue tracking systems."""
|
|
9
|
-
|
|
10
|
-
def __init__(self):
|
|
11
|
-
"""Initialize with patterns for different platforms.
|
|
10
|
+
|
|
11
|
+
def __init__(self, allowed_platforms: Optional[list[str]] = None) -> None:
|
|
12
|
+
"""Initialize with patterns for different platforms.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
allowed_platforms: List of platforms to extract tickets from.
|
|
16
|
+
If None, all platforms are allowed.
|
|
17
|
+
"""
|
|
18
|
+
self.allowed_platforms = allowed_platforms
|
|
12
19
|
self.patterns = {
|
|
13
|
-
|
|
14
|
-
r
|
|
20
|
+
"jira": [
|
|
21
|
+
r"([A-Z]{2,10}-\d+)", # Standard JIRA format: PROJ-123
|
|
15
22
|
],
|
|
16
|
-
|
|
17
|
-
r
|
|
18
|
-
r
|
|
19
|
-
r
|
|
23
|
+
"github": [
|
|
24
|
+
r"#(\d+)", # GitHub issues: #123
|
|
25
|
+
r"GH-(\d+)", # Alternative format: GH-123
|
|
26
|
+
r"(?:fix|fixes|fixed|close|closes|closed|resolve|resolves|resolved)\s+#(\d+)",
|
|
20
27
|
],
|
|
21
|
-
|
|
22
|
-
r
|
|
23
|
-
r
|
|
28
|
+
"clickup": [
|
|
29
|
+
r"CU-([a-z0-9]+)", # ClickUp: CU-abc123
|
|
30
|
+
r"#([a-z0-9]{6,})", # ClickUp short format
|
|
31
|
+
],
|
|
32
|
+
"linear": [
|
|
33
|
+
r"([A-Z]{2,5}-\d+)", # Linear: ENG-123, similar to JIRA
|
|
34
|
+
r"LIN-(\d+)", # Alternative: LIN-123
|
|
24
35
|
],
|
|
25
|
-
'linear': [
|
|
26
|
-
r'([A-Z]{2,5}-\d+)', # Linear: ENG-123, similar to JIRA
|
|
27
|
-
r'LIN-(\d+)', # Alternative: LIN-123
|
|
28
|
-
]
|
|
29
36
|
}
|
|
30
|
-
|
|
31
|
-
# Compile patterns
|
|
37
|
+
|
|
38
|
+
# Compile patterns only for allowed platforms
|
|
32
39
|
self.compiled_patterns = {}
|
|
33
40
|
for platform, patterns in self.patterns.items():
|
|
41
|
+
# Skip platforms not in allowed list
|
|
42
|
+
if self.allowed_platforms and platform not in self.allowed_platforms:
|
|
43
|
+
continue
|
|
34
44
|
self.compiled_patterns[platform] = [
|
|
35
|
-
re.compile(pattern, re.IGNORECASE if platform !=
|
|
45
|
+
re.compile(pattern, re.IGNORECASE if platform != "jira" else 0)
|
|
36
46
|
for pattern in patterns
|
|
37
47
|
]
|
|
38
|
-
|
|
39
|
-
def extract_from_text(self, text: str) ->
|
|
48
|
+
|
|
49
|
+
def extract_from_text(self, text: str) -> list[dict[str, str]]:
|
|
40
50
|
"""Extract all ticket references from text."""
|
|
41
51
|
if not text:
|
|
42
52
|
return []
|
|
43
|
-
|
|
53
|
+
|
|
44
54
|
tickets = []
|
|
45
55
|
seen = set() # Avoid duplicates
|
|
46
|
-
|
|
56
|
+
|
|
47
57
|
for platform, patterns in self.compiled_patterns.items():
|
|
48
58
|
for pattern in patterns:
|
|
49
59
|
matches = pattern.findall(text)
|
|
50
60
|
for match in matches:
|
|
51
61
|
ticket_id = match if isinstance(match, str) else match[0]
|
|
52
|
-
|
|
62
|
+
|
|
53
63
|
# Normalize ticket ID
|
|
54
|
-
if platform ==
|
|
64
|
+
if platform == "jira" or platform == "linear":
|
|
55
65
|
ticket_id = ticket_id.upper()
|
|
56
|
-
|
|
66
|
+
|
|
57
67
|
# Create unique key
|
|
58
68
|
key = f"{platform}:{ticket_id}"
|
|
59
69
|
if key not in seen:
|
|
60
70
|
seen.add(key)
|
|
61
|
-
tickets.append(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
71
|
+
tickets.append(
|
|
72
|
+
{
|
|
73
|
+
"platform": platform,
|
|
74
|
+
"id": ticket_id,
|
|
75
|
+
"full_id": self._format_ticket_id(platform, ticket_id),
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
|
|
67
79
|
return tickets
|
|
68
|
-
|
|
69
|
-
def extract_by_platform(self, text: str) ->
|
|
80
|
+
|
|
81
|
+
def extract_by_platform(self, text: str) -> dict[str, list[str]]:
|
|
70
82
|
"""Extract tickets grouped by platform."""
|
|
71
83
|
tickets = self.extract_from_text(text)
|
|
72
|
-
|
|
84
|
+
|
|
73
85
|
by_platform = defaultdict(list)
|
|
74
86
|
for ticket in tickets:
|
|
75
|
-
by_platform[ticket[
|
|
76
|
-
|
|
87
|
+
by_platform[ticket["platform"]].append(ticket["id"])
|
|
88
|
+
|
|
77
89
|
return dict(by_platform)
|
|
78
|
-
|
|
79
|
-
def analyze_ticket_coverage(
|
|
80
|
-
|
|
90
|
+
|
|
91
|
+
def analyze_ticket_coverage(
|
|
92
|
+
self, commits: list[dict[str, Any]], prs: list[dict[str, Any]]
|
|
93
|
+
) -> dict[str, Any]:
|
|
81
94
|
"""Analyze ticket reference coverage across commits and PRs."""
|
|
95
|
+
ticket_platforms: defaultdict[str, int] = defaultdict(int)
|
|
96
|
+
untracked_commits: list[dict[str, Any]] = []
|
|
97
|
+
ticket_summary: defaultdict[str, set[str]] = defaultdict(set)
|
|
98
|
+
|
|
82
99
|
results = {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
100
|
+
"total_commits": len(commits),
|
|
101
|
+
"total_prs": len(prs),
|
|
102
|
+
"commits_with_tickets": 0,
|
|
103
|
+
"prs_with_tickets": 0,
|
|
104
|
+
"ticket_platforms": ticket_platforms,
|
|
105
|
+
"untracked_commits": untracked_commits,
|
|
106
|
+
"ticket_summary": ticket_summary,
|
|
90
107
|
}
|
|
91
|
-
|
|
108
|
+
|
|
92
109
|
# Analyze commits
|
|
93
110
|
for commit in commits:
|
|
94
|
-
ticket_refs = commit.get(
|
|
111
|
+
ticket_refs = commit.get("ticket_references", [])
|
|
95
112
|
if ticket_refs:
|
|
96
|
-
results[
|
|
113
|
+
commits_with_tickets = cast(int, results["commits_with_tickets"])
|
|
114
|
+
results["commits_with_tickets"] = commits_with_tickets + 1
|
|
97
115
|
for ticket in ticket_refs:
|
|
98
116
|
if isinstance(ticket, dict):
|
|
99
|
-
platform = ticket.get(
|
|
100
|
-
ticket_id = ticket.get(
|
|
117
|
+
platform = ticket.get("platform", "unknown")
|
|
118
|
+
ticket_id = ticket.get("id", "")
|
|
101
119
|
else:
|
|
102
120
|
# Legacy format - assume JIRA
|
|
103
|
-
platform =
|
|
121
|
+
platform = "jira"
|
|
104
122
|
ticket_id = ticket
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
123
|
+
|
|
124
|
+
platform_count = ticket_platforms[platform]
|
|
125
|
+
ticket_platforms[platform] = platform_count + 1
|
|
126
|
+
ticket_summary[platform].add(ticket_id)
|
|
108
127
|
else:
|
|
109
128
|
# Track significant untracked commits
|
|
110
|
-
if
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
129
|
+
if not commit.get("is_merge") and commit.get("files_changed", 0) > 3:
|
|
130
|
+
untracked_commits.append(
|
|
131
|
+
{
|
|
132
|
+
"hash": commit.get("hash", "")[:7],
|
|
133
|
+
"message": commit.get("message", "").split("\n")[0][:60],
|
|
134
|
+
"files_changed": commit.get("files_changed", 0),
|
|
135
|
+
}
|
|
136
|
+
)
|
|
137
|
+
|
|
118
138
|
# Analyze PRs
|
|
119
139
|
for pr in prs:
|
|
120
140
|
# Extract tickets from PR title and description
|
|
121
141
|
pr_text = f"{pr.get('title', '')} {pr.get('description', '')}"
|
|
122
142
|
tickets = self.extract_from_text(pr_text)
|
|
123
|
-
|
|
143
|
+
|
|
124
144
|
if tickets:
|
|
125
|
-
results[
|
|
145
|
+
prs_with_tickets = cast(int, results["prs_with_tickets"])
|
|
146
|
+
results["prs_with_tickets"] = prs_with_tickets + 1
|
|
126
147
|
for ticket in tickets:
|
|
127
|
-
platform = ticket[
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
148
|
+
platform = ticket["platform"]
|
|
149
|
+
platform_count = ticket_platforms[platform]
|
|
150
|
+
ticket_platforms[platform] = platform_count + 1
|
|
151
|
+
ticket_summary[platform].add(ticket["id"])
|
|
152
|
+
|
|
131
153
|
# Calculate coverage percentages
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
154
|
+
total_commits = cast(int, results["total_commits"])
|
|
155
|
+
commits_with_tickets_count = cast(int, results["commits_with_tickets"])
|
|
156
|
+
results["commit_coverage_pct"] = (
|
|
157
|
+
commits_with_tickets_count / total_commits * 100 if total_commits > 0 else 0
|
|
135
158
|
)
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
159
|
+
|
|
160
|
+
total_prs = cast(int, results["total_prs"])
|
|
161
|
+
prs_with_tickets_count = cast(int, results["prs_with_tickets"])
|
|
162
|
+
results["pr_coverage_pct"] = (
|
|
163
|
+
prs_with_tickets_count / total_prs * 100 if total_prs > 0 else 0
|
|
140
164
|
)
|
|
141
|
-
|
|
165
|
+
|
|
142
166
|
# Convert sets to counts for summary
|
|
143
|
-
results[
|
|
144
|
-
platform: len(tickets)
|
|
145
|
-
for platform, tickets in results['ticket_summary'].items()
|
|
167
|
+
results["ticket_summary"] = {
|
|
168
|
+
platform: len(tickets) for platform, tickets in ticket_summary.items()
|
|
146
169
|
}
|
|
147
|
-
|
|
170
|
+
|
|
148
171
|
return results
|
|
149
|
-
|
|
172
|
+
|
|
150
173
|
def _format_ticket_id(self, platform: str, ticket_id: str) -> str:
|
|
151
174
|
"""Format ticket ID for display."""
|
|
152
|
-
if platform ==
|
|
175
|
+
if platform == "github":
|
|
153
176
|
return f"#{ticket_id}"
|
|
154
|
-
elif platform ==
|
|
155
|
-
return f"CU-{ticket_id}" if not ticket_id.startswith(
|
|
177
|
+
elif platform == "clickup":
|
|
178
|
+
return f"CU-{ticket_id}" if not ticket_id.startswith("CU-") else ticket_id
|
|
156
179
|
else:
|
|
157
|
-
return ticket_id
|
|
180
|
+
return ticket_id
|