gitflow-analytics 1.0.1__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 -11
- gitflow_analytics/_version.py +2 -2
- gitflow_analytics/cli.py +612 -258
- gitflow_analytics/cli_rich.py +353 -0
- gitflow_analytics/config.py +251 -141
- gitflow_analytics/core/analyzer.py +140 -103
- gitflow_analytics/core/branch_mapper.py +132 -132
- gitflow_analytics/core/cache.py +240 -169
- gitflow_analytics/core/identity.py +210 -173
- gitflow_analytics/extractors/base.py +13 -11
- gitflow_analytics/extractors/story_points.py +70 -59
- gitflow_analytics/extractors/tickets.py +101 -87
- gitflow_analytics/integrations/github_integration.py +84 -77
- gitflow_analytics/integrations/jira_integration.py +116 -104
- gitflow_analytics/integrations/orchestrator.py +86 -85
- gitflow_analytics/metrics/dora.py +181 -177
- gitflow_analytics/models/database.py +190 -53
- 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 +11 -4
- gitflow_analytics/reports/csv_writer.py +51 -31
- gitflow_analytics/reports/narrative_writer.py +16 -14
- 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.1.dist-info → gitflow_analytics-1.0.3.dist-info}/METADATA +31 -4
- gitflow_analytics-1.0.3.dist-info/RECORD +62 -0
- gitflow_analytics-1.0.1.dist-info/RECORD +0 -31
- {gitflow_analytics-1.0.1.dist-info → gitflow_analytics-1.0.3.dist-info}/WHEEL +0 -0
- {gitflow_analytics-1.0.1.dist-info → gitflow_analytics-1.0.3.dist-info}/entry_points.txt +0 -0
- {gitflow_analytics-1.0.1.dist-info → gitflow_analytics-1.0.3.dist-info}/licenses/LICENSE +0 -0
- {gitflow_analytics-1.0.1.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 Any,
|
|
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,39 +1,40 @@
|
|
|
1
1
|
"""Ticket reference extraction for multiple platforms."""
|
|
2
|
+
|
|
2
3
|
import re
|
|
3
4
|
from collections import defaultdict
|
|
4
|
-
from typing import Any,
|
|
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, allowed_platforms=None):
|
|
10
|
+
|
|
11
|
+
def __init__(self, allowed_platforms: Optional[list[str]] = None) -> None:
|
|
11
12
|
"""Initialize with patterns for different platforms.
|
|
12
|
-
|
|
13
|
+
|
|
13
14
|
Args:
|
|
14
15
|
allowed_platforms: List of platforms to extract tickets from.
|
|
15
16
|
If None, all platforms are allowed.
|
|
16
17
|
"""
|
|
17
18
|
self.allowed_platforms = allowed_platforms
|
|
18
19
|
self.patterns = {
|
|
19
|
-
|
|
20
|
-
r
|
|
20
|
+
"jira": [
|
|
21
|
+
r"([A-Z]{2,10}-\d+)", # Standard JIRA format: PROJ-123
|
|
21
22
|
],
|
|
22
|
-
|
|
23
|
-
r
|
|
24
|
-
r
|
|
25
|
-
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+)",
|
|
26
27
|
],
|
|
27
|
-
|
|
28
|
-
r
|
|
29
|
-
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
|
|
30
35
|
],
|
|
31
|
-
'linear': [
|
|
32
|
-
r'([A-Z]{2,5}-\d+)', # Linear: ENG-123, similar to JIRA
|
|
33
|
-
r'LIN-(\d+)', # Alternative: LIN-123
|
|
34
|
-
]
|
|
35
36
|
}
|
|
36
|
-
|
|
37
|
+
|
|
37
38
|
# Compile patterns only for allowed platforms
|
|
38
39
|
self.compiled_patterns = {}
|
|
39
40
|
for platform, patterns in self.patterns.items():
|
|
@@ -41,126 +42,139 @@ class TicketExtractor:
|
|
|
41
42
|
if self.allowed_platforms and platform not in self.allowed_platforms:
|
|
42
43
|
continue
|
|
43
44
|
self.compiled_patterns[platform] = [
|
|
44
|
-
re.compile(pattern, re.IGNORECASE if platform !=
|
|
45
|
+
re.compile(pattern, re.IGNORECASE if platform != "jira" else 0)
|
|
45
46
|
for pattern in patterns
|
|
46
47
|
]
|
|
47
|
-
|
|
48
|
-
def extract_from_text(self, text: str) ->
|
|
48
|
+
|
|
49
|
+
def extract_from_text(self, text: str) -> list[dict[str, str]]:
|
|
49
50
|
"""Extract all ticket references from text."""
|
|
50
51
|
if not text:
|
|
51
52
|
return []
|
|
52
|
-
|
|
53
|
+
|
|
53
54
|
tickets = []
|
|
54
55
|
seen = set() # Avoid duplicates
|
|
55
|
-
|
|
56
|
+
|
|
56
57
|
for platform, patterns in self.compiled_patterns.items():
|
|
57
58
|
for pattern in patterns:
|
|
58
59
|
matches = pattern.findall(text)
|
|
59
60
|
for match in matches:
|
|
60
61
|
ticket_id = match if isinstance(match, str) else match[0]
|
|
61
|
-
|
|
62
|
+
|
|
62
63
|
# Normalize ticket ID
|
|
63
|
-
if platform ==
|
|
64
|
+
if platform == "jira" or platform == "linear":
|
|
64
65
|
ticket_id = ticket_id.upper()
|
|
65
|
-
|
|
66
|
+
|
|
66
67
|
# Create unique key
|
|
67
68
|
key = f"{platform}:{ticket_id}"
|
|
68
69
|
if key not in seen:
|
|
69
70
|
seen.add(key)
|
|
70
|
-
tickets.append(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
71
|
+
tickets.append(
|
|
72
|
+
{
|
|
73
|
+
"platform": platform,
|
|
74
|
+
"id": ticket_id,
|
|
75
|
+
"full_id": self._format_ticket_id(platform, ticket_id),
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
|
|
76
79
|
return tickets
|
|
77
|
-
|
|
78
|
-
def extract_by_platform(self, text: str) ->
|
|
80
|
+
|
|
81
|
+
def extract_by_platform(self, text: str) -> dict[str, list[str]]:
|
|
79
82
|
"""Extract tickets grouped by platform."""
|
|
80
83
|
tickets = self.extract_from_text(text)
|
|
81
|
-
|
|
84
|
+
|
|
82
85
|
by_platform = defaultdict(list)
|
|
83
86
|
for ticket in tickets:
|
|
84
|
-
by_platform[ticket[
|
|
85
|
-
|
|
87
|
+
by_platform[ticket["platform"]].append(ticket["id"])
|
|
88
|
+
|
|
86
89
|
return dict(by_platform)
|
|
87
|
-
|
|
88
|
-
def analyze_ticket_coverage(
|
|
89
|
-
|
|
90
|
+
|
|
91
|
+
def analyze_ticket_coverage(
|
|
92
|
+
self, commits: list[dict[str, Any]], prs: list[dict[str, Any]]
|
|
93
|
+
) -> dict[str, Any]:
|
|
90
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
|
+
|
|
91
99
|
results = {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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,
|
|
99
107
|
}
|
|
100
|
-
|
|
108
|
+
|
|
101
109
|
# Analyze commits
|
|
102
110
|
for commit in commits:
|
|
103
|
-
ticket_refs = commit.get(
|
|
111
|
+
ticket_refs = commit.get("ticket_references", [])
|
|
104
112
|
if ticket_refs:
|
|
105
|
-
results[
|
|
113
|
+
commits_with_tickets = cast(int, results["commits_with_tickets"])
|
|
114
|
+
results["commits_with_tickets"] = commits_with_tickets + 1
|
|
106
115
|
for ticket in ticket_refs:
|
|
107
116
|
if isinstance(ticket, dict):
|
|
108
|
-
platform = ticket.get(
|
|
109
|
-
ticket_id = ticket.get(
|
|
117
|
+
platform = ticket.get("platform", "unknown")
|
|
118
|
+
ticket_id = ticket.get("id", "")
|
|
110
119
|
else:
|
|
111
120
|
# Legacy format - assume JIRA
|
|
112
|
-
platform =
|
|
121
|
+
platform = "jira"
|
|
113
122
|
ticket_id = ticket
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
123
|
+
|
|
124
|
+
platform_count = ticket_platforms[platform]
|
|
125
|
+
ticket_platforms[platform] = platform_count + 1
|
|
126
|
+
ticket_summary[platform].add(ticket_id)
|
|
117
127
|
else:
|
|
118
128
|
# Track significant untracked commits
|
|
119
|
-
if
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
+
|
|
127
138
|
# Analyze PRs
|
|
128
139
|
for pr in prs:
|
|
129
140
|
# Extract tickets from PR title and description
|
|
130
141
|
pr_text = f"{pr.get('title', '')} {pr.get('description', '')}"
|
|
131
142
|
tickets = self.extract_from_text(pr_text)
|
|
132
|
-
|
|
143
|
+
|
|
133
144
|
if tickets:
|
|
134
|
-
results[
|
|
145
|
+
prs_with_tickets = cast(int, results["prs_with_tickets"])
|
|
146
|
+
results["prs_with_tickets"] = prs_with_tickets + 1
|
|
135
147
|
for ticket in tickets:
|
|
136
|
-
platform = ticket[
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
+
|
|
140
153
|
# Calculate coverage percentages
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
|
144
158
|
)
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
|
149
164
|
)
|
|
150
|
-
|
|
165
|
+
|
|
151
166
|
# Convert sets to counts for summary
|
|
152
|
-
results[
|
|
153
|
-
platform: len(tickets)
|
|
154
|
-
for platform, tickets in results['ticket_summary'].items()
|
|
167
|
+
results["ticket_summary"] = {
|
|
168
|
+
platform: len(tickets) for platform, tickets in ticket_summary.items()
|
|
155
169
|
}
|
|
156
|
-
|
|
170
|
+
|
|
157
171
|
return results
|
|
158
|
-
|
|
172
|
+
|
|
159
173
|
def _format_ticket_id(self, platform: str, ticket_id: str) -> str:
|
|
160
174
|
"""Format ticket ID for display."""
|
|
161
|
-
if platform ==
|
|
175
|
+
if platform == "github":
|
|
162
176
|
return f"#{ticket_id}"
|
|
163
|
-
elif platform ==
|
|
164
|
-
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
|
|
165
179
|
else:
|
|
166
|
-
return ticket_id
|
|
180
|
+
return ticket_id
|