agentrepocoach 0.2.0__tar.gz → 0.3.0__tar.gz
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.
- {agentrepocoach-0.2.0 → agentrepocoach-0.3.0}/PKG-INFO +8 -2
- {agentrepocoach-0.2.0 → agentrepocoach-0.3.0}/README.md +7 -1
- {agentrepocoach-0.2.0 → agentrepocoach-0.3.0}/pyproject.toml +1 -1
- {agentrepocoach-0.2.0 → agentrepocoach-0.3.0}/src/agentrepocoach/__init__.py +1 -1
- {agentrepocoach-0.2.0 → agentrepocoach-0.3.0}/src/agentrepocoach/adapters/__init__.py +12 -2
- {agentrepocoach-0.2.0 → agentrepocoach-0.3.0}/src/agentrepocoach/adapters/csharp.py +7 -3
- {agentrepocoach-0.2.0 → agentrepocoach-0.3.0}/src/agentrepocoach/adapters/go.py +3 -1
- {agentrepocoach-0.2.0 → agentrepocoach-0.3.0}/src/agentrepocoach/adapters/rust.py +3 -1
- {agentrepocoach-0.2.0 → agentrepocoach-0.3.0}/src/agentrepocoach/cli.py +112 -6
- {agentrepocoach-0.2.0 → agentrepocoach-0.3.0}/src/agentrepocoach/components/decision_queryability.py +9 -3
- {agentrepocoach-0.2.0 → agentrepocoach-0.3.0}/src/agentrepocoach/components/test_quality.py +9 -2
- {agentrepocoach-0.2.0 → agentrepocoach-0.3.0}/src/agentrepocoach/output.py +103 -4
- agentrepocoach-0.3.0/src/agentrepocoach/pr_bot.py +174 -0
- agentrepocoach-0.3.0/src/agentrepocoach/regex_safety.py +98 -0
- {agentrepocoach-0.2.0 → agentrepocoach-0.3.0}/src/agentrepocoach.egg-info/PKG-INFO +8 -2
- {agentrepocoach-0.2.0 → agentrepocoach-0.3.0}/src/agentrepocoach.egg-info/SOURCES.txt +7 -1
- {agentrepocoach-0.2.0 → agentrepocoach-0.3.0}/tests/test_adapters.py +17 -0
- agentrepocoach-0.3.0/tests/test_cli_compare.py +342 -0
- agentrepocoach-0.3.0/tests/test_output.py +109 -0
- agentrepocoach-0.3.0/tests/test_pr_bot.py +207 -0
- agentrepocoach-0.3.0/tests/test_regex_safety.py +82 -0
- {agentrepocoach-0.2.0 → agentrepocoach-0.3.0}/LICENSE +0 -0
- {agentrepocoach-0.2.0 → agentrepocoach-0.3.0}/setup.cfg +0 -0
- {agentrepocoach-0.2.0 → agentrepocoach-0.3.0}/src/agentrepocoach/__main__.py +0 -0
- {agentrepocoach-0.2.0 → agentrepocoach-0.3.0}/src/agentrepocoach/adapters/base.py +0 -0
- {agentrepocoach-0.2.0 → agentrepocoach-0.3.0}/src/agentrepocoach/adapters/python.py +0 -0
- {agentrepocoach-0.2.0 → agentrepocoach-0.3.0}/src/agentrepocoach/adapters/typescript.py +0 -0
- {agentrepocoach-0.2.0 → agentrepocoach-0.3.0}/src/agentrepocoach/components/__init__.py +0 -0
- {agentrepocoach-0.2.0 → agentrepocoach-0.3.0}/src/agentrepocoach/components/documentation.py +0 -0
- {agentrepocoach-0.2.0 → agentrepocoach-0.3.0}/src/agentrepocoach/components/error_quality.py +0 -0
- {agentrepocoach-0.2.0 → agentrepocoach-0.3.0}/src/agentrepocoach/components/module_hygiene.py +0 -0
- {agentrepocoach-0.2.0 → agentrepocoach-0.3.0}/src/agentrepocoach/compute.py +0 -0
- {agentrepocoach-0.2.0 → agentrepocoach-0.3.0}/src/agentrepocoach/config.py +0 -0
- {agentrepocoach-0.2.0 → agentrepocoach-0.3.0}/src/agentrepocoach/scoring.py +0 -0
- {agentrepocoach-0.2.0 → agentrepocoach-0.3.0}/src/agentrepocoach.egg-info/dependency_links.txt +0 -0
- {agentrepocoach-0.2.0 → agentrepocoach-0.3.0}/src/agentrepocoach.egg-info/entry_points.txt +0 -0
- {agentrepocoach-0.2.0 → agentrepocoach-0.3.0}/src/agentrepocoach.egg-info/requires.txt +0 -0
- {agentrepocoach-0.2.0 → agentrepocoach-0.3.0}/src/agentrepocoach.egg-info/top_level.txt +0 -0
- {agentrepocoach-0.2.0 → agentrepocoach-0.3.0}/tests/test_components.py +0 -0
- {agentrepocoach-0.2.0 → agentrepocoach-0.3.0}/tests/test_config.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentrepocoach
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Score your codebase on how ready it is for AI agents — and coach you through the fixes.
|
|
5
5
|
Author: WouterDeBot
|
|
6
6
|
License: Apache-2.0
|
|
@@ -38,7 +38,7 @@ Dynamic: license-file
|
|
|
38
38
|
|
|
39
39
|
[](https://pypi.org/project/agentrepocoach/)
|
|
40
40
|
[](./LICENSE)
|
|
41
|
-
[](https://github.com/WouterDeBot/agentrepocoach/actions/workflows/ci.yml)
|
|
42
42
|
[](https://pypi.org/project/agentrepocoach/)
|
|
43
43
|
|
|
44
44
|
AgentRepoCoach computes the **Codebase Agent Health (CAH)** score: a single 0-100
|
|
@@ -123,6 +123,12 @@ python -m agentrepocoach.cli --repo . --format json --output ./report.json
|
|
|
123
123
|
# Per-sub-component breakdown
|
|
124
124
|
python -m agentrepocoach.cli --repo . --verbose
|
|
125
125
|
|
|
126
|
+
# Compare against a baseline report (inline delta)
|
|
127
|
+
python -m agentrepocoach.cli --repo . --format json --output ./pr.json --compare ./baseline.json
|
|
128
|
+
|
|
129
|
+
# Compare two saved score files
|
|
130
|
+
python -m agentrepocoach.cli compare ./baseline.json ./pr.json
|
|
131
|
+
|
|
126
132
|
# Show the installed version
|
|
127
133
|
python -m agentrepocoach.cli --version
|
|
128
134
|
```
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
[](https://pypi.org/project/agentrepocoach/)
|
|
6
6
|
[](./LICENSE)
|
|
7
|
-
[](https://github.com/WouterDeBot/agentrepocoach/actions/workflows/ci.yml)
|
|
8
8
|
[](https://pypi.org/project/agentrepocoach/)
|
|
9
9
|
|
|
10
10
|
AgentRepoCoach computes the **Codebase Agent Health (CAH)** score: a single 0-100
|
|
@@ -89,6 +89,12 @@ python -m agentrepocoach.cli --repo . --format json --output ./report.json
|
|
|
89
89
|
# Per-sub-component breakdown
|
|
90
90
|
python -m agentrepocoach.cli --repo . --verbose
|
|
91
91
|
|
|
92
|
+
# Compare against a baseline report (inline delta)
|
|
93
|
+
python -m agentrepocoach.cli --repo . --format json --output ./pr.json --compare ./baseline.json
|
|
94
|
+
|
|
95
|
+
# Compare two saved score files
|
|
96
|
+
python -m agentrepocoach.cli compare ./baseline.json ./pr.json
|
|
97
|
+
|
|
92
98
|
# Show the installed version
|
|
93
99
|
python -m agentrepocoach.cli --version
|
|
94
100
|
```
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "agentrepocoach"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "Score your codebase on how ready it is for AI agents — and coach you through the fixes."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "Apache-2.0" }
|
|
@@ -33,7 +33,12 @@ def get_adapter_by_name(name: str) -> LanguageAdapter:
|
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
def detect_primary(repo_path: Path) -> LanguageAdapter:
|
|
36
|
-
"""Try every adapter and return the one with the highest detect() confidence.
|
|
36
|
+
"""Try every adapter and return the one with the highest detect() confidence.
|
|
37
|
+
|
|
38
|
+
When multiple adapters tie on confidence, the adapter whose
|
|
39
|
+
``find_production_files`` returns more files wins — a repo with 20 .py
|
|
40
|
+
files and a single .sln fixture is almost certainly a Python project.
|
|
41
|
+
"""
|
|
37
42
|
candidates: list[tuple[float, LanguageAdapter]] = []
|
|
38
43
|
for cls in _REGISTRY.values():
|
|
39
44
|
adapter = cls()
|
|
@@ -44,7 +49,12 @@ def detect_primary(repo_path: Path) -> LanguageAdapter:
|
|
|
44
49
|
supported = ", ".join(sorted(_REGISTRY))
|
|
45
50
|
msg = f"No supported language detected in {repo_path}. Supported: {supported}."
|
|
46
51
|
raise NoAdapterError(f"{msg} Try using --language to force an adapter, or check that the repo contains a recognized project file.")
|
|
47
|
-
|
|
52
|
+
# Sort by confidence (desc), then by production file count (desc) to
|
|
53
|
+
# break ties deterministically in favour of the dominant language.
|
|
54
|
+
candidates.sort(
|
|
55
|
+
key=lambda pair: (pair[0], len(pair[1].find_production_files(repo_path))),
|
|
56
|
+
reverse=True,
|
|
57
|
+
)
|
|
48
58
|
return candidates[0][1]
|
|
49
59
|
|
|
50
60
|
|
|
@@ -116,10 +116,14 @@ class CSharpAdapter(LanguageAdapter):
|
|
|
116
116
|
# ------------------------------------------------------------------
|
|
117
117
|
|
|
118
118
|
def detect(self, repo_path: Path) -> float:
|
|
119
|
-
"""1.0 if any *.sln, 0.8 if any *.csproj, else 0.0.
|
|
120
|
-
|
|
119
|
+
"""1.0 if any *.sln, 0.8 if any *.csproj, else 0.0.
|
|
120
|
+
|
|
121
|
+
Only checks the repo root and one level deep to avoid false
|
|
122
|
+
positives from test fixtures or vendored dependencies.
|
|
123
|
+
"""
|
|
124
|
+
if any(repo_path.glob("*.sln")) or any(repo_path.glob("*/*.sln")):
|
|
121
125
|
return 1.0
|
|
122
|
-
if any(repo_path.
|
|
126
|
+
if any(repo_path.glob("*.csproj")) or any(repo_path.glob("*/*.csproj")):
|
|
123
127
|
return 0.8
|
|
124
128
|
return 0.0
|
|
125
129
|
|
|
@@ -72,7 +72,9 @@ class GoAdapter(LanguageAdapter):
|
|
|
72
72
|
def detect(self, repo_path: Path) -> float:
|
|
73
73
|
if (repo_path / "go.mod").is_file():
|
|
74
74
|
return 1.0
|
|
75
|
-
|
|
75
|
+
# Shallow search (root + one level) to avoid false positives from
|
|
76
|
+
# test fixtures or vendored dependencies.
|
|
77
|
+
if any(repo_path.glob("*.go")) or any(repo_path.glob("*/*.go")):
|
|
76
78
|
return 0.5
|
|
77
79
|
return 0.0
|
|
78
80
|
|
|
@@ -72,7 +72,9 @@ class RustAdapter(LanguageAdapter):
|
|
|
72
72
|
def detect(self, repo_path: Path) -> float:
|
|
73
73
|
if (repo_path / "Cargo.toml").is_file():
|
|
74
74
|
return 1.0
|
|
75
|
-
|
|
75
|
+
# Shallow search (root + one level) to avoid false positives from
|
|
76
|
+
# test fixtures or vendored dependencies.
|
|
77
|
+
if any(repo_path.glob("*.rs")) or any(repo_path.glob("*/*.rs")):
|
|
76
78
|
return 0.5
|
|
77
79
|
return 0.0
|
|
78
80
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
4
|
import argparse
|
|
5
|
+
import json as _json
|
|
5
6
|
import sys
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
|
|
@@ -10,12 +11,17 @@ from .adapters import NoAdapterError
|
|
|
10
11
|
from .compute import compute_cah
|
|
11
12
|
from .config import ConfigError, load_config
|
|
12
13
|
from .output import (
|
|
14
|
+
format_comparison,
|
|
15
|
+
format_comparison_markdown,
|
|
13
16
|
format_summary,
|
|
14
17
|
format_verbose,
|
|
18
|
+
render_json,
|
|
19
|
+
render_markdown_comment,
|
|
15
20
|
write_json,
|
|
16
21
|
write_markdown_comment,
|
|
17
22
|
write_prometheus,
|
|
18
23
|
)
|
|
24
|
+
from .pr_bot import compare_scores, format_pr_comment, parse_score_output
|
|
19
25
|
|
|
20
26
|
|
|
21
27
|
def build_parser() -> argparse.ArgumentParser:
|
|
@@ -49,9 +55,10 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
49
55
|
"--format",
|
|
50
56
|
choices=["json", "markdown", "both"],
|
|
51
57
|
default=None,
|
|
52
|
-
help="Output format
|
|
53
|
-
"'markdown'
|
|
54
|
-
"
|
|
58
|
+
help="Output format. 'json' prints the full report (to stdout or --output), "
|
|
59
|
+
"'markdown' prints a PR-comment summary (to stdout or --output), "
|
|
60
|
+
"'both' writes both to --output (markdown path derived by swapping "
|
|
61
|
+
"the extension to .md; requires --output).",
|
|
55
62
|
)
|
|
56
63
|
parser.add_argument(
|
|
57
64
|
"--output",
|
|
@@ -59,17 +66,88 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
59
66
|
default=None,
|
|
60
67
|
help="Output path for --format. Ignored if --format is not set.",
|
|
61
68
|
)
|
|
69
|
+
parser.add_argument(
|
|
70
|
+
"--compare",
|
|
71
|
+
type=Path,
|
|
72
|
+
default=None,
|
|
73
|
+
help="Path to a baseline JSON report. Prints a delta comparison instead of "
|
|
74
|
+
"the normal summary.",
|
|
75
|
+
)
|
|
62
76
|
parser.add_argument("--verbose", action="store_true", help="Print per-sub-component breakdown.")
|
|
63
77
|
parser.add_argument("--quiet", action="store_true", help="Print only the total score.")
|
|
64
78
|
parser.add_argument("--version", action="version", version=f"agentrepocoach {VERSION}")
|
|
79
|
+
|
|
80
|
+
# Subcommands
|
|
81
|
+
subparsers = parser.add_subparsers(dest="subcommand")
|
|
82
|
+
|
|
83
|
+
# compare subcommand
|
|
84
|
+
compare_parser = subparsers.add_parser(
|
|
85
|
+
"compare",
|
|
86
|
+
help="Compare two JSON score files and display deltas.",
|
|
87
|
+
)
|
|
88
|
+
compare_parser.add_argument(
|
|
89
|
+
"base_file",
|
|
90
|
+
type=Path,
|
|
91
|
+
help="Path to the base (target branch) JSON score file.",
|
|
92
|
+
)
|
|
93
|
+
compare_parser.add_argument(
|
|
94
|
+
"pr_file",
|
|
95
|
+
type=Path,
|
|
96
|
+
help="Path to the PR (source branch) JSON score file.",
|
|
97
|
+
)
|
|
98
|
+
compare_parser.add_argument(
|
|
99
|
+
"--json",
|
|
100
|
+
action="store_true",
|
|
101
|
+
dest="json_output",
|
|
102
|
+
help="Output raw comparison dict as JSON instead of markdown.",
|
|
103
|
+
)
|
|
104
|
+
|
|
65
105
|
return parser
|
|
66
106
|
|
|
67
107
|
|
|
108
|
+
def _run_compare(args: argparse.Namespace) -> int:
|
|
109
|
+
"""Execute the ``compare`` subcommand."""
|
|
110
|
+
base_path = args.base_file.resolve()
|
|
111
|
+
pr_path = args.pr_file.resolve()
|
|
112
|
+
|
|
113
|
+
if not base_path.is_file():
|
|
114
|
+
print(f"error: base file does not exist: {base_path}", file=sys.stderr)
|
|
115
|
+
return 2
|
|
116
|
+
if not pr_path.is_file():
|
|
117
|
+
print(f"error: pr file does not exist: {pr_path}", file=sys.stderr)
|
|
118
|
+
return 2
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
base_scores = parse_score_output(base_path.read_text())
|
|
122
|
+
except ValueError as exc:
|
|
123
|
+
print(f"error: failed to parse base file: {exc}", file=sys.stderr)
|
|
124
|
+
return 2
|
|
125
|
+
|
|
126
|
+
try:
|
|
127
|
+
pr_scores = parse_score_output(pr_path.read_text())
|
|
128
|
+
except ValueError as exc:
|
|
129
|
+
print(f"error: failed to parse pr file: {exc}", file=sys.stderr)
|
|
130
|
+
return 2
|
|
131
|
+
|
|
132
|
+
comparison = compare_scores(base_scores, pr_scores)
|
|
133
|
+
|
|
134
|
+
if args.json_output:
|
|
135
|
+
print(_json.dumps(comparison, indent=2))
|
|
136
|
+
else:
|
|
137
|
+
print(format_pr_comment(comparison))
|
|
138
|
+
|
|
139
|
+
return 0
|
|
140
|
+
|
|
141
|
+
|
|
68
142
|
def main(argv: list[str] | None = None) -> int:
|
|
69
143
|
"""Run the CLI, parse arguments, compute the CAH score, and write outputs."""
|
|
70
144
|
parser = build_parser()
|
|
71
145
|
args = parser.parse_args(argv)
|
|
72
146
|
|
|
147
|
+
# Dispatch subcommands
|
|
148
|
+
if args.subcommand == "compare":
|
|
149
|
+
return _run_compare(args)
|
|
150
|
+
|
|
73
151
|
repo_root = args.repo.resolve()
|
|
74
152
|
if not repo_root.is_dir():
|
|
75
153
|
print(f"error: repo path is not a directory: {repo_root}", file=sys.stderr)
|
|
@@ -92,7 +170,25 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
92
170
|
print(f"error: {exc}", file=sys.stderr)
|
|
93
171
|
return 2
|
|
94
172
|
|
|
95
|
-
|
|
173
|
+
# When --format is used without --output, the formatted content replaces
|
|
174
|
+
# the default terminal summary on stdout.
|
|
175
|
+
stdout_replaced_by_format = args.format and not args.output
|
|
176
|
+
|
|
177
|
+
if args.compare:
|
|
178
|
+
baseline_path = args.compare.resolve()
|
|
179
|
+
if not baseline_path.is_file():
|
|
180
|
+
print(f"error: baseline report not found: {baseline_path}", file=sys.stderr)
|
|
181
|
+
return 2
|
|
182
|
+
import json as _json
|
|
183
|
+
baseline = _json.loads(baseline_path.read_text())
|
|
184
|
+
if args.quiet:
|
|
185
|
+
delta = result["total"] - baseline["total"]
|
|
186
|
+
print(f"{delta:+.2f}")
|
|
187
|
+
else:
|
|
188
|
+
print(format_comparison(result, baseline))
|
|
189
|
+
elif stdout_replaced_by_format:
|
|
190
|
+
pass # handled below in the --format block
|
|
191
|
+
elif args.quiet:
|
|
96
192
|
print(f"{result['total']:.2f}")
|
|
97
193
|
elif args.verbose:
|
|
98
194
|
print(format_verbose(result))
|
|
@@ -117,12 +213,22 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
117
213
|
if args.format and args.output:
|
|
118
214
|
_write_formatted(result, args.format, args.output, quiet=args.quiet)
|
|
119
215
|
elif args.format and not args.output:
|
|
120
|
-
|
|
121
|
-
|
|
216
|
+
if args.format == "both":
|
|
217
|
+
print("error: --format both requires --output", file=sys.stderr)
|
|
218
|
+
return 2
|
|
219
|
+
_print_formatted(result, args.format)
|
|
122
220
|
|
|
123
221
|
return 0
|
|
124
222
|
|
|
125
223
|
|
|
224
|
+
def _print_formatted(result: dict, fmt: str) -> None:
|
|
225
|
+
"""Print formatted output to stdout when --output is not provided."""
|
|
226
|
+
if fmt == "json":
|
|
227
|
+
print(render_json(result))
|
|
228
|
+
elif fmt == "markdown":
|
|
229
|
+
print(render_markdown_comment(result))
|
|
230
|
+
|
|
231
|
+
|
|
126
232
|
def _write_formatted(
|
|
127
233
|
result: dict,
|
|
128
234
|
fmt: str,
|
{agentrepocoach-0.2.0 → agentrepocoach-0.3.0}/src/agentrepocoach/components/decision_queryability.py
RENAMED
|
@@ -14,11 +14,13 @@ inline_ref_resolution 30 -> 40. Total still sums to 100.
|
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
16
|
import re
|
|
17
|
+
import warnings
|
|
17
18
|
from pathlib import Path
|
|
18
19
|
from typing import Any
|
|
19
20
|
|
|
20
21
|
from ..adapters import LanguageAdapter
|
|
21
22
|
from ..config import Config
|
|
23
|
+
from ..regex_safety import safe_compile_pattern
|
|
22
24
|
from ..scoring import scale_linear
|
|
23
25
|
|
|
24
26
|
_ADR_COUNT_WEIGHT = 60
|
|
@@ -139,9 +141,13 @@ def _compile_inline_ref_patterns(patterns: tuple[str, ...]) -> list[re.Pattern[s
|
|
|
139
141
|
# Wrap in word boundaries if the user did not already supply them.
|
|
140
142
|
anchored = raw if raw.startswith("\\b") else rf"\b{raw}\b"
|
|
141
143
|
try:
|
|
142
|
-
compiled.append(
|
|
143
|
-
except
|
|
144
|
-
|
|
144
|
+
compiled.append(safe_compile_pattern(anchored, flags=re.IGNORECASE))
|
|
145
|
+
except ValueError as exc:
|
|
146
|
+
warnings.warn(
|
|
147
|
+
f"Skipping inline_ref_pattern {raw!r}: {exc}",
|
|
148
|
+
UserWarning,
|
|
149
|
+
stacklevel=2,
|
|
150
|
+
)
|
|
145
151
|
continue
|
|
146
152
|
return compiled
|
|
147
153
|
|
|
@@ -14,12 +14,14 @@ full credit unless the user opts in by listing project-specific patterns.
|
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
16
|
import re
|
|
17
|
+
import warnings
|
|
17
18
|
from pathlib import Path
|
|
18
19
|
from typing import Any
|
|
19
20
|
|
|
20
21
|
from ..adapters import LanguageAdapter
|
|
21
22
|
from ..adapters.base import iter_source_files
|
|
22
23
|
from ..config import Config
|
|
24
|
+
from ..regex_safety import safe_compile_pattern
|
|
23
25
|
from ..scoring import scale_linear
|
|
24
26
|
|
|
25
27
|
_NAMING_WEIGHT = 40
|
|
@@ -153,8 +155,13 @@ def _score_fixture_duplication(
|
|
|
153
155
|
compiled: list[re.Pattern[str]] = []
|
|
154
156
|
for raw in patterns:
|
|
155
157
|
try:
|
|
156
|
-
compiled.append(
|
|
157
|
-
except
|
|
158
|
+
compiled.append(safe_compile_pattern(raw))
|
|
159
|
+
except ValueError as exc:
|
|
160
|
+
warnings.warn(
|
|
161
|
+
f"Skipping fixture_duplication_pattern {raw!r}: {exc}",
|
|
162
|
+
UserWarning,
|
|
163
|
+
stacklevel=2,
|
|
164
|
+
)
|
|
158
165
|
continue
|
|
159
166
|
|
|
160
167
|
total = 0
|
|
@@ -21,14 +21,19 @@ _METRIC_HELP = "AgentRepoCoach composite codebase agent health score (0-100)."
|
|
|
21
21
|
_METRIC_NAME = "agentrepocoach_codebase_health_score"
|
|
22
22
|
|
|
23
23
|
|
|
24
|
+
def render_json(result: dict[str, Any]) -> str:
|
|
25
|
+
"""Return the full score breakdown as a JSON string."""
|
|
26
|
+
return json.dumps(result, indent=2, default=str)
|
|
27
|
+
|
|
28
|
+
|
|
24
29
|
def write_json(result: dict[str, Any], path: Path) -> None:
|
|
25
30
|
"""Write the full score breakdown as JSON."""
|
|
26
31
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
27
|
-
path.write_text(
|
|
32
|
+
path.write_text(render_json(result) + "\n")
|
|
28
33
|
|
|
29
34
|
|
|
30
|
-
def
|
|
31
|
-
"""
|
|
35
|
+
def render_prometheus(result: dict[str, Any]) -> str:
|
|
36
|
+
"""Return the score in Prometheus exposition format as a string."""
|
|
32
37
|
lines = [
|
|
33
38
|
f"# HELP {_METRIC_NAME} {_METRIC_HELP}",
|
|
34
39
|
f"# TYPE {_METRIC_NAME} gauge",
|
|
@@ -37,8 +42,36 @@ def write_prometheus(result: dict[str, Any], path: Path) -> None:
|
|
|
37
42
|
for name, component in result.get("components", {}).items():
|
|
38
43
|
score = component.get("score", 0)
|
|
39
44
|
lines.append(f'{_METRIC_NAME}{{component="{name}"}} {score}')
|
|
45
|
+
return "\n".join(lines)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def write_prometheus(result: dict[str, Any], path: Path) -> None:
|
|
49
|
+
"""Write the score in Prometheus exposition format."""
|
|
40
50
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
41
|
-
path.write_text(
|
|
51
|
+
path.write_text(render_prometheus(result) + "\n")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def render_markdown_comment(result: dict[str, Any]) -> str:
|
|
55
|
+
"""Return a short summary suitable for a GitHub PR comment as a string."""
|
|
56
|
+
lines = [
|
|
57
|
+
"### AgentRepoCoach — Codebase Agent Health",
|
|
58
|
+
"",
|
|
59
|
+
f"**Total score:** {result['total']:.2f} / 100",
|
|
60
|
+
f"**Language:** `{result.get('language', 'unknown')}`",
|
|
61
|
+
"",
|
|
62
|
+
"| Component | Score | Weight |",
|
|
63
|
+
"|---|---:|---:|",
|
|
64
|
+
]
|
|
65
|
+
weights = result.get("weights", {})
|
|
66
|
+
for name, component in result.get("components", {}).items():
|
|
67
|
+
weight = weights.get(name, 0.0)
|
|
68
|
+
lines.append(f"| {name} | {component['score']:.2f} / 100 | {weight:.2f} |")
|
|
69
|
+
tips = generate_coaching(result)
|
|
70
|
+
coaching = format_coaching_markdown(tips)
|
|
71
|
+
if coaching:
|
|
72
|
+
lines.append(coaching)
|
|
73
|
+
lines.append("<!-- agentrepocoach -->")
|
|
74
|
+
return "\n".join(lines)
|
|
42
75
|
|
|
43
76
|
|
|
44
77
|
def write_markdown_comment(result: dict[str, Any], path: Path) -> None:
|
|
@@ -254,6 +287,72 @@ def format_summary(result: dict[str, Any]) -> str:
|
|
|
254
287
|
return "\n".join(lines)
|
|
255
288
|
|
|
256
289
|
|
|
290
|
+
def format_comparison(current: dict[str, Any], baseline: dict[str, Any]) -> str:
|
|
291
|
+
"""Return a terminal-friendly delta table comparing *current* against *baseline*."""
|
|
292
|
+
cur_total = current["total"]
|
|
293
|
+
base_total = baseline["total"]
|
|
294
|
+
delta_total = cur_total - base_total
|
|
295
|
+
|
|
296
|
+
lines = [
|
|
297
|
+
"AgentRepoCoach — Score Comparison",
|
|
298
|
+
"=================================",
|
|
299
|
+
f"Total: {base_total:.2f} -> {cur_total:.2f} ({delta_total:+.2f})",
|
|
300
|
+
"",
|
|
301
|
+
f" {'Component':25s} {'Baseline':>10s} {'Current':>10s} {'Delta':>10s}",
|
|
302
|
+
f" {'-' * 25} {'-' * 10} {'-' * 10} {'-' * 10}",
|
|
303
|
+
]
|
|
304
|
+
|
|
305
|
+
base_components = baseline.get("components", {})
|
|
306
|
+
cur_components = current.get("components", {})
|
|
307
|
+
all_names = list(dict.fromkeys(list(base_components) + list(cur_components)))
|
|
308
|
+
|
|
309
|
+
for name in all_names:
|
|
310
|
+
base_score = base_components.get(name, {}).get("score", 0.0)
|
|
311
|
+
cur_score = cur_components.get(name, {}).get("score", 0.0)
|
|
312
|
+
delta = cur_score - base_score
|
|
313
|
+
lines.append(
|
|
314
|
+
f" {name:25s} {base_score:10.2f} {cur_score:10.2f} {delta:+10.2f}"
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
return "\n".join(lines)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def format_comparison_markdown(current: dict[str, Any], baseline: dict[str, Any]) -> str:
|
|
321
|
+
"""Return a markdown delta table suitable for a GitHub PR comment."""
|
|
322
|
+
cur_total = current["total"]
|
|
323
|
+
base_total = baseline["total"]
|
|
324
|
+
delta_total = cur_total - base_total
|
|
325
|
+
|
|
326
|
+
lines = [
|
|
327
|
+
"### AgentRepoCoach — Score Comparison",
|
|
328
|
+
"",
|
|
329
|
+
f"**Total score:** {base_total:.2f} -> {cur_total:.2f} ({delta_total:+.2f})",
|
|
330
|
+
"",
|
|
331
|
+
"| Component | Baseline | Current | Delta |",
|
|
332
|
+
"|---|---:|---:|---:|",
|
|
333
|
+
]
|
|
334
|
+
|
|
335
|
+
base_components = baseline.get("components", {})
|
|
336
|
+
cur_components = current.get("components", {})
|
|
337
|
+
all_names = list(dict.fromkeys(list(base_components) + list(cur_components)))
|
|
338
|
+
|
|
339
|
+
for name in all_names:
|
|
340
|
+
base_score = base_components.get(name, {}).get("score", 0.0)
|
|
341
|
+
cur_score = cur_components.get(name, {}).get("score", 0.0)
|
|
342
|
+
delta = cur_score - base_score
|
|
343
|
+
lines.append(f"| {name} | {base_score:.2f} | {cur_score:.2f} | {delta:+.2f} |")
|
|
344
|
+
|
|
345
|
+
# Append coaching tips from the current result
|
|
346
|
+
tips = generate_coaching(current)
|
|
347
|
+
coaching = format_coaching_markdown(tips)
|
|
348
|
+
if coaching:
|
|
349
|
+
lines.append(coaching)
|
|
350
|
+
|
|
351
|
+
lines.append("<!-- agentrepocoach -->")
|
|
352
|
+
|
|
353
|
+
return "\n".join(lines)
|
|
354
|
+
|
|
355
|
+
|
|
257
356
|
def format_verbose(result: dict[str, Any]) -> str:
|
|
258
357
|
"""Return the summary plus a per-sub-component breakdown."""
|
|
259
358
|
lines = [format_summary(result), "", "Sub-component breakdown:"]
|