open-autoforge 0.1.0__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.
- autoforge/__init__.py +3 -0
- autoforge/__main__.py +327 -0
- autoforge/adapters/__init__.py +1 -0
- autoforge/adapters/base.py +42 -0
- autoforge/adapters/complexity.py +124 -0
- autoforge/adapters/test_quality.py +949 -0
- autoforge/budget.py +102 -0
- autoforge/git_manager.py +137 -0
- autoforge/models.py +299 -0
- autoforge/registry.py +113 -0
- autoforge/regression.py +161 -0
- autoforge/reporting.py +209 -0
- autoforge/runner.py +432 -0
- autoforge/workflows/__init__.py +1 -0
- open_autoforge-0.1.0.dist-info/METADATA +266 -0
- open_autoforge-0.1.0.dist-info/RECORD +20 -0
- open_autoforge-0.1.0.dist-info/WHEEL +5 -0
- open_autoforge-0.1.0.dist-info/entry_points.txt +2 -0
- open_autoforge-0.1.0.dist-info/licenses/LICENSE +21 -0
- open_autoforge-0.1.0.dist-info/top_level.txt +1 -0
autoforge/__init__.py
ADDED
autoforge/__main__.py
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AutoForge CLI entry point.
|
|
3
|
+
|
|
4
|
+
Commands:
|
|
5
|
+
run — Execute a workflow (measure -> act -> validate loop)
|
|
6
|
+
health — Run all metric adapters and produce a health dashboard
|
|
7
|
+
list — List available workflows and adapters
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import json
|
|
14
|
+
import logging
|
|
15
|
+
import sys
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from autoforge import __version__
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _setup_logging(verbose: bool = False) -> None:
|
|
22
|
+
level = logging.DEBUG if verbose else logging.INFO
|
|
23
|
+
logging.basicConfig(
|
|
24
|
+
level=level,
|
|
25
|
+
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
26
|
+
datefmt="%H:%M:%S",
|
|
27
|
+
force=True,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
# Commands
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
def cmd_run(args: argparse.Namespace) -> int:
|
|
36
|
+
"""Execute a workflow."""
|
|
37
|
+
from autoforge.registry import find_workflow_config, get_adapter
|
|
38
|
+
from autoforge.reporting import report_to_markdown, save_run_report
|
|
39
|
+
from autoforge.runner import WorkflowRunner
|
|
40
|
+
|
|
41
|
+
# Load workflow config
|
|
42
|
+
if args.config:
|
|
43
|
+
from autoforge.registry import load_workflow_config
|
|
44
|
+
config = load_workflow_config(args.config)
|
|
45
|
+
else:
|
|
46
|
+
config = find_workflow_config(args.workflow)
|
|
47
|
+
|
|
48
|
+
# Override budget from CLI
|
|
49
|
+
if args.max_iterations is not None:
|
|
50
|
+
config.budget.max_iterations = args.max_iterations
|
|
51
|
+
if args.max_tokens is not None:
|
|
52
|
+
config.budget.max_tokens = args.max_tokens
|
|
53
|
+
if args.max_time is not None:
|
|
54
|
+
config.budget.max_wall_clock_minutes = args.max_time
|
|
55
|
+
|
|
56
|
+
# Get the adapter
|
|
57
|
+
adapter_name = args.adapter or config.adapter or config.name
|
|
58
|
+
adapter = get_adapter(adapter_name)
|
|
59
|
+
|
|
60
|
+
# Resolve paths
|
|
61
|
+
repo_path = str(Path(args.repo).resolve())
|
|
62
|
+
target_path = str(Path(args.path).resolve()) if args.path else repo_path
|
|
63
|
+
|
|
64
|
+
# Create and run
|
|
65
|
+
runner = WorkflowRunner(
|
|
66
|
+
config=config,
|
|
67
|
+
adapter=adapter,
|
|
68
|
+
repo_path=repo_path,
|
|
69
|
+
target_path=target_path,
|
|
70
|
+
target_value=args.target,
|
|
71
|
+
test_command=args.test_command,
|
|
72
|
+
skip_tests=args.skip_tests,
|
|
73
|
+
skip_git=args.skip_git,
|
|
74
|
+
dry_run=args.dry_run,
|
|
75
|
+
agent_command=args.agent_command,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
report = runner.run()
|
|
79
|
+
|
|
80
|
+
# Save report
|
|
81
|
+
output_dir = args.output or str(Path(repo_path) / ".autoforge" / "reports")
|
|
82
|
+
json_path, md_path = save_run_report(report, output_dir)
|
|
83
|
+
|
|
84
|
+
# Print summary
|
|
85
|
+
print()
|
|
86
|
+
print(report_to_markdown(report))
|
|
87
|
+
print()
|
|
88
|
+
print(f"Reports saved to:")
|
|
89
|
+
print(f" JSON: {json_path}")
|
|
90
|
+
print(f" Markdown: {md_path}")
|
|
91
|
+
|
|
92
|
+
return 0 if report.outcome.value == "target_met" else 1
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def cmd_health(args: argparse.Namespace) -> int:
|
|
96
|
+
"""Run health check across all (or specified) adapters."""
|
|
97
|
+
from autoforge.registry import get_adapter, list_adapters
|
|
98
|
+
from autoforge.reporting import format_health_dashboard
|
|
99
|
+
from autoforge.models import MetricResult
|
|
100
|
+
|
|
101
|
+
repo_path = str(Path(args.repo).resolve())
|
|
102
|
+
target_path = str(Path(args.path).resolve()) if args.path else repo_path
|
|
103
|
+
|
|
104
|
+
adapter_names = args.adapters.split(",") if args.adapters else list_adapters()
|
|
105
|
+
|
|
106
|
+
metrics: dict[str, MetricResult] = {}
|
|
107
|
+
for name in adapter_names:
|
|
108
|
+
try:
|
|
109
|
+
adapter = get_adapter(name)
|
|
110
|
+
if adapter.check_prerequisites(repo_path):
|
|
111
|
+
result = adapter.measure(repo_path, target_path)
|
|
112
|
+
metrics[name] = result
|
|
113
|
+
else:
|
|
114
|
+
logging.getLogger(__name__).warning(
|
|
115
|
+
"Skipping adapter '%s': prerequisites not met", name
|
|
116
|
+
)
|
|
117
|
+
except Exception as e:
|
|
118
|
+
logging.getLogger(__name__).error("Adapter '%s' failed: %s", name, e)
|
|
119
|
+
|
|
120
|
+
if not metrics:
|
|
121
|
+
print("No metrics collected. Check adapter prerequisites.")
|
|
122
|
+
return 1
|
|
123
|
+
|
|
124
|
+
dashboard = format_health_dashboard(metrics)
|
|
125
|
+
|
|
126
|
+
if args.format == "json":
|
|
127
|
+
data = {
|
|
128
|
+
name: {
|
|
129
|
+
"metric_name": r.metric_name,
|
|
130
|
+
"value": r.value,
|
|
131
|
+
"unit": r.unit,
|
|
132
|
+
"direction": r.direction.value,
|
|
133
|
+
"breakdown": r.breakdown,
|
|
134
|
+
}
|
|
135
|
+
for name, r in metrics.items()
|
|
136
|
+
}
|
|
137
|
+
print(json.dumps(data, indent=2))
|
|
138
|
+
else:
|
|
139
|
+
print(dashboard)
|
|
140
|
+
|
|
141
|
+
# Save if output specified
|
|
142
|
+
if args.output:
|
|
143
|
+
out = Path(args.output)
|
|
144
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
145
|
+
out.write_text(dashboard + "\n")
|
|
146
|
+
print(f"\nSaved to: {out}")
|
|
147
|
+
|
|
148
|
+
return 0
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def cmd_list(args: argparse.Namespace) -> int:
|
|
152
|
+
"""List available workflows and adapters."""
|
|
153
|
+
from autoforge.registry import list_adapters, list_workflows
|
|
154
|
+
|
|
155
|
+
print("Available Adapters:")
|
|
156
|
+
for name in list_adapters():
|
|
157
|
+
print(f" - {name}")
|
|
158
|
+
|
|
159
|
+
print()
|
|
160
|
+
print("Available Workflows:")
|
|
161
|
+
workflows = list_workflows()
|
|
162
|
+
if workflows:
|
|
163
|
+
for name in workflows:
|
|
164
|
+
print(f" - {name}")
|
|
165
|
+
else:
|
|
166
|
+
print(" (none found)")
|
|
167
|
+
|
|
168
|
+
return 0
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# ---------------------------------------------------------------------------
|
|
172
|
+
# Argument Parser
|
|
173
|
+
# ---------------------------------------------------------------------------
|
|
174
|
+
|
|
175
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
176
|
+
parser = argparse.ArgumentParser(
|
|
177
|
+
prog="autoforge",
|
|
178
|
+
description="AutoForge: Autonomous metric-driven agentic coding framework",
|
|
179
|
+
)
|
|
180
|
+
parser.add_argument(
|
|
181
|
+
"--version", "-V",
|
|
182
|
+
action="version",
|
|
183
|
+
version=f"%(prog)s {__version__}",
|
|
184
|
+
)
|
|
185
|
+
parser.add_argument(
|
|
186
|
+
"--verbose", "-v",
|
|
187
|
+
action="store_true",
|
|
188
|
+
help="Enable debug logging",
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
subparsers = parser.add_subparsers(dest="command", help="Command to run")
|
|
192
|
+
|
|
193
|
+
# --- run ---
|
|
194
|
+
run_p = subparsers.add_parser("run", help="Execute a workflow")
|
|
195
|
+
run_p.add_argument(
|
|
196
|
+
"workflow",
|
|
197
|
+
help="Workflow name (e.g., 'complexity_refactor') or path to YAML config",
|
|
198
|
+
)
|
|
199
|
+
run_p.add_argument(
|
|
200
|
+
"--path", "-p",
|
|
201
|
+
default=None,
|
|
202
|
+
help="Target path to improve (default: repo root)",
|
|
203
|
+
)
|
|
204
|
+
run_p.add_argument(
|
|
205
|
+
"--repo", "-r",
|
|
206
|
+
default=".",
|
|
207
|
+
help="Repository root (default: current directory)",
|
|
208
|
+
)
|
|
209
|
+
run_p.add_argument(
|
|
210
|
+
"--target", "-t",
|
|
211
|
+
type=float,
|
|
212
|
+
default=None,
|
|
213
|
+
help="Target metric value to achieve",
|
|
214
|
+
)
|
|
215
|
+
run_p.add_argument(
|
|
216
|
+
"--adapter", "-a",
|
|
217
|
+
default=None,
|
|
218
|
+
help="Metric adapter to use (default: inferred from workflow)",
|
|
219
|
+
)
|
|
220
|
+
run_p.add_argument(
|
|
221
|
+
"--config", "-c",
|
|
222
|
+
default=None,
|
|
223
|
+
help="Path to workflow YAML config file",
|
|
224
|
+
)
|
|
225
|
+
run_p.add_argument(
|
|
226
|
+
"--max-iterations",
|
|
227
|
+
type=int,
|
|
228
|
+
default=None,
|
|
229
|
+
help="Override max iterations",
|
|
230
|
+
)
|
|
231
|
+
run_p.add_argument(
|
|
232
|
+
"--max-tokens",
|
|
233
|
+
type=int,
|
|
234
|
+
default=None,
|
|
235
|
+
help="Override max token budget",
|
|
236
|
+
)
|
|
237
|
+
run_p.add_argument(
|
|
238
|
+
"--max-time",
|
|
239
|
+
type=int,
|
|
240
|
+
default=None,
|
|
241
|
+
help="Override max wall-clock time (minutes)",
|
|
242
|
+
)
|
|
243
|
+
run_p.add_argument(
|
|
244
|
+
"--test-command",
|
|
245
|
+
default=None,
|
|
246
|
+
help="Test command to run for regression guard",
|
|
247
|
+
)
|
|
248
|
+
run_p.add_argument(
|
|
249
|
+
"--skip-tests",
|
|
250
|
+
action="store_true",
|
|
251
|
+
help="Skip test validation between iterations",
|
|
252
|
+
)
|
|
253
|
+
run_p.add_argument(
|
|
254
|
+
"--skip-git",
|
|
255
|
+
action="store_true",
|
|
256
|
+
help="Skip git branch/commit management",
|
|
257
|
+
)
|
|
258
|
+
run_p.add_argument(
|
|
259
|
+
"--dry-run",
|
|
260
|
+
action="store_true",
|
|
261
|
+
help="Measure only, don't run agent",
|
|
262
|
+
)
|
|
263
|
+
run_p.add_argument(
|
|
264
|
+
"--agent-command",
|
|
265
|
+
default=None,
|
|
266
|
+
help="Custom agent command (overrides workflow agent.command; used as-is)",
|
|
267
|
+
)
|
|
268
|
+
run_p.add_argument(
|
|
269
|
+
"--output", "-o",
|
|
270
|
+
default=None,
|
|
271
|
+
help="Output directory for reports",
|
|
272
|
+
)
|
|
273
|
+
run_p.set_defaults(func=cmd_run)
|
|
274
|
+
|
|
275
|
+
# --- health ---
|
|
276
|
+
health_p = subparsers.add_parser("health", help="Run health check")
|
|
277
|
+
health_p.add_argument(
|
|
278
|
+
"--path", "-p",
|
|
279
|
+
default=None,
|
|
280
|
+
help="Target path to analyze (default: repo root)",
|
|
281
|
+
)
|
|
282
|
+
health_p.add_argument(
|
|
283
|
+
"--repo", "-r",
|
|
284
|
+
default=".",
|
|
285
|
+
help="Repository root (default: current directory)",
|
|
286
|
+
)
|
|
287
|
+
health_p.add_argument(
|
|
288
|
+
"--adapters",
|
|
289
|
+
default=None,
|
|
290
|
+
help="Comma-separated list of adapters to run (default: all)",
|
|
291
|
+
)
|
|
292
|
+
health_p.add_argument(
|
|
293
|
+
"--format", "-f",
|
|
294
|
+
choices=["text", "json"],
|
|
295
|
+
default="text",
|
|
296
|
+
help="Output format",
|
|
297
|
+
)
|
|
298
|
+
health_p.add_argument(
|
|
299
|
+
"--output", "-o",
|
|
300
|
+
default=None,
|
|
301
|
+
help="Save output to file",
|
|
302
|
+
)
|
|
303
|
+
health_p.set_defaults(func=cmd_health)
|
|
304
|
+
|
|
305
|
+
# --- list ---
|
|
306
|
+
list_p = subparsers.add_parser("list", help="List available workflows and adapters")
|
|
307
|
+
list_p.set_defaults(func=cmd_list)
|
|
308
|
+
|
|
309
|
+
return parser
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def main() -> None:
|
|
313
|
+
parser = build_parser()
|
|
314
|
+
args = parser.parse_args()
|
|
315
|
+
|
|
316
|
+
_setup_logging(getattr(args, "verbose", False))
|
|
317
|
+
|
|
318
|
+
if not args.command:
|
|
319
|
+
parser.print_help()
|
|
320
|
+
sys.exit(1)
|
|
321
|
+
|
|
322
|
+
exit_code = args.func(args)
|
|
323
|
+
sys.exit(exit_code)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
if __name__ == "__main__":
|
|
327
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Metric adapters for AutoForge."""
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base metric adapter interface.
|
|
3
|
+
|
|
4
|
+
All metric adapters implement the MetricAdapter protocol defined in models.py.
|
|
5
|
+
This module provides a concrete base class with shared utilities.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import shutil
|
|
12
|
+
from abc import ABC, abstractmethod
|
|
13
|
+
|
|
14
|
+
from autoforge.models import MetricResult
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class BaseMetricAdapter(ABC):
|
|
20
|
+
"""Abstract base class for metric adapters with shared utilities."""
|
|
21
|
+
|
|
22
|
+
name: str = ""
|
|
23
|
+
supported_languages: list[str] = []
|
|
24
|
+
|
|
25
|
+
def check_tool_available(self, tool_cmd: str) -> bool:
|
|
26
|
+
"""Check if a CLI tool is available on PATH."""
|
|
27
|
+
return shutil.which(tool_cmd) is not None
|
|
28
|
+
|
|
29
|
+
@abstractmethod
|
|
30
|
+
def check_prerequisites(self, repo_path: str) -> bool:
|
|
31
|
+
"""Verify tool is installed and repo is compatible."""
|
|
32
|
+
...
|
|
33
|
+
|
|
34
|
+
@abstractmethod
|
|
35
|
+
def measure(self, repo_path: str, target_path: str) -> MetricResult:
|
|
36
|
+
"""Run the metric tool and return normalized result."""
|
|
37
|
+
...
|
|
38
|
+
|
|
39
|
+
@abstractmethod
|
|
40
|
+
def identify_targets(self, result: MetricResult, n: int) -> list[str]:
|
|
41
|
+
"""Return top-n files to target for improvement (worst first)."""
|
|
42
|
+
...
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Complexity-accounting metric adapter.
|
|
3
|
+
|
|
4
|
+
Wraps the complexity-accounting tool (code-complexity-measure) to provide
|
|
5
|
+
Net Complexity Score (NCS) measurements through the standard MetricAdapter
|
|
6
|
+
interface.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
from datetime import datetime, timezone
|
|
16
|
+
|
|
17
|
+
from autoforge.adapters.base import BaseMetricAdapter
|
|
18
|
+
from autoforge.models import Direction, MetricResult
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ComplexityAdapter(BaseMetricAdapter):
|
|
24
|
+
"""Metric adapter for complexity-accounting (NCS measurement)."""
|
|
25
|
+
|
|
26
|
+
name = "complexity"
|
|
27
|
+
supported_languages = ["python", "go", "java", "javascript", "typescript", "rust", "cpp"]
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
*,
|
|
32
|
+
no_churn: bool = True,
|
|
33
|
+
no_coupling: bool = False,
|
|
34
|
+
no_duplication: bool = False,
|
|
35
|
+
ncs_model: str = "multiplicative",
|
|
36
|
+
threshold: int | None = None,
|
|
37
|
+
include_tests: bool = False,
|
|
38
|
+
):
|
|
39
|
+
self.no_churn = no_churn
|
|
40
|
+
self.no_coupling = no_coupling
|
|
41
|
+
self.no_duplication = no_duplication
|
|
42
|
+
self.ncs_model = ncs_model
|
|
43
|
+
self.threshold = threshold
|
|
44
|
+
self.include_tests = include_tests
|
|
45
|
+
|
|
46
|
+
def check_prerequisites(self, repo_path: str) -> bool:
|
|
47
|
+
"""Check that complexity-accounting is importable."""
|
|
48
|
+
try:
|
|
49
|
+
import complexity_accounting # noqa: F401
|
|
50
|
+
return True
|
|
51
|
+
except ImportError:
|
|
52
|
+
logger.warning(
|
|
53
|
+
"complexity-accounting not installed. "
|
|
54
|
+
"Install with: pip install complexity-accounting"
|
|
55
|
+
)
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
def _build_command(self, target_path: str) -> list[str]:
|
|
59
|
+
"""Build the complexity-accounting scan command."""
|
|
60
|
+
cmd = [
|
|
61
|
+
sys.executable, "-m", "complexity_accounting",
|
|
62
|
+
"scan", target_path,
|
|
63
|
+
"--format", "json",
|
|
64
|
+
"--ncs-model", self.ncs_model,
|
|
65
|
+
]
|
|
66
|
+
if self.no_churn:
|
|
67
|
+
cmd.append("--no-churn")
|
|
68
|
+
if self.no_coupling:
|
|
69
|
+
cmd.append("--no-coupling")
|
|
70
|
+
if self.no_duplication:
|
|
71
|
+
cmd.append("--no-duplication")
|
|
72
|
+
if self.threshold is not None:
|
|
73
|
+
cmd.extend(["--threshold", str(self.threshold)])
|
|
74
|
+
if self.include_tests:
|
|
75
|
+
cmd.append("--include-tests")
|
|
76
|
+
return cmd
|
|
77
|
+
|
|
78
|
+
def measure(self, repo_path: str, target_path: str) -> MetricResult:
|
|
79
|
+
"""Run complexity-accounting scan and return NCS as MetricResult."""
|
|
80
|
+
cmd = self._build_command(target_path)
|
|
81
|
+
logger.info("Running: %s", " ".join(cmd))
|
|
82
|
+
|
|
83
|
+
result = subprocess.run(
|
|
84
|
+
cmd,
|
|
85
|
+
capture_output=True,
|
|
86
|
+
text=True,
|
|
87
|
+
cwd=repo_path,
|
|
88
|
+
timeout=300,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
if result.returncode != 0:
|
|
92
|
+
raise RuntimeError(
|
|
93
|
+
f"complexity-accounting scan failed (exit {result.returncode}):\n"
|
|
94
|
+
f"{result.stderr}"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
data = json.loads(result.stdout)
|
|
98
|
+
summary = data.get("summary", {})
|
|
99
|
+
ncs = summary.get("net_complexity_score", 0.0)
|
|
100
|
+
|
|
101
|
+
breakdown: dict[str, float] = {}
|
|
102
|
+
for file_info in data.get("files", []):
|
|
103
|
+
path = file_info.get("path", "")
|
|
104
|
+
avg_cog = file_info.get("avg_cognitive", 0.0)
|
|
105
|
+
breakdown[path] = avg_cog
|
|
106
|
+
|
|
107
|
+
return MetricResult(
|
|
108
|
+
metric_name="net_complexity_score",
|
|
109
|
+
value=ncs,
|
|
110
|
+
unit="score",
|
|
111
|
+
direction=Direction.MINIMIZE,
|
|
112
|
+
breakdown=breakdown,
|
|
113
|
+
tool="complexity-accounting",
|
|
114
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
def identify_targets(self, result: MetricResult, n: int) -> list[str]:
|
|
118
|
+
"""Return top-n files with highest average cognitive complexity."""
|
|
119
|
+
sorted_files = sorted(
|
|
120
|
+
result.breakdown.items(),
|
|
121
|
+
key=lambda kv: kv[1],
|
|
122
|
+
reverse=True,
|
|
123
|
+
)
|
|
124
|
+
return [path for path, _ in sorted_files[:n]]
|