agentrepocoach 0.2.0__tar.gz → 0.3.1__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.1}/PKG-INFO +8 -5
- {agentrepocoach-0.2.0 → agentrepocoach-0.3.1}/README.md +7 -4
- {agentrepocoach-0.2.0 → agentrepocoach-0.3.1}/pyproject.toml +1 -1
- {agentrepocoach-0.2.0 → agentrepocoach-0.3.1}/src/agentrepocoach/__init__.py +1 -1
- {agentrepocoach-0.2.0 → agentrepocoach-0.3.1}/src/agentrepocoach/adapters/__init__.py +44 -3
- {agentrepocoach-0.2.0 → agentrepocoach-0.3.1}/src/agentrepocoach/adapters/csharp.py +7 -3
- {agentrepocoach-0.2.0 → agentrepocoach-0.3.1}/src/agentrepocoach/adapters/go.py +3 -1
- {agentrepocoach-0.2.0 → agentrepocoach-0.3.1}/src/agentrepocoach/adapters/rust.py +3 -1
- agentrepocoach-0.3.1/src/agentrepocoach/cli.py +346 -0
- {agentrepocoach-0.2.0 → agentrepocoach-0.3.1}/src/agentrepocoach/components/decision_queryability.py +9 -3
- {agentrepocoach-0.2.0 → agentrepocoach-0.3.1}/src/agentrepocoach/components/test_quality.py +9 -2
- {agentrepocoach-0.2.0 → agentrepocoach-0.3.1}/src/agentrepocoach/compute.py +57 -1
- {agentrepocoach-0.2.0 → agentrepocoach-0.3.1}/src/agentrepocoach/output.py +103 -4
- agentrepocoach-0.3.1/src/agentrepocoach/pr_bot.py +174 -0
- agentrepocoach-0.3.1/src/agentrepocoach/regex_safety.py +98 -0
- {agentrepocoach-0.2.0 → agentrepocoach-0.3.1}/src/agentrepocoach.egg-info/PKG-INFO +8 -5
- {agentrepocoach-0.2.0 → agentrepocoach-0.3.1}/src/agentrepocoach.egg-info/SOURCES.txt +9 -1
- {agentrepocoach-0.2.0 → agentrepocoach-0.3.1}/tests/test_adapters.py +17 -0
- agentrepocoach-0.3.1/tests/test_cli.py +108 -0
- agentrepocoach-0.3.1/tests/test_cli_compare.py +342 -0
- agentrepocoach-0.3.1/tests/test_multi_language.py +197 -0
- agentrepocoach-0.3.1/tests/test_output.py +109 -0
- agentrepocoach-0.3.1/tests/test_pr_bot.py +207 -0
- agentrepocoach-0.3.1/tests/test_regex_safety.py +82 -0
- agentrepocoach-0.2.0/src/agentrepocoach/cli.py +0 -155
- {agentrepocoach-0.2.0 → agentrepocoach-0.3.1}/LICENSE +0 -0
- {agentrepocoach-0.2.0 → agentrepocoach-0.3.1}/setup.cfg +0 -0
- {agentrepocoach-0.2.0 → agentrepocoach-0.3.1}/src/agentrepocoach/__main__.py +0 -0
- {agentrepocoach-0.2.0 → agentrepocoach-0.3.1}/src/agentrepocoach/adapters/base.py +0 -0
- {agentrepocoach-0.2.0 → agentrepocoach-0.3.1}/src/agentrepocoach/adapters/python.py +0 -0
- {agentrepocoach-0.2.0 → agentrepocoach-0.3.1}/src/agentrepocoach/adapters/typescript.py +0 -0
- {agentrepocoach-0.2.0 → agentrepocoach-0.3.1}/src/agentrepocoach/components/__init__.py +0 -0
- {agentrepocoach-0.2.0 → agentrepocoach-0.3.1}/src/agentrepocoach/components/documentation.py +0 -0
- {agentrepocoach-0.2.0 → agentrepocoach-0.3.1}/src/agentrepocoach/components/error_quality.py +0 -0
- {agentrepocoach-0.2.0 → agentrepocoach-0.3.1}/src/agentrepocoach/components/module_hygiene.py +0 -0
- {agentrepocoach-0.2.0 → agentrepocoach-0.3.1}/src/agentrepocoach/config.py +0 -0
- {agentrepocoach-0.2.0 → agentrepocoach-0.3.1}/src/agentrepocoach/scoring.py +0 -0
- {agentrepocoach-0.2.0 → agentrepocoach-0.3.1}/src/agentrepocoach.egg-info/dependency_links.txt +0 -0
- {agentrepocoach-0.2.0 → agentrepocoach-0.3.1}/src/agentrepocoach.egg-info/entry_points.txt +0 -0
- {agentrepocoach-0.2.0 → agentrepocoach-0.3.1}/src/agentrepocoach.egg-info/requires.txt +0 -0
- {agentrepocoach-0.2.0 → agentrepocoach-0.3.1}/src/agentrepocoach.egg-info/top_level.txt +0 -0
- {agentrepocoach-0.2.0 → agentrepocoach-0.3.1}/tests/test_components.py +0 -0
- {agentrepocoach-0.2.0 → agentrepocoach-0.3.1}/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.1
|
|
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
|
```
|
|
@@ -197,6 +203,3 @@ are safe to publish as CI artifacts.
|
|
|
197
203
|
## License
|
|
198
204
|
|
|
199
205
|
Apache 2.0. See [LICENSE](LICENSE).
|
|
200
|
-
|
|
201
|
-
Built using the [GSD](https://github.com/gsd-build/get-shit-done) workflow
|
|
202
|
-
methodology (MIT).
|
|
@@ -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
|
```
|
|
@@ -163,6 +169,3 @@ are safe to publish as CI artifacts.
|
|
|
163
169
|
## License
|
|
164
170
|
|
|
165
171
|
Apache 2.0. See [LICENSE](LICENSE).
|
|
166
|
-
|
|
167
|
-
Built using the [GSD](https://github.com/gsd-build/get-shit-done) workflow
|
|
168
|
-
methodology (MIT).
|
|
@@ -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.1"
|
|
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" }
|
|
@@ -32,22 +32,62 @@ def get_adapter_by_name(name: str) -> LanguageAdapter:
|
|
|
32
32
|
return _REGISTRY[name]()
|
|
33
33
|
|
|
34
34
|
|
|
35
|
-
def
|
|
36
|
-
"""
|
|
35
|
+
def _collect_candidates(repo_path: Path) -> list[tuple[float, LanguageAdapter]]:
|
|
36
|
+
"""Collect all adapters with confidence > 0.0, sorted descending by (confidence, file_count)."""
|
|
37
37
|
candidates: list[tuple[float, LanguageAdapter]] = []
|
|
38
38
|
for cls in _REGISTRY.values():
|
|
39
39
|
adapter = cls()
|
|
40
40
|
confidence = adapter.detect(repo_path)
|
|
41
41
|
if confidence > 0.0:
|
|
42
42
|
candidates.append((confidence, adapter))
|
|
43
|
+
candidates.sort(
|
|
44
|
+
key=lambda pair: (pair[0], len(pair[1].find_production_files(repo_path))),
|
|
45
|
+
reverse=True,
|
|
46
|
+
)
|
|
47
|
+
return candidates
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def detect_primary(repo_path: Path) -> LanguageAdapter:
|
|
51
|
+
"""Try every adapter and return the one with the highest detect() confidence.
|
|
52
|
+
|
|
53
|
+
When multiple adapters tie on confidence, the adapter whose
|
|
54
|
+
``find_production_files`` returns more files wins — a repo with 20 .py
|
|
55
|
+
files and a single .sln fixture is almost certainly a Python project.
|
|
56
|
+
"""
|
|
57
|
+
candidates = _collect_candidates(repo_path)
|
|
43
58
|
if not candidates:
|
|
44
59
|
supported = ", ".join(sorted(_REGISTRY))
|
|
45
60
|
msg = f"No supported language detected in {repo_path}. Supported: {supported}."
|
|
46
61
|
raise NoAdapterError(f"{msg} Try using --language to force an adapter, or check that the repo contains a recognized project file.")
|
|
47
|
-
candidates.sort(key=lambda pair: pair[0], reverse=True)
|
|
48
62
|
return candidates[0][1]
|
|
49
63
|
|
|
50
64
|
|
|
65
|
+
def detect_all(repo_path: Path) -> list[tuple[float, LanguageAdapter]]:
|
|
66
|
+
"""Return all adapters that meet the detection threshold.
|
|
67
|
+
|
|
68
|
+
An adapter is included when ``confidence >= 0.5`` AND it has
|
|
69
|
+
``file_count >= 3`` production files under ``repo_path``. This filters
|
|
70
|
+
out repos where a language appears only as tooling sprinkles (e.g. a
|
|
71
|
+
single Makefile helper script in an otherwise Go repo).
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
A list of ``(confidence, adapter)`` tuples, sorted descending by
|
|
75
|
+
``(confidence, file_count)``. Returns an empty list when no adapter
|
|
76
|
+
meets the threshold.
|
|
77
|
+
"""
|
|
78
|
+
_CONFIDENCE_FLOOR = 0.5
|
|
79
|
+
_FILE_COUNT_FLOOR = 3
|
|
80
|
+
|
|
81
|
+
result: list[tuple[float, LanguageAdapter]] = []
|
|
82
|
+
for confidence, adapter in _collect_candidates(repo_path):
|
|
83
|
+
if confidence < _CONFIDENCE_FLOOR:
|
|
84
|
+
continue
|
|
85
|
+
if len(adapter.find_production_files(repo_path)) < _FILE_COUNT_FLOOR:
|
|
86
|
+
continue
|
|
87
|
+
result.append((confidence, adapter))
|
|
88
|
+
return result
|
|
89
|
+
|
|
90
|
+
|
|
51
91
|
__all__ = [
|
|
52
92
|
"CSharpAdapter",
|
|
53
93
|
"Declaration",
|
|
@@ -59,6 +99,7 @@ __all__ = [
|
|
|
59
99
|
"RustAdapter",
|
|
60
100
|
"ThrowSite",
|
|
61
101
|
"TypeScriptAdapter",
|
|
102
|
+
"detect_all",
|
|
62
103
|
"detect_primary",
|
|
63
104
|
"get_adapter_by_name",
|
|
64
105
|
]
|
|
@@ -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
|
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
"""AgentRepoCoach CLI entry point."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json as _json
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from . import VERSION
|
|
10
|
+
from .adapters import NoAdapterError, _REGISTRY
|
|
11
|
+
from .compute import compute_cah, compute_cah_all
|
|
12
|
+
from .config import Config, ConfigError, load_config
|
|
13
|
+
from .output import (
|
|
14
|
+
format_comparison,
|
|
15
|
+
format_comparison_markdown,
|
|
16
|
+
format_summary,
|
|
17
|
+
format_verbose,
|
|
18
|
+
render_json,
|
|
19
|
+
render_markdown_comment,
|
|
20
|
+
write_json,
|
|
21
|
+
write_markdown_comment,
|
|
22
|
+
write_prometheus,
|
|
23
|
+
)
|
|
24
|
+
from .pr_bot import compare_scores, format_pr_comment, parse_score_output
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
28
|
+
"""Build the argument parser for the ``agentrepocoach`` CLI."""
|
|
29
|
+
parser = argparse.ArgumentParser(
|
|
30
|
+
prog="agentrepocoach",
|
|
31
|
+
description="Compute the Codebase Agent Health (CAH) composite score for a repository.",
|
|
32
|
+
)
|
|
33
|
+
parser.add_argument(
|
|
34
|
+
"--repo",
|
|
35
|
+
type=Path,
|
|
36
|
+
default=None,
|
|
37
|
+
help="Path to the repository to score (default: current directory). "
|
|
38
|
+
"You may also pass the path as a positional argument: agentrepocoach [PATH].",
|
|
39
|
+
)
|
|
40
|
+
parser.add_argument(
|
|
41
|
+
"--config",
|
|
42
|
+
type=Path,
|
|
43
|
+
default=None,
|
|
44
|
+
help="Explicit config file path (default: <repo>/.agentrepocoach.toml).",
|
|
45
|
+
)
|
|
46
|
+
_adapter_names = "|".join(sorted(_REGISTRY)) + "|auto"
|
|
47
|
+
lang_group = parser.add_mutually_exclusive_group()
|
|
48
|
+
lang_group.add_argument(
|
|
49
|
+
"--language",
|
|
50
|
+
type=str,
|
|
51
|
+
default=None,
|
|
52
|
+
help=f"Override language detection ({_adapter_names}). Mutually exclusive with --all-languages.",
|
|
53
|
+
)
|
|
54
|
+
lang_group.add_argument(
|
|
55
|
+
"--all-languages",
|
|
56
|
+
action="store_true",
|
|
57
|
+
default=False,
|
|
58
|
+
dest="all_languages",
|
|
59
|
+
help="Score every detected language above threshold. Mutually exclusive with --language.",
|
|
60
|
+
)
|
|
61
|
+
parser.add_argument("--json", type=Path, help="Write full JSON result to this path.")
|
|
62
|
+
parser.add_argument("--prometheus", type=Path, help="Write Prometheus metrics to this path.")
|
|
63
|
+
parser.add_argument("--comment", type=Path, help="Write a PR-comment markdown file to this path.")
|
|
64
|
+
parser.add_argument(
|
|
65
|
+
"--format",
|
|
66
|
+
choices=["json", "markdown", "both"],
|
|
67
|
+
default=None,
|
|
68
|
+
help="Output format. 'json' prints the full report (to stdout or --output), "
|
|
69
|
+
"'markdown' prints a PR-comment summary (to stdout or --output), "
|
|
70
|
+
"'both' writes both to --output (markdown path derived by swapping "
|
|
71
|
+
"the extension to .md; requires --output).",
|
|
72
|
+
)
|
|
73
|
+
parser.add_argument(
|
|
74
|
+
"--output",
|
|
75
|
+
type=Path,
|
|
76
|
+
default=None,
|
|
77
|
+
help="Output path for --format. Ignored if --format is not set.",
|
|
78
|
+
)
|
|
79
|
+
parser.add_argument(
|
|
80
|
+
"--compare",
|
|
81
|
+
type=Path,
|
|
82
|
+
default=None,
|
|
83
|
+
help="Path to a baseline JSON report. Prints a delta comparison instead of "
|
|
84
|
+
"the normal summary.",
|
|
85
|
+
)
|
|
86
|
+
parser.add_argument("--verbose", action="store_true", help="Print per-sub-component breakdown.")
|
|
87
|
+
parser.add_argument("--quiet", action="store_true", help="Print only the total score.")
|
|
88
|
+
parser.add_argument("--version", action="version", version=f"agentrepocoach {VERSION}")
|
|
89
|
+
|
|
90
|
+
# Subcommands
|
|
91
|
+
subparsers = parser.add_subparsers(dest="subcommand")
|
|
92
|
+
|
|
93
|
+
# compare subcommand
|
|
94
|
+
compare_parser = subparsers.add_parser(
|
|
95
|
+
"compare",
|
|
96
|
+
help="Compare two JSON score files and display deltas.",
|
|
97
|
+
)
|
|
98
|
+
compare_parser.add_argument(
|
|
99
|
+
"base_file",
|
|
100
|
+
type=Path,
|
|
101
|
+
help="Path to the base (target branch) JSON score file.",
|
|
102
|
+
)
|
|
103
|
+
compare_parser.add_argument(
|
|
104
|
+
"pr_file",
|
|
105
|
+
type=Path,
|
|
106
|
+
help="Path to the PR (source branch) JSON score file.",
|
|
107
|
+
)
|
|
108
|
+
compare_parser.add_argument(
|
|
109
|
+
"--json",
|
|
110
|
+
action="store_true",
|
|
111
|
+
dest="json_output",
|
|
112
|
+
help="Output raw comparison dict as JSON instead of markdown.",
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
return parser
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _run_compare(args: argparse.Namespace) -> int:
|
|
119
|
+
"""Execute the ``compare`` subcommand."""
|
|
120
|
+
base_path = args.base_file.resolve()
|
|
121
|
+
pr_path = args.pr_file.resolve()
|
|
122
|
+
|
|
123
|
+
if not base_path.is_file():
|
|
124
|
+
print(f"error: base file does not exist: {base_path}", file=sys.stderr)
|
|
125
|
+
return 2
|
|
126
|
+
if not pr_path.is_file():
|
|
127
|
+
print(f"error: pr file does not exist: {pr_path}", file=sys.stderr)
|
|
128
|
+
return 2
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
base_scores = parse_score_output(base_path.read_text())
|
|
132
|
+
except ValueError as exc:
|
|
133
|
+
print(f"error: failed to parse base file: {exc}", file=sys.stderr)
|
|
134
|
+
return 2
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
pr_scores = parse_score_output(pr_path.read_text())
|
|
138
|
+
except ValueError as exc:
|
|
139
|
+
print(f"error: failed to parse pr file: {exc}", file=sys.stderr)
|
|
140
|
+
return 2
|
|
141
|
+
|
|
142
|
+
comparison = compare_scores(base_scores, pr_scores)
|
|
143
|
+
|
|
144
|
+
if args.json_output:
|
|
145
|
+
print(_json.dumps(comparison, indent=2))
|
|
146
|
+
else:
|
|
147
|
+
print(format_pr_comment(comparison))
|
|
148
|
+
|
|
149
|
+
return 0
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def main(argv: list[str] | None = None) -> int:
|
|
153
|
+
"""Run the CLI, parse arguments, compute the CAH score, and write outputs."""
|
|
154
|
+
parser = build_parser()
|
|
155
|
+
|
|
156
|
+
# Pre-process argv to support positional path: `agentrepocoach .`
|
|
157
|
+
# If the first non-option argument is not a known subcommand and looks like
|
|
158
|
+
# a path (not starting with '-'), inject it as `--repo` so argparse handles
|
|
159
|
+
# the rest normally. This preserves full backward-compat with `--repo` and
|
|
160
|
+
# with subcommands like `compare`.
|
|
161
|
+
_argv = list(argv) if argv is not None else sys.argv[1:]
|
|
162
|
+
_known_subcommands = {"compare"}
|
|
163
|
+
_positional_path: Path | None = None
|
|
164
|
+
if _argv and not _argv[0].startswith("-") and _argv[0] not in _known_subcommands:
|
|
165
|
+
_positional_path = Path(_argv[0])
|
|
166
|
+
_argv = _argv[1:]
|
|
167
|
+
|
|
168
|
+
args = parser.parse_args(_argv)
|
|
169
|
+
|
|
170
|
+
# Dispatch subcommands
|
|
171
|
+
if args.subcommand == "compare":
|
|
172
|
+
return _run_compare(args)
|
|
173
|
+
|
|
174
|
+
# Resolve repo path: --repo wins over positional; default is cwd.
|
|
175
|
+
if args.repo is not None and _positional_path is not None:
|
|
176
|
+
print(
|
|
177
|
+
"notice: both --repo and positional PATH given; --repo takes precedence.",
|
|
178
|
+
file=sys.stderr,
|
|
179
|
+
)
|
|
180
|
+
resolved_repo = args.repo
|
|
181
|
+
elif args.repo is not None:
|
|
182
|
+
resolved_repo = args.repo
|
|
183
|
+
elif _positional_path is not None:
|
|
184
|
+
resolved_repo = _positional_path
|
|
185
|
+
else:
|
|
186
|
+
resolved_repo = Path.cwd()
|
|
187
|
+
|
|
188
|
+
repo_root = resolved_repo.resolve()
|
|
189
|
+
if not repo_root.is_dir():
|
|
190
|
+
print(f"error: repo path is not a directory: {repo_root}", file=sys.stderr)
|
|
191
|
+
return 2
|
|
192
|
+
|
|
193
|
+
try:
|
|
194
|
+
config = load_config(repo_root, config_path=args.config)
|
|
195
|
+
except ConfigError as exc:
|
|
196
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
197
|
+
return 2
|
|
198
|
+
|
|
199
|
+
if args.language:
|
|
200
|
+
# Replace the config's language field. Dataclass is frozen -> rebuild.
|
|
201
|
+
from dataclasses import replace as _replace
|
|
202
|
+
config = _replace(config, language=args.language)
|
|
203
|
+
|
|
204
|
+
if args.all_languages:
|
|
205
|
+
return _run_all_languages(repo_root, config, args)
|
|
206
|
+
|
|
207
|
+
try:
|
|
208
|
+
result = compute_cah(repo_root, config=config)
|
|
209
|
+
except NoAdapterError as exc:
|
|
210
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
211
|
+
return 2
|
|
212
|
+
|
|
213
|
+
# When --format is used without --output, the formatted content replaces
|
|
214
|
+
# the default terminal summary on stdout.
|
|
215
|
+
stdout_replaced_by_format = args.format and not args.output
|
|
216
|
+
|
|
217
|
+
if args.compare:
|
|
218
|
+
baseline_path = args.compare.resolve()
|
|
219
|
+
if not baseline_path.is_file():
|
|
220
|
+
print(f"error: baseline report not found: {baseline_path}", file=sys.stderr)
|
|
221
|
+
return 2
|
|
222
|
+
import json as _json
|
|
223
|
+
baseline = _json.loads(baseline_path.read_text())
|
|
224
|
+
if args.quiet:
|
|
225
|
+
delta = result["total"] - baseline["total"]
|
|
226
|
+
print(f"{delta:+.2f}")
|
|
227
|
+
else:
|
|
228
|
+
print(format_comparison(result, baseline))
|
|
229
|
+
elif stdout_replaced_by_format:
|
|
230
|
+
pass # handled below in the --format block
|
|
231
|
+
elif args.quiet:
|
|
232
|
+
print(f"{result['total']:.2f}")
|
|
233
|
+
elif args.verbose:
|
|
234
|
+
print(format_verbose(result))
|
|
235
|
+
else:
|
|
236
|
+
print(format_summary(result))
|
|
237
|
+
|
|
238
|
+
if args.json:
|
|
239
|
+
write_json(result, args.json)
|
|
240
|
+
if not args.quiet:
|
|
241
|
+
print(f"\nJSON report written to {args.json}")
|
|
242
|
+
|
|
243
|
+
if args.prometheus:
|
|
244
|
+
write_prometheus(result, args.prometheus)
|
|
245
|
+
if not args.quiet:
|
|
246
|
+
print(f"Prometheus metrics written to {args.prometheus}")
|
|
247
|
+
|
|
248
|
+
if args.comment:
|
|
249
|
+
write_markdown_comment(result, args.comment)
|
|
250
|
+
if not args.quiet:
|
|
251
|
+
print(f"PR comment written to {args.comment}")
|
|
252
|
+
|
|
253
|
+
if args.format and args.output:
|
|
254
|
+
_write_formatted(result, args.format, args.output, quiet=args.quiet)
|
|
255
|
+
elif args.format and not args.output:
|
|
256
|
+
if args.format == "both":
|
|
257
|
+
print("error: --format both requires --output", file=sys.stderr)
|
|
258
|
+
return 2
|
|
259
|
+
_print_formatted(result, args.format)
|
|
260
|
+
|
|
261
|
+
return 0
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _run_all_languages(
|
|
265
|
+
repo_root: Path,
|
|
266
|
+
config: Config,
|
|
267
|
+
args: argparse.Namespace,
|
|
268
|
+
) -> int:
|
|
269
|
+
"""Handle the --all-languages code path."""
|
|
270
|
+
try:
|
|
271
|
+
result = compute_cah_all(repo_root, config=config)
|
|
272
|
+
except (NoAdapterError, RuntimeError) as exc:
|
|
273
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
274
|
+
return 2
|
|
275
|
+
|
|
276
|
+
languages: dict = result.get("languages", {})
|
|
277
|
+
|
|
278
|
+
if not languages:
|
|
279
|
+
print("No language met the detection threshold (confidence >= 0.5, file count >= 3).", file=sys.stderr)
|
|
280
|
+
return 2
|
|
281
|
+
|
|
282
|
+
# Text output: one summary block per language, separated by a header line.
|
|
283
|
+
if not args.quiet:
|
|
284
|
+
first = True
|
|
285
|
+
for lang_name, lang_result in languages.items():
|
|
286
|
+
if not first:
|
|
287
|
+
print()
|
|
288
|
+
print(f"=== Language: {lang_name} ===")
|
|
289
|
+
print(format_summary(lang_result))
|
|
290
|
+
first = False
|
|
291
|
+
|
|
292
|
+
# JSON file output: write the nested multi-language shape.
|
|
293
|
+
if args.json:
|
|
294
|
+
write_json(result, args.json)
|
|
295
|
+
if not args.quiet:
|
|
296
|
+
print(f"\nJSON report written to {args.json}")
|
|
297
|
+
|
|
298
|
+
# --format json to stdout.
|
|
299
|
+
if args.format == "json" and not args.output:
|
|
300
|
+
print(render_json(result))
|
|
301
|
+
elif args.format == "json" and args.output:
|
|
302
|
+
write_json(result, args.output)
|
|
303
|
+
if not args.quiet:
|
|
304
|
+
print(f"\nJSON report written to {args.output}")
|
|
305
|
+
|
|
306
|
+
return 0
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _print_formatted(result: dict, fmt: str) -> None:
|
|
310
|
+
"""Print formatted output to stdout when --output is not provided."""
|
|
311
|
+
if fmt == "json":
|
|
312
|
+
print(render_json(result))
|
|
313
|
+
elif fmt == "markdown":
|
|
314
|
+
print(render_markdown_comment(result))
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _write_formatted(
|
|
318
|
+
result: dict,
|
|
319
|
+
fmt: str,
|
|
320
|
+
output: Path,
|
|
321
|
+
*,
|
|
322
|
+
quiet: bool,
|
|
323
|
+
) -> None:
|
|
324
|
+
"""Dispatch --format/--output combinations to the underlying writers."""
|
|
325
|
+
if fmt == "json":
|
|
326
|
+
write_json(result, output)
|
|
327
|
+
if not quiet:
|
|
328
|
+
print(f"\nJSON report written to {output}")
|
|
329
|
+
return
|
|
330
|
+
if fmt == "markdown":
|
|
331
|
+
write_markdown_comment(result, output)
|
|
332
|
+
if not quiet:
|
|
333
|
+
print(f"\nMarkdown report written to {output}")
|
|
334
|
+
return
|
|
335
|
+
# fmt == "both"
|
|
336
|
+
json_path = output
|
|
337
|
+
markdown_path = output.with_suffix(".md")
|
|
338
|
+
write_json(result, json_path)
|
|
339
|
+
write_markdown_comment(result, markdown_path)
|
|
340
|
+
if not quiet:
|
|
341
|
+
print(f"\nJSON report written to {json_path}")
|
|
342
|
+
print(f"Markdown report written to {markdown_path}")
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
if __name__ == "__main__":
|
|
346
|
+
sys.exit(main())
|
{agentrepocoach-0.2.0 → agentrepocoach-0.3.1}/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
|
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
from typing import Any
|
|
6
6
|
|
|
7
|
-
from .adapters import LanguageAdapter, detect_primary, get_adapter_by_name
|
|
7
|
+
from .adapters import LanguageAdapter, detect_all, detect_primary, get_adapter_by_name
|
|
8
8
|
from .components import (
|
|
9
9
|
compute_decision_queryability,
|
|
10
10
|
compute_error_quality,
|
|
@@ -82,3 +82,59 @@ def _pick_adapter(repo_root: Path, config: Config) -> LanguageAdapter:
|
|
|
82
82
|
if config.language and config.language != "auto":
|
|
83
83
|
return get_adapter_by_name(config.language)
|
|
84
84
|
return detect_primary(repo_root)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# Schema version for the multi-language output shape.
|
|
88
|
+
_MULTI_LANGUAGE_SCHEMA_VERSION = 2
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def compute_cah_all(repo_root: Path, config: Config | None = None) -> dict[str, Any]:
|
|
92
|
+
"""Compute CAH scores for every language that meets the detection threshold.
|
|
93
|
+
|
|
94
|
+
Uses ``detect_all()`` to find adapters with ``confidence >= 0.5`` AND
|
|
95
|
+
``file_count >= 3``, then calls ``compute_cah()`` once per adapter.
|
|
96
|
+
|
|
97
|
+
The returned dict uses a nested shape distinct from the single-language
|
|
98
|
+
shape so downstream consumers can distinguish the two without ambiguity:
|
|
99
|
+
|
|
100
|
+
.. code-block:: json
|
|
101
|
+
|
|
102
|
+
{
|
|
103
|
+
"schema_version": 2,
|
|
104
|
+
"generator": "agentrepocoach <version>",
|
|
105
|
+
"languages": {
|
|
106
|
+
"python": {"total": 72.4, "components": {...}, "language": "python"},
|
|
107
|
+
"typescript": {"total": 61.2, "components": {...}, "language": "typescript"}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
Note: the top-level ``"total"`` and ``"language"`` keys are intentionally
|
|
112
|
+
**absent** — use the per-language sub-dicts for those values.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
repo_root: Path to the repository to score.
|
|
116
|
+
config: Optional explicit config. If None, loads from
|
|
117
|
+
``<repo_root>/.agentrepocoach.toml`` with defaults.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
Multi-language result dict as described above. ``"languages"`` is
|
|
121
|
+
empty when no adapter meets the threshold.
|
|
122
|
+
"""
|
|
123
|
+
from . import VERSION # local import to avoid circular reference
|
|
124
|
+
|
|
125
|
+
repo_root = repo_root.resolve()
|
|
126
|
+
if config is None:
|
|
127
|
+
config = load_config(repo_root)
|
|
128
|
+
|
|
129
|
+
adapters = detect_all(repo_root)
|
|
130
|
+
|
|
131
|
+
per_language: dict[str, Any] = {}
|
|
132
|
+
for _confidence, adapter in adapters:
|
|
133
|
+
lang_result = compute_cah(repo_root, config=config, adapter=adapter)
|
|
134
|
+
per_language[adapter.name] = lang_result
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
"schema_version": _MULTI_LANGUAGE_SCHEMA_VERSION,
|
|
138
|
+
"generator": f"{_GENERATOR_NAME} {VERSION}",
|
|
139
|
+
"languages": per_language,
|
|
140
|
+
}
|