agentrepocoach 0.3.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.3.0 → agentrepocoach-0.3.1}/PKG-INFO +1 -4
- {agentrepocoach-0.3.0 → agentrepocoach-0.3.1}/README.md +0 -3
- {agentrepocoach-0.3.0 → agentrepocoach-0.3.1}/pyproject.toml +1 -1
- {agentrepocoach-0.3.0 → agentrepocoach-0.3.1}/src/agentrepocoach/__init__.py +1 -1
- {agentrepocoach-0.3.0 → agentrepocoach-0.3.1}/src/agentrepocoach/adapters/__init__.py +43 -12
- {agentrepocoach-0.3.0 → agentrepocoach-0.3.1}/src/agentrepocoach/cli.py +94 -9
- {agentrepocoach-0.3.0 → agentrepocoach-0.3.1}/src/agentrepocoach/compute.py +57 -1
- {agentrepocoach-0.3.0 → agentrepocoach-0.3.1}/src/agentrepocoach.egg-info/PKG-INFO +1 -4
- {agentrepocoach-0.3.0 → agentrepocoach-0.3.1}/src/agentrepocoach.egg-info/SOURCES.txt +2 -0
- agentrepocoach-0.3.1/tests/test_cli.py +108 -0
- agentrepocoach-0.3.1/tests/test_multi_language.py +197 -0
- {agentrepocoach-0.3.0 → agentrepocoach-0.3.1}/LICENSE +0 -0
- {agentrepocoach-0.3.0 → agentrepocoach-0.3.1}/setup.cfg +0 -0
- {agentrepocoach-0.3.0 → agentrepocoach-0.3.1}/src/agentrepocoach/__main__.py +0 -0
- {agentrepocoach-0.3.0 → agentrepocoach-0.3.1}/src/agentrepocoach/adapters/base.py +0 -0
- {agentrepocoach-0.3.0 → agentrepocoach-0.3.1}/src/agentrepocoach/adapters/csharp.py +0 -0
- {agentrepocoach-0.3.0 → agentrepocoach-0.3.1}/src/agentrepocoach/adapters/go.py +0 -0
- {agentrepocoach-0.3.0 → agentrepocoach-0.3.1}/src/agentrepocoach/adapters/python.py +0 -0
- {agentrepocoach-0.3.0 → agentrepocoach-0.3.1}/src/agentrepocoach/adapters/rust.py +0 -0
- {agentrepocoach-0.3.0 → agentrepocoach-0.3.1}/src/agentrepocoach/adapters/typescript.py +0 -0
- {agentrepocoach-0.3.0 → agentrepocoach-0.3.1}/src/agentrepocoach/components/__init__.py +0 -0
- {agentrepocoach-0.3.0 → agentrepocoach-0.3.1}/src/agentrepocoach/components/decision_queryability.py +0 -0
- {agentrepocoach-0.3.0 → agentrepocoach-0.3.1}/src/agentrepocoach/components/documentation.py +0 -0
- {agentrepocoach-0.3.0 → agentrepocoach-0.3.1}/src/agentrepocoach/components/error_quality.py +0 -0
- {agentrepocoach-0.3.0 → agentrepocoach-0.3.1}/src/agentrepocoach/components/module_hygiene.py +0 -0
- {agentrepocoach-0.3.0 → agentrepocoach-0.3.1}/src/agentrepocoach/components/test_quality.py +0 -0
- {agentrepocoach-0.3.0 → agentrepocoach-0.3.1}/src/agentrepocoach/config.py +0 -0
- {agentrepocoach-0.3.0 → agentrepocoach-0.3.1}/src/agentrepocoach/output.py +0 -0
- {agentrepocoach-0.3.0 → agentrepocoach-0.3.1}/src/agentrepocoach/pr_bot.py +0 -0
- {agentrepocoach-0.3.0 → agentrepocoach-0.3.1}/src/agentrepocoach/regex_safety.py +0 -0
- {agentrepocoach-0.3.0 → agentrepocoach-0.3.1}/src/agentrepocoach/scoring.py +0 -0
- {agentrepocoach-0.3.0 → agentrepocoach-0.3.1}/src/agentrepocoach.egg-info/dependency_links.txt +0 -0
- {agentrepocoach-0.3.0 → agentrepocoach-0.3.1}/src/agentrepocoach.egg-info/entry_points.txt +0 -0
- {agentrepocoach-0.3.0 → agentrepocoach-0.3.1}/src/agentrepocoach.egg-info/requires.txt +0 -0
- {agentrepocoach-0.3.0 → agentrepocoach-0.3.1}/src/agentrepocoach.egg-info/top_level.txt +0 -0
- {agentrepocoach-0.3.0 → agentrepocoach-0.3.1}/tests/test_adapters.py +0 -0
- {agentrepocoach-0.3.0 → agentrepocoach-0.3.1}/tests/test_cli_compare.py +0 -0
- {agentrepocoach-0.3.0 → agentrepocoach-0.3.1}/tests/test_components.py +0 -0
- {agentrepocoach-0.3.0 → agentrepocoach-0.3.1}/tests/test_config.py +0 -0
- {agentrepocoach-0.3.0 → agentrepocoach-0.3.1}/tests/test_output.py +0 -0
- {agentrepocoach-0.3.0 → agentrepocoach-0.3.1}/tests/test_pr_bot.py +0 -0
- {agentrepocoach-0.3.0 → agentrepocoach-0.3.1}/tests/test_regex_safety.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentrepocoach
|
|
3
|
-
Version: 0.3.
|
|
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
|
|
@@ -203,6 +203,3 @@ are safe to publish as CI artifacts.
|
|
|
203
203
|
## License
|
|
204
204
|
|
|
205
205
|
Apache 2.0. See [LICENSE](LICENSE).
|
|
206
|
-
|
|
207
|
-
Built using the [GSD](https://github.com/gsd-build/get-shit-done) workflow
|
|
208
|
-
methodology (MIT).
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "agentrepocoach"
|
|
7
|
-
version = "0.3.
|
|
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,6 +32,21 @@ def get_adapter_by_name(name: str) -> LanguageAdapter:
|
|
|
32
32
|
return _REGISTRY[name]()
|
|
33
33
|
|
|
34
34
|
|
|
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
|
+
candidates: list[tuple[float, LanguageAdapter]] = []
|
|
38
|
+
for cls in _REGISTRY.values():
|
|
39
|
+
adapter = cls()
|
|
40
|
+
confidence = adapter.detect(repo_path)
|
|
41
|
+
if confidence > 0.0:
|
|
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
|
+
|
|
35
50
|
def detect_primary(repo_path: Path) -> LanguageAdapter:
|
|
36
51
|
"""Try every adapter and return the one with the highest detect() confidence.
|
|
37
52
|
|
|
@@ -39,25 +54,40 @@ def detect_primary(repo_path: Path) -> LanguageAdapter:
|
|
|
39
54
|
``find_production_files`` returns more files wins — a repo with 20 .py
|
|
40
55
|
files and a single .sln fixture is almost certainly a Python project.
|
|
41
56
|
"""
|
|
42
|
-
candidates
|
|
43
|
-
for cls in _REGISTRY.values():
|
|
44
|
-
adapter = cls()
|
|
45
|
-
confidence = adapter.detect(repo_path)
|
|
46
|
-
if confidence > 0.0:
|
|
47
|
-
candidates.append((confidence, adapter))
|
|
57
|
+
candidates = _collect_candidates(repo_path)
|
|
48
58
|
if not candidates:
|
|
49
59
|
supported = ", ".join(sorted(_REGISTRY))
|
|
50
60
|
msg = f"No supported language detected in {repo_path}. Supported: {supported}."
|
|
51
61
|
raise NoAdapterError(f"{msg} Try using --language to force an adapter, or check that the repo contains a recognized project file.")
|
|
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
|
-
)
|
|
58
62
|
return candidates[0][1]
|
|
59
63
|
|
|
60
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
|
+
|
|
61
91
|
__all__ = [
|
|
62
92
|
"CSharpAdapter",
|
|
63
93
|
"Declaration",
|
|
@@ -69,6 +99,7 @@ __all__ = [
|
|
|
69
99
|
"RustAdapter",
|
|
70
100
|
"ThrowSite",
|
|
71
101
|
"TypeScriptAdapter",
|
|
102
|
+
"detect_all",
|
|
72
103
|
"detect_primary",
|
|
73
104
|
"get_adapter_by_name",
|
|
74
105
|
]
|
|
@@ -7,9 +7,9 @@ import sys
|
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
|
|
9
9
|
from . import VERSION
|
|
10
|
-
from .adapters import NoAdapterError
|
|
11
|
-
from .compute import compute_cah
|
|
12
|
-
from .config import ConfigError, load_config
|
|
10
|
+
from .adapters import NoAdapterError, _REGISTRY
|
|
11
|
+
from .compute import compute_cah, compute_cah_all
|
|
12
|
+
from .config import Config, ConfigError, load_config
|
|
13
13
|
from .output import (
|
|
14
14
|
format_comparison,
|
|
15
15
|
format_comparison_markdown,
|
|
@@ -33,8 +33,9 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
33
33
|
parser.add_argument(
|
|
34
34
|
"--repo",
|
|
35
35
|
type=Path,
|
|
36
|
-
default=
|
|
37
|
-
help="Path to the repository to score (default: current directory)."
|
|
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].",
|
|
38
39
|
)
|
|
39
40
|
parser.add_argument(
|
|
40
41
|
"--config",
|
|
@@ -42,11 +43,20 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
42
43
|
default=None,
|
|
43
44
|
help="Explicit config file path (default: <repo>/.agentrepocoach.toml).",
|
|
44
45
|
)
|
|
45
|
-
|
|
46
|
+
_adapter_names = "|".join(sorted(_REGISTRY)) + "|auto"
|
|
47
|
+
lang_group = parser.add_mutually_exclusive_group()
|
|
48
|
+
lang_group.add_argument(
|
|
46
49
|
"--language",
|
|
47
50
|
type=str,
|
|
48
51
|
default=None,
|
|
49
|
-
help="Override language detection (
|
|
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.",
|
|
50
60
|
)
|
|
51
61
|
parser.add_argument("--json", type=Path, help="Write full JSON result to this path.")
|
|
52
62
|
parser.add_argument("--prometheus", type=Path, help="Write Prometheus metrics to this path.")
|
|
@@ -142,13 +152,40 @@ def _run_compare(args: argparse.Namespace) -> int:
|
|
|
142
152
|
def main(argv: list[str] | None = None) -> int:
|
|
143
153
|
"""Run the CLI, parse arguments, compute the CAH score, and write outputs."""
|
|
144
154
|
parser = build_parser()
|
|
145
|
-
|
|
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)
|
|
146
169
|
|
|
147
170
|
# Dispatch subcommands
|
|
148
171
|
if args.subcommand == "compare":
|
|
149
172
|
return _run_compare(args)
|
|
150
173
|
|
|
151
|
-
|
|
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()
|
|
152
189
|
if not repo_root.is_dir():
|
|
153
190
|
print(f"error: repo path is not a directory: {repo_root}", file=sys.stderr)
|
|
154
191
|
return 2
|
|
@@ -164,6 +201,9 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
164
201
|
from dataclasses import replace as _replace
|
|
165
202
|
config = _replace(config, language=args.language)
|
|
166
203
|
|
|
204
|
+
if args.all_languages:
|
|
205
|
+
return _run_all_languages(repo_root, config, args)
|
|
206
|
+
|
|
167
207
|
try:
|
|
168
208
|
result = compute_cah(repo_root, config=config)
|
|
169
209
|
except NoAdapterError as exc:
|
|
@@ -221,6 +261,51 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
221
261
|
return 0
|
|
222
262
|
|
|
223
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
|
+
|
|
224
309
|
def _print_formatted(result: dict, fmt: str) -> None:
|
|
225
310
|
"""Print formatted output to stdout when --output is not provided."""
|
|
226
311
|
if fmt == "json":
|
|
@@ -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
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentrepocoach
|
|
3
|
-
Version: 0.3.
|
|
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
|
|
@@ -203,6 +203,3 @@ are safe to publish as CI artifacts.
|
|
|
203
203
|
## License
|
|
204
204
|
|
|
205
205
|
Apache 2.0. See [LICENSE](LICENSE).
|
|
206
|
-
|
|
207
|
-
Built using the [GSD](https://github.com/gsd-build/get-shit-done) workflow
|
|
208
|
-
methodology (MIT).
|
|
@@ -30,9 +30,11 @@ src/agentrepocoach/components/error_quality.py
|
|
|
30
30
|
src/agentrepocoach/components/module_hygiene.py
|
|
31
31
|
src/agentrepocoach/components/test_quality.py
|
|
32
32
|
tests/test_adapters.py
|
|
33
|
+
tests/test_cli.py
|
|
33
34
|
tests/test_cli_compare.py
|
|
34
35
|
tests/test_components.py
|
|
35
36
|
tests/test_config.py
|
|
37
|
+
tests/test_multi_language.py
|
|
36
38
|
tests/test_output.py
|
|
37
39
|
tests/test_pr_bot.py
|
|
38
40
|
tests/test_regex_safety.py
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Tests for v0.3.1 release-integrity fixes.
|
|
2
|
+
|
|
3
|
+
AC-01: --all-languages flag exists in --help output
|
|
4
|
+
AC-02: --language help text lists all registered adapters
|
|
5
|
+
AC-03: agentrepocoach <positional-path> works without --repo
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from unittest.mock import patch
|
|
13
|
+
|
|
14
|
+
import pytest
|
|
15
|
+
|
|
16
|
+
from agentrepocoach.cli import main
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
FIXTURES_ROOT = Path(__file__).parent / "fixtures"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TestAllLanguagesFlagExists:
|
|
23
|
+
"""AC-01: --all-languages flag must appear in --help output."""
|
|
24
|
+
|
|
25
|
+
def test_all_languages_flag_exists(self) -> None:
|
|
26
|
+
"""Invoke --help via subprocess and assert --all-languages is listed."""
|
|
27
|
+
result = subprocess.run(
|
|
28
|
+
[sys.executable, "-m", "agentrepocoach", "--help"],
|
|
29
|
+
capture_output=True,
|
|
30
|
+
text=True,
|
|
31
|
+
)
|
|
32
|
+
assert result.returncode == 0
|
|
33
|
+
assert "--all-languages" in result.stdout
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class TestLanguageHelpListsAllAdapters:
|
|
37
|
+
"""AC-02: --language help text must list all registered adapter names."""
|
|
38
|
+
|
|
39
|
+
def test_language_help_lists_all_adapters(self) -> None:
|
|
40
|
+
"""Invoke --help, assert each adapter name appears in --language help text."""
|
|
41
|
+
result = subprocess.run(
|
|
42
|
+
[sys.executable, "-m", "agentrepocoach", "--help"],
|
|
43
|
+
capture_output=True,
|
|
44
|
+
text=True,
|
|
45
|
+
)
|
|
46
|
+
assert result.returncode == 0
|
|
47
|
+
stdout = result.stdout
|
|
48
|
+
# All five adapter names must appear in the help output
|
|
49
|
+
for adapter_name in ("csharp", "go", "python", "rust", "typescript"):
|
|
50
|
+
assert adapter_name in stdout, (
|
|
51
|
+
f"Adapter '{adapter_name}' missing from --help output. "
|
|
52
|
+
f"Full stdout:\n{stdout}"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class TestPositionalPathArgument:
|
|
57
|
+
"""AC-03: agentrepocoach <path> (positional) must succeed and produce a score."""
|
|
58
|
+
|
|
59
|
+
def test_positional_path_argument(self, capsys: pytest.CaptureFixture[str]) -> None:
|
|
60
|
+
"""Invoke main with positional path (no --repo), assert exit 0 and score produced."""
|
|
61
|
+
python_repo = FIXTURES_ROOT / "sample-python-repo"
|
|
62
|
+
mock_result = {
|
|
63
|
+
"total": 72.00,
|
|
64
|
+
"language": "python",
|
|
65
|
+
"weights": {"navigability": 0.20},
|
|
66
|
+
"components": {
|
|
67
|
+
"navigability": {"score": 72.0, "breakdown": {}},
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
with patch("agentrepocoach.cli.compute_cah", return_value=mock_result):
|
|
71
|
+
rc = main([str(python_repo)])
|
|
72
|
+
assert rc == 0
|
|
73
|
+
out = capsys.readouterr().out
|
|
74
|
+
assert "72.00" in out
|
|
75
|
+
|
|
76
|
+
def test_positional_path_dot_uses_cwd(self, capsys: pytest.CaptureFixture[str]) -> None:
|
|
77
|
+
"""agentrepocoach . should resolve to cwd without error."""
|
|
78
|
+
mock_result = {
|
|
79
|
+
"total": 65.00,
|
|
80
|
+
"language": "python",
|
|
81
|
+
"weights": {"navigability": 0.20},
|
|
82
|
+
"components": {
|
|
83
|
+
"navigability": {"score": 65.0, "breakdown": {}},
|
|
84
|
+
},
|
|
85
|
+
}
|
|
86
|
+
with patch("agentrepocoach.cli.compute_cah", return_value=mock_result):
|
|
87
|
+
rc = main(["."])
|
|
88
|
+
assert rc == 0
|
|
89
|
+
out = capsys.readouterr().out
|
|
90
|
+
assert "65.00" in out
|
|
91
|
+
|
|
92
|
+
def test_repo_flag_wins_over_positional(self, capsys: pytest.CaptureFixture[str]) -> None:
|
|
93
|
+
"""When both --repo and positional are given, --repo wins and a notice is printed."""
|
|
94
|
+
python_repo = FIXTURES_ROOT / "sample-python-repo"
|
|
95
|
+
mock_result = {
|
|
96
|
+
"total": 80.00,
|
|
97
|
+
"language": "python",
|
|
98
|
+
"weights": {"navigability": 0.20},
|
|
99
|
+
"components": {
|
|
100
|
+
"navigability": {"score": 80.0, "breakdown": {}},
|
|
101
|
+
},
|
|
102
|
+
}
|
|
103
|
+
with patch("agentrepocoach.cli.compute_cah", return_value=mock_result):
|
|
104
|
+
rc = main([str(python_repo), "--repo", str(python_repo)])
|
|
105
|
+
assert rc == 0
|
|
106
|
+
captured = capsys.readouterr()
|
|
107
|
+
# --repo takes precedence; notice on stderr
|
|
108
|
+
assert "takes precedence" in captured.err or "notice" in captured.err
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""Tests for multi-language scoring (detect_all, compute_cah_all, --all-languages flag).
|
|
2
|
+
|
|
3
|
+
Coverage maps to XPL-003-MVP acceptance criteria:
|
|
4
|
+
- AC-1: Threshold logic edge cases (confidence and file_count boundaries)
|
|
5
|
+
- AC-2: detect_all() positive — mixed-language fixture
|
|
6
|
+
- AC-3: detect_all() empty — nothing meets threshold
|
|
7
|
+
- AC-4: compute_cah_all() JSON shape contract
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Generator
|
|
13
|
+
from unittest.mock import patch
|
|
14
|
+
|
|
15
|
+
import pytest
|
|
16
|
+
|
|
17
|
+
from agentrepocoach.adapters import LanguageAdapter, PythonAdapter, detect_all
|
|
18
|
+
from agentrepocoach.compute import compute_cah_all, _MULTI_LANGUAGE_SCHEMA_VERSION
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
# Helpers / mini-fixtures
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _make_python_repo(root: Path, file_count: int) -> Path:
|
|
27
|
+
"""Scaffold a minimal Python repo with ``file_count`` .py source files."""
|
|
28
|
+
src = root / "src"
|
|
29
|
+
src.mkdir(parents=True)
|
|
30
|
+
(root / "pyproject.toml").write_text("[project]\nname = 'test'\n")
|
|
31
|
+
for i in range(file_count):
|
|
32
|
+
(src / f"module_{i}.py").write_text(f"def func_{i}(): pass\n")
|
|
33
|
+
return root
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
# AC-1: Threshold logic edge cases
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class TestDetectAllThresholdEdgeCases:
|
|
42
|
+
"""Confidence and file_count boundary conditions (AC-1)."""
|
|
43
|
+
|
|
44
|
+
def test_confidence_below_floor_excluded(self, tmp_path: Path) -> None:
|
|
45
|
+
"""Adapter with confidence=0.49 must be excluded."""
|
|
46
|
+
repo = _make_python_repo(tmp_path / "repo", file_count=5)
|
|
47
|
+
adapter = PythonAdapter()
|
|
48
|
+
with patch.object(type(adapter), "detect", return_value=0.49):
|
|
49
|
+
with patch("agentrepocoach.adapters._REGISTRY", {"python": lambda: adapter}):
|
|
50
|
+
result = detect_all(repo)
|
|
51
|
+
assert result == []
|
|
52
|
+
|
|
53
|
+
def test_confidence_at_floor_included(self, tmp_path: Path) -> None:
|
|
54
|
+
"""Adapter with confidence=0.50 and file_count>=3 must be included."""
|
|
55
|
+
repo = _make_python_repo(tmp_path / "repo", file_count=5)
|
|
56
|
+
adapter = PythonAdapter()
|
|
57
|
+
with patch.object(type(adapter), "detect", return_value=0.50):
|
|
58
|
+
with patch("agentrepocoach.adapters._REGISTRY", {"python": lambda: adapter}):
|
|
59
|
+
result = detect_all(repo)
|
|
60
|
+
assert len(result) == 1
|
|
61
|
+
assert result[0][0] == pytest.approx(0.50)
|
|
62
|
+
|
|
63
|
+
def test_file_count_below_floor_excluded(self, tmp_path: Path) -> None:
|
|
64
|
+
"""Adapter with file_count=2 (< 3) must be excluded even at confidence=1.0."""
|
|
65
|
+
repo = _make_python_repo(tmp_path / "repo", file_count=2)
|
|
66
|
+
# PythonAdapter.detect() will return 1.0 (pyproject.toml present)
|
|
67
|
+
# but only 2 production files exist → excluded
|
|
68
|
+
result = detect_all(repo)
|
|
69
|
+
assert result == []
|
|
70
|
+
|
|
71
|
+
def test_file_count_at_floor_included(self, tmp_path: Path) -> None:
|
|
72
|
+
"""Adapter with file_count=3 and confidence>=0.5 must be included."""
|
|
73
|
+
repo = _make_python_repo(tmp_path / "repo", file_count=3)
|
|
74
|
+
result = detect_all(repo)
|
|
75
|
+
assert len(result) == 1
|
|
76
|
+
assert result[0][1].name == "python"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# ---------------------------------------------------------------------------
|
|
80
|
+
# AC-2: detect_all() positive — multiple languages above threshold
|
|
81
|
+
# ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class TestDetectAllPositive:
|
|
85
|
+
"""detect_all() returns multiple adapters when the repo is multi-language (AC-2)."""
|
|
86
|
+
|
|
87
|
+
def test_detect_all_returns_multiple_adapters(self, tmp_path: Path) -> None:
|
|
88
|
+
"""A repo with Python AND Go source files above threshold returns both adapters.
|
|
89
|
+
|
|
90
|
+
PythonAdapter.find_production_files() looks in ``src/`` or ``lib/`` dirs.
|
|
91
|
+
GoAdapter.find_production_files() looks for ``*.go`` files.
|
|
92
|
+
"""
|
|
93
|
+
repo = tmp_path / "mixed"
|
|
94
|
+
repo.mkdir()
|
|
95
|
+
|
|
96
|
+
# Python side: pyproject.toml + 3 .py files under src/
|
|
97
|
+
(repo / "pyproject.toml").write_text("[project]\nname = 'mixed'\n")
|
|
98
|
+
py_src = repo / "src"
|
|
99
|
+
py_src.mkdir()
|
|
100
|
+
for i in range(3):
|
|
101
|
+
(py_src / f"module_{i}.py").write_text(f"def func_{i}(): pass\n")
|
|
102
|
+
|
|
103
|
+
# Go side: go.mod + 3 .go files
|
|
104
|
+
(repo / "go.mod").write_text("module example.com/mixed\ngo 1.21\n")
|
|
105
|
+
go_src = repo / "go_src"
|
|
106
|
+
go_src.mkdir()
|
|
107
|
+
for i in range(3):
|
|
108
|
+
(go_src / f"pkg{i}.go").write_text(f"package main\nfunc F{i}() {{}}\n")
|
|
109
|
+
|
|
110
|
+
result = detect_all(repo)
|
|
111
|
+
adapter_names = {adapter.name for _, adapter in result}
|
|
112
|
+
assert "python" in adapter_names
|
|
113
|
+
assert "go" in adapter_names
|
|
114
|
+
|
|
115
|
+
def test_detect_all_sorted_by_confidence_descending(self, tmp_path: Path) -> None:
|
|
116
|
+
"""Results are sorted confidence-descending."""
|
|
117
|
+
repo = _make_python_repo(tmp_path / "repo", file_count=5)
|
|
118
|
+
result = detect_all(repo)
|
|
119
|
+
# At least one result (python) must be present
|
|
120
|
+
assert result
|
|
121
|
+
confidences = [c for c, _ in result]
|
|
122
|
+
assert confidences == sorted(confidences, reverse=True)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# ---------------------------------------------------------------------------
|
|
126
|
+
# AC-3: detect_all() empty — no language meets threshold
|
|
127
|
+
# ---------------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class TestDetectAllEmpty:
|
|
131
|
+
"""detect_all() returns [] when nothing meets the detection threshold (AC-3)."""
|
|
132
|
+
|
|
133
|
+
def test_empty_repo_returns_empty_list(self, tmp_path: Path) -> None:
|
|
134
|
+
"""A repo with no recognised language files returns []."""
|
|
135
|
+
empty = tmp_path / "empty"
|
|
136
|
+
empty.mkdir()
|
|
137
|
+
result = detect_all(empty)
|
|
138
|
+
assert result == []
|
|
139
|
+
|
|
140
|
+
def test_below_file_count_threshold_returns_empty_list(self, tmp_path: Path) -> None:
|
|
141
|
+
"""A Python repo with only 2 production files (< 3) returns []."""
|
|
142
|
+
repo = _make_python_repo(tmp_path / "repo", file_count=2)
|
|
143
|
+
result = detect_all(repo)
|
|
144
|
+
assert result == []
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# ---------------------------------------------------------------------------
|
|
148
|
+
# AC-4: compute_cah_all() JSON shape contract
|
|
149
|
+
# ---------------------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class TestComputeCahAllShape:
|
|
153
|
+
"""compute_cah_all() output shape: no top-level total/language, nested languages dict (AC-4)."""
|
|
154
|
+
|
|
155
|
+
def test_no_top_level_total(self, tmp_path: Path) -> None:
|
|
156
|
+
"""Top-level 'total' key must be absent from multi-language output."""
|
|
157
|
+
repo = _make_python_repo(tmp_path / "repo", file_count=3)
|
|
158
|
+
result = compute_cah_all(repo)
|
|
159
|
+
assert "total" not in result
|
|
160
|
+
|
|
161
|
+
def test_no_top_level_language(self, tmp_path: Path) -> None:
|
|
162
|
+
"""Top-level 'language' key must be absent from multi-language output."""
|
|
163
|
+
repo = _make_python_repo(tmp_path / "repo", file_count=3)
|
|
164
|
+
result = compute_cah_all(repo)
|
|
165
|
+
assert "language" not in result
|
|
166
|
+
|
|
167
|
+
def test_languages_dict_present(self, tmp_path: Path) -> None:
|
|
168
|
+
"""Top-level 'languages' dict is present and non-empty for a qualifying repo."""
|
|
169
|
+
repo = _make_python_repo(tmp_path / "repo", file_count=3)
|
|
170
|
+
result = compute_cah_all(repo)
|
|
171
|
+
assert "languages" in result
|
|
172
|
+
assert isinstance(result["languages"], dict)
|
|
173
|
+
assert len(result["languages"]) >= 1
|
|
174
|
+
|
|
175
|
+
def test_languages_sub_shape(self, tmp_path: Path) -> None:
|
|
176
|
+
"""Each language entry has 'total' and 'language' sub-keys."""
|
|
177
|
+
repo = _make_python_repo(tmp_path / "repo", file_count=3)
|
|
178
|
+
result = compute_cah_all(repo)
|
|
179
|
+
for lang_name, lang_result in result["languages"].items():
|
|
180
|
+
assert "total" in lang_result, f"{lang_name} missing 'total'"
|
|
181
|
+
assert "language" in lang_result, f"{lang_name} missing 'language'"
|
|
182
|
+
assert lang_result["language"] == lang_name
|
|
183
|
+
|
|
184
|
+
def test_schema_version_is_bumped(self, tmp_path: Path) -> None:
|
|
185
|
+
"""schema_version must be the multi-language version (2), not the v0.3 value (1)."""
|
|
186
|
+
repo = _make_python_repo(tmp_path / "repo", file_count=3)
|
|
187
|
+
result = compute_cah_all(repo)
|
|
188
|
+
assert result["schema_version"] == _MULTI_LANGUAGE_SCHEMA_VERSION
|
|
189
|
+
assert result["schema_version"] == 2
|
|
190
|
+
|
|
191
|
+
def test_empty_repo_returns_empty_languages(self, tmp_path: Path) -> None:
|
|
192
|
+
"""When no language meets threshold, 'languages' is an empty dict."""
|
|
193
|
+
empty = tmp_path / "empty"
|
|
194
|
+
empty.mkdir()
|
|
195
|
+
result = compute_cah_all(empty)
|
|
196
|
+
assert result["languages"] == {}
|
|
197
|
+
assert "total" not in result
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentrepocoach-0.3.0 → agentrepocoach-0.3.1}/src/agentrepocoach/components/decision_queryability.py
RENAMED
|
File without changes
|
{agentrepocoach-0.3.0 → agentrepocoach-0.3.1}/src/agentrepocoach/components/documentation.py
RENAMED
|
File without changes
|
{agentrepocoach-0.3.0 → agentrepocoach-0.3.1}/src/agentrepocoach/components/error_quality.py
RENAMED
|
File without changes
|
{agentrepocoach-0.3.0 → agentrepocoach-0.3.1}/src/agentrepocoach/components/module_hygiene.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentrepocoach-0.3.0 → agentrepocoach-0.3.1}/src/agentrepocoach.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|