tapps-agents 3.5.38__py3-none-any.whl → 3.5.40__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.
- tapps_agents/__init__.py +2 -2
- tapps_agents/agents/cleanup/__init__.py +7 -0
- tapps_agents/agents/cleanup/agent.py +445 -0
- tapps_agents/agents/enhancer/agent.py +2 -2
- tapps_agents/agents/implementer/agent.py +35 -13
- tapps_agents/agents/reviewer/agent.py +43 -10
- tapps_agents/agents/reviewer/scoring.py +59 -68
- tapps_agents/agents/reviewer/tools/__init__.py +24 -0
- tapps_agents/agents/reviewer/tools/ruff_grouping.py +250 -0
- tapps_agents/agents/reviewer/tools/scoped_mypy.py +284 -0
- tapps_agents/beads/__init__.py +11 -0
- tapps_agents/beads/hydration.py +213 -0
- tapps_agents/beads/specs.py +206 -0
- tapps_agents/cli/commands/cleanup_agent.py +92 -0
- tapps_agents/cli/commands/health.py +19 -3
- tapps_agents/cli/commands/simple_mode.py +842 -676
- tapps_agents/cli/commands/task.py +219 -0
- tapps_agents/cli/commands/top_level.py +13 -0
- tapps_agents/cli/main.py +15 -2
- tapps_agents/cli/parsers/cleanup_agent.py +228 -0
- tapps_agents/cli/parsers/top_level.py +1978 -1881
- tapps_agents/core/config.py +43 -0
- tapps_agents/core/init_project.py +3012 -2896
- tapps_agents/epic/markdown_sync.py +105 -0
- tapps_agents/epic/orchestrator.py +1 -2
- tapps_agents/epic/parser.py +427 -423
- tapps_agents/experts/adaptive_domain_detector.py +0 -2
- tapps_agents/experts/knowledge/api-design-integration/api-security-patterns.md +15 -15
- tapps_agents/experts/knowledge/api-design-integration/external-api-integration.md +19 -44
- tapps_agents/health/checks/outcomes.backup_20260204_064058.py +324 -0
- tapps_agents/health/checks/outcomes.backup_20260204_064256.py +324 -0
- tapps_agents/health/checks/outcomes.backup_20260204_064600.py +324 -0
- tapps_agents/health/checks/outcomes.py +134 -46
- tapps_agents/health/orchestrator.py +12 -4
- tapps_agents/hooks/__init__.py +33 -0
- tapps_agents/hooks/config.py +140 -0
- tapps_agents/hooks/events.py +135 -0
- tapps_agents/hooks/executor.py +128 -0
- tapps_agents/hooks/manager.py +143 -0
- tapps_agents/session/__init__.py +19 -0
- tapps_agents/session/manager.py +256 -0
- tapps_agents/simple_mode/code_snippet_handler.py +382 -0
- tapps_agents/simple_mode/intent_parser.py +29 -4
- tapps_agents/simple_mode/orchestrators/base.py +185 -59
- tapps_agents/simple_mode/orchestrators/build_orchestrator.py +2667 -2642
- tapps_agents/simple_mode/orchestrators/fix_orchestrator.py +723 -723
- tapps_agents/simple_mode/workflow_suggester.py +37 -3
- tapps_agents/workflow/agent_handlers/implementer_handler.py +18 -3
- tapps_agents/workflow/cursor_executor.py +2196 -2118
- tapps_agents/workflow/direct_execution_fallback.py +16 -3
- tapps_agents/workflow/enforcer.py +36 -23
- tapps_agents/workflow/message_formatter.py +188 -0
- tapps_agents/workflow/parallel_executor.py +43 -4
- tapps_agents/workflow/parser.py +375 -357
- tapps_agents/workflow/rules_generator.py +337 -331
- tapps_agents/workflow/skill_invoker.py +9 -3
- {tapps_agents-3.5.38.dist-info → tapps_agents-3.5.40.dist-info}/METADATA +9 -5
- {tapps_agents-3.5.38.dist-info → tapps_agents-3.5.40.dist-info}/RECORD +62 -53
- tapps_agents/agents/analyst/SKILL.md +0 -85
- tapps_agents/agents/architect/SKILL.md +0 -80
- tapps_agents/agents/debugger/SKILL.md +0 -66
- tapps_agents/agents/designer/SKILL.md +0 -78
- tapps_agents/agents/documenter/SKILL.md +0 -95
- tapps_agents/agents/enhancer/SKILL.md +0 -189
- tapps_agents/agents/implementer/SKILL.md +0 -117
- tapps_agents/agents/improver/SKILL.md +0 -55
- tapps_agents/agents/ops/SKILL.md +0 -64
- tapps_agents/agents/orchestrator/SKILL.md +0 -238
- tapps_agents/agents/planner/story_template.md +0 -37
- tapps_agents/agents/reviewer/templates/quality-dashboard.html.j2 +0 -150
- tapps_agents/agents/tester/SKILL.md +0 -71
- {tapps_agents-3.5.38.dist-info → tapps_agents-3.5.40.dist-info}/WHEEL +0 -0
- {tapps_agents-3.5.38.dist-info → tapps_agents-3.5.40.dist-info}/entry_points.txt +0 -0
- {tapps_agents-3.5.38.dist-info → tapps_agents-3.5.40.dist-info}/licenses/LICENSE +0 -0
- {tapps_agents-3.5.38.dist-info → tapps_agents-3.5.40.dist-info}/top_level.txt +0 -0
|
@@ -964,9 +964,7 @@ class CodeScorer(BaseScorer):
|
|
|
964
964
|
|
|
965
965
|
Phase 6.2: Modern Quality Analysis - mypy Integration
|
|
966
966
|
Phase 5 (P1): Fixed to actually run mypy and return real scores (not static 5.0).
|
|
967
|
-
|
|
968
|
-
Returns:
|
|
969
|
-
Type checking score (0-10), where 10 = no issues, 0 = many issues
|
|
967
|
+
ENH-002-S2: Prefer ScopedMypyExecutor (--follow-imports=skip, <10s) with fallback to full mypy.
|
|
970
968
|
"""
|
|
971
969
|
if not self.has_mypy:
|
|
972
970
|
logger.debug("mypy not available - returning neutral score")
|
|
@@ -976,10 +974,25 @@ class CodeScorer(BaseScorer):
|
|
|
976
974
|
if file_path.suffix != ".py":
|
|
977
975
|
return 10.0 # Perfect score for non-Python files (can't type check)
|
|
978
976
|
|
|
977
|
+
# ENH-002-S2: Try scoped mypy first (faster)
|
|
978
|
+
try:
|
|
979
|
+
from .tools.scoped_mypy import ScopedMypyExecutor
|
|
980
|
+
executor = ScopedMypyExecutor()
|
|
981
|
+
result = executor.run_scoped_sync(file_path, timeout=10)
|
|
982
|
+
if result.files_checked == 1 or result.issues:
|
|
983
|
+
error_count = len(result.issues)
|
|
984
|
+
if error_count == 0:
|
|
985
|
+
return 10.0
|
|
986
|
+
score = 10.0 - (error_count * 0.5)
|
|
987
|
+
logger.debug(
|
|
988
|
+
"mypy (scoped) found %s errors for %s, score: %s/10",
|
|
989
|
+
error_count, file_path, score,
|
|
990
|
+
)
|
|
991
|
+
return max(0.0, min(10.0, score))
|
|
992
|
+
except Exception as e:
|
|
993
|
+
logger.debug("scoped mypy not used, falling back to full mypy: %s", e)
|
|
994
|
+
|
|
979
995
|
try:
|
|
980
|
-
# Phase 5 fix: Actually run mypy and parse errors
|
|
981
|
-
# Run mypy with show-error-codes for better error parsing
|
|
982
|
-
# Optimization (ENH-002): Scoped to single file with --no-incremental for 6x speedup
|
|
983
996
|
result = subprocess.run( # nosec B603
|
|
984
997
|
[
|
|
985
998
|
sys.executable,
|
|
@@ -988,63 +1001,44 @@ class CodeScorer(BaseScorer):
|
|
|
988
1001
|
"--show-error-codes",
|
|
989
1002
|
"--no-error-summary",
|
|
990
1003
|
"--no-color-output",
|
|
991
|
-
"--no-incremental",
|
|
1004
|
+
"--no-incremental",
|
|
992
1005
|
str(file_path),
|
|
993
1006
|
],
|
|
994
1007
|
capture_output=True,
|
|
995
1008
|
text=True,
|
|
996
1009
|
encoding="utf-8",
|
|
997
1010
|
errors="replace",
|
|
998
|
-
timeout=30,
|
|
1011
|
+
timeout=30,
|
|
999
1012
|
cwd=file_path.parent if file_path.parent.exists() else None,
|
|
1000
1013
|
)
|
|
1001
|
-
|
|
1002
|
-
# Phase 5 fix: Parse mypy output correctly
|
|
1003
1014
|
if result.returncode == 0:
|
|
1004
|
-
|
|
1005
|
-
logger.debug(f"mypy found no errors for {file_path}")
|
|
1015
|
+
logger.debug("mypy found no errors for %s", file_path)
|
|
1006
1016
|
return 10.0
|
|
1007
|
-
|
|
1008
|
-
# mypy returns non-zero exit code when errors found
|
|
1009
|
-
# mypy outputs errors to stdout (not stderr)
|
|
1010
1017
|
output = result.stdout.strip()
|
|
1011
1018
|
if not output:
|
|
1012
|
-
|
|
1013
|
-
logger.debug(f"mypy returned non-zero but no output for {file_path}")
|
|
1019
|
+
logger.debug("mypy returned non-zero but no output for %s", file_path)
|
|
1014
1020
|
return 10.0
|
|
1015
|
-
|
|
1016
|
-
# Parse mypy error output
|
|
1017
|
-
# Format: filename:line: error: message [error-code]
|
|
1018
|
-
# Or: filename:line:column: error: message [error-code]
|
|
1019
1021
|
error_lines = [
|
|
1020
1022
|
line
|
|
1021
1023
|
for line in output.split("\n")
|
|
1022
1024
|
if "error:" in line.lower() and file_path.name in line
|
|
1023
1025
|
]
|
|
1024
1026
|
error_count = len(error_lines)
|
|
1025
|
-
|
|
1026
1027
|
if error_count == 0:
|
|
1027
|
-
|
|
1028
|
-
logger.debug(f"mypy returned non-zero but no parseable errors for {file_path}")
|
|
1028
|
+
logger.debug("mypy returned non-zero but no parseable errors for %s", file_path)
|
|
1029
1029
|
return 10.0
|
|
1030
|
-
|
|
1031
|
-
# Phase 5 fix: Calculate score based on actual error count
|
|
1032
|
-
# Formula: 10 - (errors * 0.5), but cap at 0.0
|
|
1033
1030
|
score = 10.0 - (error_count * 0.5)
|
|
1034
|
-
logger.debug(
|
|
1031
|
+
logger.debug("mypy found %s errors for %s, score: %s/10", error_count, file_path, score)
|
|
1035
1032
|
return max(0.0, min(10.0, score))
|
|
1036
|
-
|
|
1037
1033
|
except subprocess.TimeoutExpired:
|
|
1038
|
-
logger.warning(
|
|
1039
|
-
return 5.0
|
|
1034
|
+
logger.warning("mypy timed out for %s", file_path)
|
|
1035
|
+
return 5.0
|
|
1040
1036
|
except FileNotFoundError:
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
self.has_mypy = False # Update availability flag
|
|
1037
|
+
logger.debug("mypy not found in PATH for %s", file_path)
|
|
1038
|
+
self.has_mypy = False
|
|
1044
1039
|
return 5.0
|
|
1045
1040
|
except Exception as e:
|
|
1046
|
-
|
|
1047
|
-
logger.warning(f"mypy failed for {file_path}: {e}", exc_info=True)
|
|
1041
|
+
logger.warning("mypy failed for %s: %s", file_path, e, exc_info=True)
|
|
1048
1042
|
return 5.0
|
|
1049
1043
|
|
|
1050
1044
|
def get_mypy_errors(self, file_path: Path) -> list[dict[str, Any]]:
|
|
@@ -1052,15 +1046,30 @@ class CodeScorer(BaseScorer):
|
|
|
1052
1046
|
Get detailed mypy type checking errors for a file.
|
|
1053
1047
|
|
|
1054
1048
|
Phase 6.2: Modern Quality Analysis - mypy Integration
|
|
1055
|
-
|
|
1056
|
-
Returns:
|
|
1057
|
-
List of error dictionaries with line, message, error_code, etc.
|
|
1049
|
+
ENH-002-S2: Prefer ScopedMypyExecutor with fallback to full mypy.
|
|
1058
1050
|
"""
|
|
1059
1051
|
if not self.has_mypy or file_path.suffix != ".py":
|
|
1060
1052
|
return []
|
|
1061
1053
|
|
|
1062
1054
|
try:
|
|
1063
|
-
|
|
1055
|
+
from .tools.scoped_mypy import ScopedMypyExecutor
|
|
1056
|
+
executor = ScopedMypyExecutor()
|
|
1057
|
+
result = executor.run_scoped_sync(file_path, timeout=10)
|
|
1058
|
+
if result.files_checked == 1 or result.issues:
|
|
1059
|
+
return [
|
|
1060
|
+
{
|
|
1061
|
+
"filename": str(i.file_path),
|
|
1062
|
+
"line": i.line,
|
|
1063
|
+
"message": i.message,
|
|
1064
|
+
"error_code": i.error_code,
|
|
1065
|
+
"severity": i.severity,
|
|
1066
|
+
}
|
|
1067
|
+
for i in result.issues
|
|
1068
|
+
]
|
|
1069
|
+
except Exception:
|
|
1070
|
+
pass
|
|
1071
|
+
|
|
1072
|
+
try:
|
|
1064
1073
|
result = subprocess.run( # nosec B603
|
|
1065
1074
|
[
|
|
1066
1075
|
sys.executable,
|
|
@@ -1068,32 +1077,22 @@ class CodeScorer(BaseScorer):
|
|
|
1068
1077
|
"mypy",
|
|
1069
1078
|
"--show-error-codes",
|
|
1070
1079
|
"--no-error-summary",
|
|
1071
|
-
"--no-incremental",
|
|
1080
|
+
"--no-incremental",
|
|
1072
1081
|
str(file_path),
|
|
1073
1082
|
],
|
|
1074
1083
|
capture_output=True,
|
|
1075
1084
|
text=True,
|
|
1076
1085
|
encoding="utf-8",
|
|
1077
1086
|
errors="replace",
|
|
1078
|
-
timeout=30,
|
|
1087
|
+
timeout=30,
|
|
1079
1088
|
cwd=file_path.parent if file_path.parent.exists() else None,
|
|
1080
1089
|
)
|
|
1081
|
-
|
|
1082
|
-
if result.returncode == 0:
|
|
1083
|
-
# No errors
|
|
1084
|
-
return []
|
|
1085
|
-
|
|
1086
|
-
if not result.stdout.strip():
|
|
1090
|
+
if result.returncode == 0 or not result.stdout.strip():
|
|
1087
1091
|
return []
|
|
1088
|
-
|
|
1089
|
-
# Parse mypy error output
|
|
1090
|
-
# Format: filename:line: error: message [error-code]
|
|
1091
1092
|
errors = []
|
|
1092
1093
|
for line in result.stdout.strip().split("\n"):
|
|
1093
1094
|
if "error:" not in line.lower():
|
|
1094
1095
|
continue
|
|
1095
|
-
|
|
1096
|
-
# Parse line: "file.py:12: error: Missing return type [func-returns]"
|
|
1097
1096
|
parts = line.split(":", 3)
|
|
1098
1097
|
if len(parts) >= 4:
|
|
1099
1098
|
filename = parts[0]
|
|
@@ -1101,10 +1100,7 @@ class CodeScorer(BaseScorer):
|
|
|
1101
1100
|
line_num = int(parts[1])
|
|
1102
1101
|
except ValueError:
|
|
1103
1102
|
continue
|
|
1104
|
-
|
|
1105
1103
|
error_msg = parts[3].strip()
|
|
1106
|
-
|
|
1107
|
-
# Extract error code (if present)
|
|
1108
1104
|
error_code = None
|
|
1109
1105
|
if "[" in error_msg and "]" in error_msg:
|
|
1110
1106
|
start = error_msg.rfind("[")
|
|
@@ -1112,19 +1108,14 @@ class CodeScorer(BaseScorer):
|
|
|
1112
1108
|
if start < end:
|
|
1113
1109
|
error_code = error_msg[start + 1 : end]
|
|
1114
1110
|
error_msg = error_msg[:start].strip()
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
"severity": "error",
|
|
1123
|
-
}
|
|
1124
|
-
)
|
|
1125
|
-
|
|
1111
|
+
errors.append({
|
|
1112
|
+
"filename": filename,
|
|
1113
|
+
"line": line_num,
|
|
1114
|
+
"message": error_msg,
|
|
1115
|
+
"error_code": error_code,
|
|
1116
|
+
"severity": "error",
|
|
1117
|
+
})
|
|
1126
1118
|
return errors
|
|
1127
|
-
|
|
1128
1119
|
except (subprocess.TimeoutExpired, FileNotFoundError, Exception):
|
|
1129
1120
|
return []
|
|
1130
1121
|
|
|
@@ -8,9 +8,33 @@ from .parallel_executor import (
|
|
|
8
8
|
ToolResult,
|
|
9
9
|
ToolStatus,
|
|
10
10
|
)
|
|
11
|
+
from .ruff_grouping import (
|
|
12
|
+
GroupedRuffIssues,
|
|
13
|
+
RuffGroupingConfig,
|
|
14
|
+
RuffGroupingParser,
|
|
15
|
+
RuffIssue,
|
|
16
|
+
RuffParsingError,
|
|
17
|
+
)
|
|
18
|
+
from .scoped_mypy import (
|
|
19
|
+
MypyIssue,
|
|
20
|
+
MypyResult,
|
|
21
|
+
MypyTimeoutError,
|
|
22
|
+
ScopedMypyConfig,
|
|
23
|
+
ScopedMypyExecutor,
|
|
24
|
+
)
|
|
11
25
|
|
|
12
26
|
__all__ = [
|
|
27
|
+
"GroupedRuffIssues",
|
|
28
|
+
"MypyIssue",
|
|
29
|
+
"MypyResult",
|
|
30
|
+
"MypyTimeoutError",
|
|
13
31
|
"ParallelToolExecutor",
|
|
32
|
+
"RuffGroupingConfig",
|
|
33
|
+
"RuffGroupingParser",
|
|
34
|
+
"RuffIssue",
|
|
35
|
+
"RuffParsingError",
|
|
36
|
+
"ScopedMypyConfig",
|
|
37
|
+
"ScopedMypyExecutor",
|
|
14
38
|
"ToolExecutionConfig",
|
|
15
39
|
"ToolResult",
|
|
16
40
|
"ToolStatus",
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Ruff Output Grouping - ENH-002-S3
|
|
3
|
+
|
|
4
|
+
Parses Ruff JSON output and groups issues by error code for cleaner reports.
|
|
5
|
+
Sorts by severity (error > warning > info), then by count.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class RuffParsingError(Exception):
|
|
19
|
+
"""Ruff output parsing failed."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, reason: str) -> None:
|
|
22
|
+
self.reason = reason
|
|
23
|
+
super().__init__(f"Ruff parsing failed: {reason}")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class RuffIssue:
|
|
28
|
+
"""Single Ruff linting issue."""
|
|
29
|
+
|
|
30
|
+
code: str
|
|
31
|
+
message: str
|
|
32
|
+
line: int
|
|
33
|
+
column: int
|
|
34
|
+
severity: str
|
|
35
|
+
fixable: bool
|
|
36
|
+
|
|
37
|
+
def to_dict(self) -> dict[str, Any]:
|
|
38
|
+
"""Convert to dictionary for serialization."""
|
|
39
|
+
return {
|
|
40
|
+
"code": self.code,
|
|
41
|
+
"message": self.message,
|
|
42
|
+
"line": self.line,
|
|
43
|
+
"column": self.column,
|
|
44
|
+
"severity": self.severity,
|
|
45
|
+
"fixable": self.fixable,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True)
|
|
50
|
+
class GroupedRuffIssues:
|
|
51
|
+
"""Grouped Ruff issues by error code."""
|
|
52
|
+
|
|
53
|
+
groups: dict[str, tuple[RuffIssue, ...]]
|
|
54
|
+
total_issues: int
|
|
55
|
+
unique_codes: int
|
|
56
|
+
severity_summary: dict[str, int]
|
|
57
|
+
fixable_count: int
|
|
58
|
+
|
|
59
|
+
def to_dict(self) -> dict[str, Any]:
|
|
60
|
+
"""Convert to dictionary for serialization."""
|
|
61
|
+
return {
|
|
62
|
+
"groups": {
|
|
63
|
+
code: [i.to_dict() for i in issues]
|
|
64
|
+
for code, issues in self.groups.items()
|
|
65
|
+
},
|
|
66
|
+
"total_issues": self.total_issues,
|
|
67
|
+
"unique_codes": self.unique_codes,
|
|
68
|
+
"severity_summary": self.severity_summary,
|
|
69
|
+
"fixable_count": self.fixable_count,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass(frozen=True)
|
|
74
|
+
class RuffGroupingConfig:
|
|
75
|
+
"""Configuration for Ruff grouping."""
|
|
76
|
+
|
|
77
|
+
enabled: bool = True
|
|
78
|
+
sort_by: str = "severity"
|
|
79
|
+
include_fix_suggestions: bool = True
|
|
80
|
+
max_issues_per_group: int = 10
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _severity_order(severity: str) -> int:
|
|
84
|
+
"""Lower = higher priority (error=0, warning=1, info=2)."""
|
|
85
|
+
order = {"error": 0, "warning": 1, "info": 2, "fatal": 0}
|
|
86
|
+
return order.get(severity.lower(), 1)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class RuffGroupingParser:
|
|
90
|
+
"""
|
|
91
|
+
Parse Ruff JSON and group issues by error code.
|
|
92
|
+
|
|
93
|
+
Sorts groups by severity then count; supports markdown, HTML, JSON output.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
def __init__(self, config: RuffGroupingConfig | None = None) -> None:
|
|
97
|
+
self.config = config or RuffGroupingConfig()
|
|
98
|
+
|
|
99
|
+
def parse_and_group(self, ruff_json: str) -> GroupedRuffIssues:
|
|
100
|
+
"""
|
|
101
|
+
Parse Ruff JSON output and group by error code.
|
|
102
|
+
|
|
103
|
+
Ruff JSON is a list of diagnostics; each has "code" (dict with "name"),
|
|
104
|
+
"message", "location" (row, column), "fix" (optional).
|
|
105
|
+
"""
|
|
106
|
+
try:
|
|
107
|
+
data = json.loads(ruff_json) if ruff_json.strip() else []
|
|
108
|
+
except json.JSONDecodeError as e:
|
|
109
|
+
raise RuffParsingError(str(e)) from e
|
|
110
|
+
if not isinstance(data, list):
|
|
111
|
+
raise RuffParsingError("Expected a JSON array of diagnostics")
|
|
112
|
+
|
|
113
|
+
issues: list[RuffIssue] = []
|
|
114
|
+
for diag in data:
|
|
115
|
+
if not isinstance(diag, dict):
|
|
116
|
+
continue
|
|
117
|
+
code_info = diag.get("code")
|
|
118
|
+
if isinstance(code_info, dict):
|
|
119
|
+
code = code_info.get("name") or code_info.get("code") or "unknown"
|
|
120
|
+
else:
|
|
121
|
+
code = str(code_info) if code_info else "unknown"
|
|
122
|
+
message = diag.get("message", "")
|
|
123
|
+
loc = diag.get("location", {}) or {}
|
|
124
|
+
row = int(loc.get("row", 0)) if isinstance(loc, dict) else 0
|
|
125
|
+
col = int(loc.get("column", 0)) if isinstance(loc, dict) else 0
|
|
126
|
+
fix = diag.get("fix")
|
|
127
|
+
fixable = fix is not None and fix != {}
|
|
128
|
+
severity = "error"
|
|
129
|
+
if isinstance(code_info, dict):
|
|
130
|
+
severity = (code_info.get("severity") or "error").lower()
|
|
131
|
+
if code.startswith("E") or code.startswith("F"):
|
|
132
|
+
severity = "error"
|
|
133
|
+
elif code.startswith("W"):
|
|
134
|
+
severity = "warning"
|
|
135
|
+
elif code.startswith("I"):
|
|
136
|
+
severity = "info"
|
|
137
|
+
issues.append(
|
|
138
|
+
RuffIssue(
|
|
139
|
+
code=code,
|
|
140
|
+
message=message,
|
|
141
|
+
line=row,
|
|
142
|
+
column=col,
|
|
143
|
+
severity=severity,
|
|
144
|
+
fixable=fixable,
|
|
145
|
+
)
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
groups: dict[str, list[RuffIssue]] = {}
|
|
149
|
+
severity_summary: dict[str, int] = {}
|
|
150
|
+
fixable_count = 0
|
|
151
|
+
for i in issues:
|
|
152
|
+
groups.setdefault(i.code, []).append(i)
|
|
153
|
+
severity_summary[i.severity] = severity_summary.get(i.severity, 0) + 1
|
|
154
|
+
if i.fixable:
|
|
155
|
+
fixable_count += 1
|
|
156
|
+
|
|
157
|
+
return GroupedRuffIssues(
|
|
158
|
+
groups={k: tuple(v) for k, v in groups.items()},
|
|
159
|
+
total_issues=len(issues),
|
|
160
|
+
unique_codes=len(groups),
|
|
161
|
+
severity_summary=severity_summary,
|
|
162
|
+
fixable_count=fixable_count,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
def sort_groups(
|
|
166
|
+
self,
|
|
167
|
+
groups: dict[str, tuple[RuffIssue, ...]],
|
|
168
|
+
by: str = "severity",
|
|
169
|
+
) -> list[tuple[str, tuple[RuffIssue, ...]]]:
|
|
170
|
+
"""
|
|
171
|
+
Sort groups by severity (error > warning > info), then count, then code.
|
|
172
|
+
"""
|
|
173
|
+
items = list(groups.items())
|
|
174
|
+
if by == "code":
|
|
175
|
+
return sorted(items, key=lambda x: x[0])
|
|
176
|
+
if by == "count":
|
|
177
|
+
return sorted(items, key=lambda x: -len(x[1]))
|
|
178
|
+
# severity: worst severity first, then by count descending
|
|
179
|
+
def key(item: tuple[str, tuple[RuffIssue, ...]]) -> tuple[int, int, str]:
|
|
180
|
+
code, iss = item
|
|
181
|
+
min_sev = min(_severity_order(i.severity) for i in iss)
|
|
182
|
+
return (min_sev, -len(iss), code)
|
|
183
|
+
|
|
184
|
+
return sorted(items, key=key)
|
|
185
|
+
|
|
186
|
+
def render_grouped(
|
|
187
|
+
self,
|
|
188
|
+
grouped: GroupedRuffIssues,
|
|
189
|
+
format: str = "markdown",
|
|
190
|
+
) -> str:
|
|
191
|
+
"""Render grouped issues as markdown, HTML, or JSON."""
|
|
192
|
+
sorted_pairs = self.sort_groups(grouped.groups, by=self.config.sort_by)
|
|
193
|
+
if format == "json":
|
|
194
|
+
return json.dumps(grouped.to_dict(), indent=2)
|
|
195
|
+
if format == "markdown":
|
|
196
|
+
return self._render_markdown(sorted_pairs, grouped)
|
|
197
|
+
if format == "html":
|
|
198
|
+
return self._render_html(sorted_pairs, grouped)
|
|
199
|
+
return self._render_markdown(sorted_pairs, grouped)
|
|
200
|
+
|
|
201
|
+
def _render_markdown(
|
|
202
|
+
self,
|
|
203
|
+
sorted_pairs: list[tuple[str, tuple[RuffIssue, ...]]],
|
|
204
|
+
grouped: GroupedRuffIssues,
|
|
205
|
+
) -> str:
|
|
206
|
+
lines = [
|
|
207
|
+
"### Issues by Code",
|
|
208
|
+
"",
|
|
209
|
+
f"Total: {grouped.total_issues} issues in {grouped.unique_codes} categories.",
|
|
210
|
+
]
|
|
211
|
+
if self.config.include_fix_suggestions and grouped.fixable_count:
|
|
212
|
+
lines.append(f"*{grouped.fixable_count} auto-fixable*")
|
|
213
|
+
lines.append("")
|
|
214
|
+
max_per = self.config.max_issues_per_group
|
|
215
|
+
for code, iss in sorted_pairs:
|
|
216
|
+
fixable = sum(1 for i in iss if i.fixable)
|
|
217
|
+
sev = iss[0].severity if iss else "error"
|
|
218
|
+
lines.append(f"#### {code} ({len(iss)} issues, {sev})")
|
|
219
|
+
if self.config.include_fix_suggestions and fixable:
|
|
220
|
+
lines.append(f"*{fixable} auto-fixable*")
|
|
221
|
+
for i in iss[:max_per]:
|
|
222
|
+
lines.append(f"- Line {i.line}: {i.message}")
|
|
223
|
+
if len(iss) > max_per:
|
|
224
|
+
lines.append(f"- ... and {len(iss) - max_per} more")
|
|
225
|
+
lines.append("")
|
|
226
|
+
return "\n".join(lines).strip()
|
|
227
|
+
|
|
228
|
+
def _render_html(
|
|
229
|
+
self,
|
|
230
|
+
sorted_pairs: list[tuple[str, tuple[RuffIssue, ...]]],
|
|
231
|
+
grouped: GroupedRuffIssues,
|
|
232
|
+
) -> str:
|
|
233
|
+
lines = [
|
|
234
|
+
"<div class='ruff-grouped'>",
|
|
235
|
+
f"<p>Total: {grouped.total_issues} issues in {grouped.unique_codes} categories.</p>",
|
|
236
|
+
]
|
|
237
|
+
for code, iss in sorted_pairs:
|
|
238
|
+
fixable = sum(1 for i in iss if i.fixable)
|
|
239
|
+
sev = iss[0].severity if iss else "error"
|
|
240
|
+
lines.append(f"<details><summary>{code} ({len(iss)} issues, {sev})")
|
|
241
|
+
if fixable:
|
|
242
|
+
lines.append(f" — {fixable} auto-fixable")
|
|
243
|
+
lines.append("</summary><ul>")
|
|
244
|
+
for i in iss[: self.config.max_issues_per_group]:
|
|
245
|
+
lines.append(f"<li>Line {i.line}: {i.message}</li>")
|
|
246
|
+
if len(iss) > self.config.max_issues_per_group:
|
|
247
|
+
lines.append(f"<li>... and {len(iss) - self.config.max_issues_per_group} more</li>")
|
|
248
|
+
lines.append("</ul></details>")
|
|
249
|
+
lines.append("</div>")
|
|
250
|
+
return "\n".join(lines)
|