agentrepocoach 0.3.1__tar.gz → 0.4.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.3.1 → agentrepocoach-0.4.0}/PKG-INFO +48 -14
- {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/README.md +47 -13
- {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/pyproject.toml +1 -1
- {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/src/agentrepocoach/__init__.py +1 -1
- {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/src/agentrepocoach/components/__init__.py +3 -0
- agentrepocoach-0.4.0/src/agentrepocoach/components/bootstrap_signals.py +204 -0
- {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/src/agentrepocoach/compute.py +2 -0
- {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/src/agentrepocoach/config.py +89 -10
- {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/src/agentrepocoach.egg-info/PKG-INFO +48 -14
- {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/src/agentrepocoach.egg-info/SOURCES.txt +2 -0
- agentrepocoach-0.4.0/tests/test_bootstrap_signals_security.py +33 -0
- {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/tests/test_components.py +89 -1
- agentrepocoach-0.4.0/tests/test_config.py +197 -0
- agentrepocoach-0.3.1/tests/test_config.py +0 -101
- {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/LICENSE +0 -0
- {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/setup.cfg +0 -0
- {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/src/agentrepocoach/__main__.py +0 -0
- {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/src/agentrepocoach/adapters/__init__.py +0 -0
- {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/src/agentrepocoach/adapters/base.py +0 -0
- {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/src/agentrepocoach/adapters/csharp.py +0 -0
- {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/src/agentrepocoach/adapters/go.py +0 -0
- {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/src/agentrepocoach/adapters/python.py +0 -0
- {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/src/agentrepocoach/adapters/rust.py +0 -0
- {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/src/agentrepocoach/adapters/typescript.py +0 -0
- {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/src/agentrepocoach/cli.py +0 -0
- {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/src/agentrepocoach/components/decision_queryability.py +0 -0
- {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/src/agentrepocoach/components/documentation.py +0 -0
- {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/src/agentrepocoach/components/error_quality.py +0 -0
- {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/src/agentrepocoach/components/module_hygiene.py +0 -0
- {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/src/agentrepocoach/components/test_quality.py +0 -0
- {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/src/agentrepocoach/output.py +0 -0
- {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/src/agentrepocoach/pr_bot.py +0 -0
- {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/src/agentrepocoach/regex_safety.py +0 -0
- {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/src/agentrepocoach/scoring.py +0 -0
- {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/src/agentrepocoach.egg-info/dependency_links.txt +0 -0
- {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/src/agentrepocoach.egg-info/entry_points.txt +0 -0
- {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/src/agentrepocoach.egg-info/requires.txt +0 -0
- {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/src/agentrepocoach.egg-info/top_level.txt +0 -0
- {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/tests/test_adapters.py +0 -0
- {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/tests/test_cli.py +0 -0
- {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/tests/test_cli_compare.py +0 -0
- {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/tests/test_multi_language.py +0 -0
- {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/tests/test_output.py +0 -0
- {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/tests/test_pr_bot.py +0 -0
- {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/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
|
+
Version: 0.4.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
|
|
@@ -43,13 +43,14 @@ Dynamic: license-file
|
|
|
43
43
|
|
|
44
44
|
AgentRepoCoach computes the **Codebase Agent Health (CAH)** score: a single 0-100
|
|
45
45
|
composite measuring how friendly a repository is for autonomous AI agents.
|
|
46
|
-
It blends
|
|
46
|
+
It blends six statically-measurable components:
|
|
47
47
|
|
|
48
|
-
- **Navigability** (
|
|
49
|
-
- **Error quality** (
|
|
50
|
-
- **Decision queryability** (
|
|
51
|
-
- **Test quality** (
|
|
52
|
-
- **Module hygiene** (
|
|
48
|
+
- **Navigability** (22%) — `AGENTS.md`, codebase map, CLI manifest, root cleanliness
|
|
49
|
+
- **Error quality** (22%) — fix-hint coverage, exception typing, generic-exception dominance
|
|
50
|
+
- **Decision queryability** (18%) — ADR catalog, inline reference resolution
|
|
51
|
+
- **Test quality** (13%) — naming convention, helper presence, fixture duplication
|
|
52
|
+
- **Module hygiene** (13%) — internal visibility, god files, doc coverage, architecture doc freshness
|
|
53
|
+
- **Bootstrap signals** (12%) — CI-Signal (runnable test workflow on PR triggers) + README-quality (install + test commands in first 100 lines)
|
|
53
54
|
|
|
54
55
|
AgentRepoCoach ships with zero runtime dependencies — it uses the Python 3.11+
|
|
55
56
|
standard library only, including `tomllib` for config parsing.
|
|
@@ -111,9 +112,21 @@ jobs:
|
|
|
111
112
|
|
|
112
113
|
## Usage as a CLI
|
|
113
114
|
|
|
115
|
+
Install and run:
|
|
116
|
+
|
|
114
117
|
```bash
|
|
115
118
|
pip install agentrepocoach
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Run tests after contributing:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
pytest tests/ -q
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Score your repository:
|
|
116
128
|
|
|
129
|
+
```bash
|
|
117
130
|
# Score the current directory (prints a summary table)
|
|
118
131
|
python -m agentrepocoach.cli --repo .
|
|
119
132
|
|
|
@@ -139,16 +152,19 @@ AgentRepoCoach looks for `.agentrepocoach.toml` at the repo root. Every field is
|
|
|
139
152
|
optional — the tool ships with sensible defaults and will score zero-config
|
|
140
153
|
repos without complaint.
|
|
141
154
|
|
|
142
|
-
Minimal example:
|
|
155
|
+
Minimal example (schema v2, required since v0.4.0):
|
|
143
156
|
|
|
144
157
|
```toml
|
|
145
158
|
# .agentrepocoach.toml
|
|
159
|
+
schema_version = 2
|
|
160
|
+
|
|
146
161
|
[weights]
|
|
147
|
-
navigability = 0.
|
|
148
|
-
error_quality = 0.
|
|
149
|
-
decision_queryability = 0.
|
|
150
|
-
test_quality = 0.
|
|
151
|
-
module_hygiene = 0.
|
|
162
|
+
navigability = 0.22
|
|
163
|
+
error_quality = 0.22
|
|
164
|
+
decision_queryability = 0.18
|
|
165
|
+
test_quality = 0.13
|
|
166
|
+
module_hygiene = 0.13
|
|
167
|
+
bootstrap_signals = 0.12
|
|
152
168
|
|
|
153
169
|
[paths]
|
|
154
170
|
adr_dir = "docs/adr/"
|
|
@@ -159,8 +175,17 @@ domain_exception_types = ["DomainError", "ValidationError"]
|
|
|
159
175
|
|
|
160
176
|
[decision_queryability]
|
|
161
177
|
inline_ref_patterns = ["ADR-\\d+"]
|
|
178
|
+
|
|
179
|
+
# Optional: tune bootstrap_signals detection globs / patterns
|
|
180
|
+
[bootstrap_signals]
|
|
181
|
+
ci_workflow_globs = [".github/workflows/*.yml", ".gitlab-ci.yml"]
|
|
162
182
|
```
|
|
163
183
|
|
|
184
|
+
**Migrating from v1?** Add `schema_version = 2` at the top and add
|
|
185
|
+
`bootstrap_signals = 0.12` to `[weights]`, then rebalance the other five
|
|
186
|
+
weights so they still sum to 1.0. See [`docs/configuration.md`](docs/configuration.md)
|
|
187
|
+
for the one-line migration recipe.
|
|
188
|
+
|
|
164
189
|
See [`docs/METHODOLOGY.md`](docs/METHODOLOGY.md) for the full config schema
|
|
165
190
|
and scoring formula.
|
|
166
191
|
|
|
@@ -192,10 +217,19 @@ appear in:
|
|
|
192
217
|
## How it works
|
|
193
218
|
|
|
194
219
|
AgentRepoCoach detects the primary language of the repo, loads a language
|
|
195
|
-
adapter, and runs
|
|
220
|
+
adapter, and runs six component scorers against the adapter's view of
|
|
196
221
|
the codebase. Each component returns a 0-100 sub-score with a transparent
|
|
197
222
|
breakdown. The weighted sum is the composite CAH score.
|
|
198
223
|
|
|
224
|
+
The six components are:
|
|
225
|
+
|
|
226
|
+
- **Navigability** — checks for `AGENTS.md`, codebase map, CLI manifest, and root cleanliness.
|
|
227
|
+
- **Error quality** — scores fix-hint coverage, exception typing, and generic-exception usage.
|
|
228
|
+
- **Decision queryability** — audits ADR catalog completeness and inline ADR cross-references.
|
|
229
|
+
- **Test quality** — checks test naming conventions, helper presence, and fixture duplication.
|
|
230
|
+
- **Module hygiene** — inspects internal visibility ratios, god files, doc coverage, and architecture doc freshness.
|
|
231
|
+
- **Bootstrap signals** — language-agnostic CI-Signal scorer (does the repo have a CI workflow triggered on pull requests?) and README-quality scorer (do the first 100 README lines contain both an install and a test command in fenced code blocks?). Both are configurable via `[bootstrap_signals]` in `.agentrepocoach.toml`.
|
|
232
|
+
|
|
199
233
|
Every output field is a count, percentage, type name, or file path —
|
|
200
234
|
AgentRepoCoach never emits code snippets or raw message bodies, so reports
|
|
201
235
|
are safe to publish as CI artifacts.
|
|
@@ -9,13 +9,14 @@
|
|
|
9
9
|
|
|
10
10
|
AgentRepoCoach computes the **Codebase Agent Health (CAH)** score: a single 0-100
|
|
11
11
|
composite measuring how friendly a repository is for autonomous AI agents.
|
|
12
|
-
It blends
|
|
12
|
+
It blends six statically-measurable components:
|
|
13
13
|
|
|
14
|
-
- **Navigability** (
|
|
15
|
-
- **Error quality** (
|
|
16
|
-
- **Decision queryability** (
|
|
17
|
-
- **Test quality** (
|
|
18
|
-
- **Module hygiene** (
|
|
14
|
+
- **Navigability** (22%) — `AGENTS.md`, codebase map, CLI manifest, root cleanliness
|
|
15
|
+
- **Error quality** (22%) — fix-hint coverage, exception typing, generic-exception dominance
|
|
16
|
+
- **Decision queryability** (18%) — ADR catalog, inline reference resolution
|
|
17
|
+
- **Test quality** (13%) — naming convention, helper presence, fixture duplication
|
|
18
|
+
- **Module hygiene** (13%) — internal visibility, god files, doc coverage, architecture doc freshness
|
|
19
|
+
- **Bootstrap signals** (12%) — CI-Signal (runnable test workflow on PR triggers) + README-quality (install + test commands in first 100 lines)
|
|
19
20
|
|
|
20
21
|
AgentRepoCoach ships with zero runtime dependencies — it uses the Python 3.11+
|
|
21
22
|
standard library only, including `tomllib` for config parsing.
|
|
@@ -77,9 +78,21 @@ jobs:
|
|
|
77
78
|
|
|
78
79
|
## Usage as a CLI
|
|
79
80
|
|
|
81
|
+
Install and run:
|
|
82
|
+
|
|
80
83
|
```bash
|
|
81
84
|
pip install agentrepocoach
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Run tests after contributing:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
pytest tests/ -q
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Score your repository:
|
|
82
94
|
|
|
95
|
+
```bash
|
|
83
96
|
# Score the current directory (prints a summary table)
|
|
84
97
|
python -m agentrepocoach.cli --repo .
|
|
85
98
|
|
|
@@ -105,16 +118,19 @@ AgentRepoCoach looks for `.agentrepocoach.toml` at the repo root. Every field is
|
|
|
105
118
|
optional — the tool ships with sensible defaults and will score zero-config
|
|
106
119
|
repos without complaint.
|
|
107
120
|
|
|
108
|
-
Minimal example:
|
|
121
|
+
Minimal example (schema v2, required since v0.4.0):
|
|
109
122
|
|
|
110
123
|
```toml
|
|
111
124
|
# .agentrepocoach.toml
|
|
125
|
+
schema_version = 2
|
|
126
|
+
|
|
112
127
|
[weights]
|
|
113
|
-
navigability = 0.
|
|
114
|
-
error_quality = 0.
|
|
115
|
-
decision_queryability = 0.
|
|
116
|
-
test_quality = 0.
|
|
117
|
-
module_hygiene = 0.
|
|
128
|
+
navigability = 0.22
|
|
129
|
+
error_quality = 0.22
|
|
130
|
+
decision_queryability = 0.18
|
|
131
|
+
test_quality = 0.13
|
|
132
|
+
module_hygiene = 0.13
|
|
133
|
+
bootstrap_signals = 0.12
|
|
118
134
|
|
|
119
135
|
[paths]
|
|
120
136
|
adr_dir = "docs/adr/"
|
|
@@ -125,8 +141,17 @@ domain_exception_types = ["DomainError", "ValidationError"]
|
|
|
125
141
|
|
|
126
142
|
[decision_queryability]
|
|
127
143
|
inline_ref_patterns = ["ADR-\\d+"]
|
|
144
|
+
|
|
145
|
+
# Optional: tune bootstrap_signals detection globs / patterns
|
|
146
|
+
[bootstrap_signals]
|
|
147
|
+
ci_workflow_globs = [".github/workflows/*.yml", ".gitlab-ci.yml"]
|
|
128
148
|
```
|
|
129
149
|
|
|
150
|
+
**Migrating from v1?** Add `schema_version = 2` at the top and add
|
|
151
|
+
`bootstrap_signals = 0.12` to `[weights]`, then rebalance the other five
|
|
152
|
+
weights so they still sum to 1.0. See [`docs/configuration.md`](docs/configuration.md)
|
|
153
|
+
for the one-line migration recipe.
|
|
154
|
+
|
|
130
155
|
See [`docs/METHODOLOGY.md`](docs/METHODOLOGY.md) for the full config schema
|
|
131
156
|
and scoring formula.
|
|
132
157
|
|
|
@@ -158,10 +183,19 @@ appear in:
|
|
|
158
183
|
## How it works
|
|
159
184
|
|
|
160
185
|
AgentRepoCoach detects the primary language of the repo, loads a language
|
|
161
|
-
adapter, and runs
|
|
186
|
+
adapter, and runs six component scorers against the adapter's view of
|
|
162
187
|
the codebase. Each component returns a 0-100 sub-score with a transparent
|
|
163
188
|
breakdown. The weighted sum is the composite CAH score.
|
|
164
189
|
|
|
190
|
+
The six components are:
|
|
191
|
+
|
|
192
|
+
- **Navigability** — checks for `AGENTS.md`, codebase map, CLI manifest, and root cleanliness.
|
|
193
|
+
- **Error quality** — scores fix-hint coverage, exception typing, and generic-exception usage.
|
|
194
|
+
- **Decision queryability** — audits ADR catalog completeness and inline ADR cross-references.
|
|
195
|
+
- **Test quality** — checks test naming conventions, helper presence, and fixture duplication.
|
|
196
|
+
- **Module hygiene** — inspects internal visibility ratios, god files, doc coverage, and architecture doc freshness.
|
|
197
|
+
- **Bootstrap signals** — language-agnostic CI-Signal scorer (does the repo have a CI workflow triggered on pull requests?) and README-quality scorer (do the first 100 README lines contain both an install and a test command in fenced code blocks?). Both are configurable via `[bootstrap_signals]` in `.agentrepocoach.toml`.
|
|
198
|
+
|
|
165
199
|
Every output field is a count, percentage, type name, or file path —
|
|
166
200
|
AgentRepoCoach never emits code snippets or raw message bodies, so reports
|
|
167
201
|
are safe to publish as CI artifacts.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "agentrepocoach"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.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" }
|
|
@@ -11,7 +11,9 @@ File-to-component mapping:
|
|
|
11
11
|
- ``decision_queryability.py`` -> ``decision_queryability``
|
|
12
12
|
- ``test_quality.py`` -> ``test_quality``
|
|
13
13
|
- ``module_hygiene.py`` -> ``module_hygiene``
|
|
14
|
+
- ``bootstrap_signals.py`` -> ``bootstrap_signals`` (CI workflow + README quality)
|
|
14
15
|
"""
|
|
16
|
+
from .bootstrap_signals import compute_bootstrap_signals
|
|
15
17
|
from .decision_queryability import compute_decision_queryability
|
|
16
18
|
from .documentation import compute_navigability
|
|
17
19
|
from .error_quality import compute_error_quality
|
|
@@ -19,6 +21,7 @@ from .module_hygiene import compute_module_hygiene
|
|
|
19
21
|
from .test_quality import compute_test_quality
|
|
20
22
|
|
|
21
23
|
__all__ = [
|
|
24
|
+
"compute_bootstrap_signals",
|
|
22
25
|
"compute_decision_queryability",
|
|
23
26
|
"compute_error_quality",
|
|
24
27
|
"compute_module_hygiene",
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""Bootstrap-signals component — scores the two artifacts an agent needs
|
|
2
|
+
to validate its own work against the repo: a runnable CI workflow on PRs
|
|
3
|
+
(50 pts), and a README that surfaces install + test commands in the
|
|
4
|
+
first 100 lines (50 pts).
|
|
5
|
+
|
|
6
|
+
Security invariants (AC-06):
|
|
7
|
+
- No shell-out calls (see test_bootstrap_signals_security.py for the grep guard).
|
|
8
|
+
- README reads are capped at _README_BYTE_CAP bytes before line scan.
|
|
9
|
+
- CI workflow scans are limited to _CI_FILES_MAX_SCAN files.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from ..adapters import LanguageAdapter
|
|
18
|
+
from ..config import BootstrapSignalsConfig, Config
|
|
19
|
+
|
|
20
|
+
_CI_SIGNAL_WEIGHT = 50
|
|
21
|
+
_README_QUALITY_WEIGHT = 50
|
|
22
|
+
|
|
23
|
+
_README_HEAD_LINES = 100 # line cap for README scoring
|
|
24
|
+
_README_BYTE_CAP = 200_000 # hard byte cap before line scan (DoS guard)
|
|
25
|
+
_CI_FILES_MAX_SCAN = 50 # short-circuit for pathological repos
|
|
26
|
+
|
|
27
|
+
# Regex patterns for detecting "on: pull_request" in YAML files.
|
|
28
|
+
# Covers three common forms:
|
|
29
|
+
# on: pull_request
|
|
30
|
+
# on: [pull_request, push]
|
|
31
|
+
# on:\n pull_request:
|
|
32
|
+
_PR_SCALAR_RE = re.compile(r"^on:\s+\[?[^#\n]*pull_request", re.MULTILINE)
|
|
33
|
+
_PR_MAP_BLOCK_RE = re.compile(r"^\s*on:\s*$", re.MULTILINE)
|
|
34
|
+
_PR_MAP_VALUE_RE = re.compile(r"^\s+pull_request\b", re.MULTILINE)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def compute_bootstrap_signals(
|
|
38
|
+
repo_root: Path, config: Config, adapter: LanguageAdapter,
|
|
39
|
+
) -> dict[str, Any]:
|
|
40
|
+
"""Score the bootstrap-signals component.
|
|
41
|
+
|
|
42
|
+
Returns a dict with ``{"score": float, "total": 100, "breakdown": {...}}``.
|
|
43
|
+
The ``adapter`` parameter is accepted for interface consistency and future
|
|
44
|
+
per-language override hooks; it is currently unused.
|
|
45
|
+
"""
|
|
46
|
+
ci = _score_ci_signal(repo_root, config)
|
|
47
|
+
readme = _score_readme_quality(repo_root, config)
|
|
48
|
+
total = ci["score"] + readme["score"]
|
|
49
|
+
return {
|
|
50
|
+
"score": round(total, 2),
|
|
51
|
+
"total": 100,
|
|
52
|
+
"breakdown": {"ci_signal": ci, "readme_quality": readme},
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _score_ci_signal(repo_root: Path, config: Config) -> dict[str, Any]:
|
|
57
|
+
"""Score CI-signal sub-component (0–50 pts).
|
|
58
|
+
|
|
59
|
+
30 pts: any CI workflow file containing a recognisable test command exists.
|
|
60
|
+
20 pts: at least one such workflow triggers on pull_request.
|
|
61
|
+
"""
|
|
62
|
+
bsc = config.bootstrap_signals
|
|
63
|
+
workflow_files: list[Path] = []
|
|
64
|
+
for glob_pattern in bsc.ci_workflow_globs:
|
|
65
|
+
matches = sorted(repo_root.glob(glob_pattern))
|
|
66
|
+
workflow_files.extend(matches)
|
|
67
|
+
if len(workflow_files) >= _CI_FILES_MAX_SCAN:
|
|
68
|
+
workflow_files = workflow_files[:_CI_FILES_MAX_SCAN]
|
|
69
|
+
break
|
|
70
|
+
|
|
71
|
+
if not workflow_files:
|
|
72
|
+
return {
|
|
73
|
+
"score": 0,
|
|
74
|
+
"total": _CI_SIGNAL_WEIGHT,
|
|
75
|
+
"workflows_found": 0,
|
|
76
|
+
"pr_trigger": False,
|
|
77
|
+
"note": "No CI workflow files found.",
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
# 30 pts for having any workflow; 20 pts for a pull_request trigger.
|
|
81
|
+
has_pr_trigger = any(_file_has_pr_trigger(f, config) for f in workflow_files)
|
|
82
|
+
score = 30 + (20 if has_pr_trigger else 0)
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
"score": score,
|
|
86
|
+
"total": _CI_SIGNAL_WEIGHT,
|
|
87
|
+
"workflows_found": len(workflow_files),
|
|
88
|
+
"pr_trigger": has_pr_trigger,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _file_has_pr_trigger(path: Path, config: Config) -> bool:
|
|
93
|
+
"""Return True if the file triggers on pull_request."""
|
|
94
|
+
try:
|
|
95
|
+
byte_size = path.stat().st_size
|
|
96
|
+
if byte_size > config.thresholds.max_file_bytes:
|
|
97
|
+
return False
|
|
98
|
+
text = path.read_text(encoding="utf-8", errors="ignore")
|
|
99
|
+
except OSError:
|
|
100
|
+
return False
|
|
101
|
+
|
|
102
|
+
# scalar form: on: pull_request OR on: [pull_request, push]
|
|
103
|
+
if _PR_SCALAR_RE.search(text):
|
|
104
|
+
return True
|
|
105
|
+
|
|
106
|
+
# block map form: on:\n pull_request:
|
|
107
|
+
for match in _PR_MAP_BLOCK_RE.finditer(text):
|
|
108
|
+
tail = text[match.end():]
|
|
109
|
+
first_line = tail.lstrip("\n").split("\n")[0] if tail else ""
|
|
110
|
+
if _PR_MAP_VALUE_RE.match("\n" + first_line):
|
|
111
|
+
return True
|
|
112
|
+
|
|
113
|
+
return False
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _score_readme_quality(repo_root: Path, config: Config) -> dict[str, Any]:
|
|
117
|
+
"""Score README-quality sub-component (0–50 pts).
|
|
118
|
+
|
|
119
|
+
25 pts: a fenced code block in the first 100 lines contains an install command.
|
|
120
|
+
25 pts: a fenced code block in the first 100 lines contains a test command.
|
|
121
|
+
"""
|
|
122
|
+
bsc = config.bootstrap_signals
|
|
123
|
+
|
|
124
|
+
# Try common README filenames in priority order.
|
|
125
|
+
readme_path: Path | None = None
|
|
126
|
+
for candidate in ("README.md", "README.rst", "README.txt", "README"):
|
|
127
|
+
p = repo_root / candidate
|
|
128
|
+
if p.is_file():
|
|
129
|
+
readme_path = p
|
|
130
|
+
break
|
|
131
|
+
|
|
132
|
+
if readme_path is None:
|
|
133
|
+
return {
|
|
134
|
+
"score": 0,
|
|
135
|
+
"total": _README_QUALITY_WEIGHT,
|
|
136
|
+
"install_found": False,
|
|
137
|
+
"test_found": False,
|
|
138
|
+
"note": "No README file found.",
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
byte_size = readme_path.stat().st_size
|
|
143
|
+
if byte_size > _README_BYTE_CAP:
|
|
144
|
+
return {
|
|
145
|
+
"score": 0,
|
|
146
|
+
"total": _README_QUALITY_WEIGHT,
|
|
147
|
+
"install_found": False,
|
|
148
|
+
"test_found": False,
|
|
149
|
+
"note": f"README exceeds {_README_BYTE_CAP} byte cap; skipped for DoS safety.",
|
|
150
|
+
}
|
|
151
|
+
text = readme_path.read_text(encoding="utf-8", errors="ignore")
|
|
152
|
+
except OSError:
|
|
153
|
+
return {
|
|
154
|
+
"score": 0,
|
|
155
|
+
"total": _README_QUALITY_WEIGHT,
|
|
156
|
+
"install_found": False,
|
|
157
|
+
"test_found": False,
|
|
158
|
+
"note": "README could not be read.",
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
head_lines = text.splitlines()[: bsc.readme_head_lines]
|
|
162
|
+
code_blocks = _extract_fenced_code_blocks(head_lines)
|
|
163
|
+
|
|
164
|
+
install_found = _any_matches(code_blocks, bsc.install_command_patterns)
|
|
165
|
+
test_found = _any_matches(code_blocks, bsc.test_command_patterns)
|
|
166
|
+
|
|
167
|
+
score = (25 if install_found else 0) + (25 if test_found else 0)
|
|
168
|
+
return {
|
|
169
|
+
"score": score,
|
|
170
|
+
"total": _README_QUALITY_WEIGHT,
|
|
171
|
+
"install_found": install_found,
|
|
172
|
+
"test_found": test_found,
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _extract_fenced_code_blocks(lines: list[str]) -> list[str]:
|
|
177
|
+
"""Return a flat list of all lines that appear inside fenced code blocks."""
|
|
178
|
+
inside = False
|
|
179
|
+
fence_marker = ""
|
|
180
|
+
collected: list[str] = []
|
|
181
|
+
|
|
182
|
+
for line in lines:
|
|
183
|
+
stripped = line.strip()
|
|
184
|
+
if not inside:
|
|
185
|
+
if stripped.startswith("```") or stripped.startswith("~~~"):
|
|
186
|
+
inside = True
|
|
187
|
+
fence_marker = stripped[:3]
|
|
188
|
+
else:
|
|
189
|
+
if stripped.startswith(fence_marker) and len(stripped) >= len(fence_marker):
|
|
190
|
+
inside = False
|
|
191
|
+
fence_marker = ""
|
|
192
|
+
else:
|
|
193
|
+
collected.append(line)
|
|
194
|
+
|
|
195
|
+
return collected
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _any_matches(lines: list[str], patterns: tuple[str, ...]) -> bool:
|
|
199
|
+
"""Return True if any line contains any of the pattern substrings."""
|
|
200
|
+
for line in lines:
|
|
201
|
+
for pattern in patterns:
|
|
202
|
+
if pattern in line:
|
|
203
|
+
return True
|
|
204
|
+
return False
|
|
@@ -6,6 +6,7 @@ from typing import Any
|
|
|
6
6
|
|
|
7
7
|
from .adapters import LanguageAdapter, detect_all, detect_primary, get_adapter_by_name
|
|
8
8
|
from .components import (
|
|
9
|
+
compute_bootstrap_signals,
|
|
9
10
|
compute_decision_queryability,
|
|
10
11
|
compute_error_quality,
|
|
11
12
|
compute_module_hygiene,
|
|
@@ -44,6 +45,7 @@ def compute_cah(repo_root: Path, config: Config | None = None, adapter: Language
|
|
|
44
45
|
"decision_queryability": compute_decision_queryability(repo_root, config, adapter),
|
|
45
46
|
"test_quality": compute_test_quality(repo_root, config, adapter),
|
|
46
47
|
"module_hygiene": compute_module_hygiene(repo_root, config, adapter),
|
|
48
|
+
"bootstrap_signals": compute_bootstrap_signals(repo_root, config, adapter),
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
total = 0.0
|
|
@@ -9,23 +9,34 @@ or unreadable, defaults are used.
|
|
|
9
9
|
"""
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
|
+
import sys
|
|
12
13
|
import tomllib
|
|
13
14
|
from dataclasses import dataclass, field
|
|
14
15
|
from pathlib import Path
|
|
15
16
|
from typing import Any
|
|
16
17
|
|
|
18
|
+
# Module-level guard: emit the soft-upgrade warning at most once per process
|
|
19
|
+
# per schema version encountered.
|
|
20
|
+
_warned_schemas: set[int] = set()
|
|
21
|
+
|
|
17
22
|
# Schema version — bump on breaking config changes.
|
|
18
|
-
|
|
23
|
+
# v1 → v2: Added 6th component ``bootstrap_signals`` (ARC-005). Existing
|
|
24
|
+
# configs that pin all five weights must add ``bootstrap_signals`` to the
|
|
25
|
+
# [weights] table and set ``schema_version = 2``.
|
|
26
|
+
CURRENT_SCHEMA_VERSION = 2
|
|
19
27
|
|
|
20
28
|
# Default component weights. Derived from methodology research: navigability
|
|
21
29
|
# and error quality dominate because agents fail fastest on missing entry
|
|
22
|
-
# points and unactionable errors.
|
|
30
|
+
# points and unactionable errors. Rebalanced in v2 to accommodate the new
|
|
31
|
+
# bootstrap_signals component (navigability 0.25→0.22, error_quality 0.25→0.22,
|
|
32
|
+
# decision_queryability 0.20→0.18, test_quality 0.15→0.13, module_hygiene 0.15→0.13).
|
|
23
33
|
DEFAULT_WEIGHTS: dict[str, float] = {
|
|
24
|
-
"navigability": 0.
|
|
25
|
-
"error_quality": 0.
|
|
26
|
-
"decision_queryability": 0.
|
|
27
|
-
"test_quality": 0.
|
|
28
|
-
"module_hygiene": 0.
|
|
34
|
+
"navigability": 0.22,
|
|
35
|
+
"error_quality": 0.22,
|
|
36
|
+
"decision_queryability": 0.18,
|
|
37
|
+
"test_quality": 0.13,
|
|
38
|
+
"module_hygiene": 0.13,
|
|
39
|
+
"bootstrap_signals": 0.12,
|
|
29
40
|
}
|
|
30
41
|
|
|
31
42
|
DEFAULT_EXCLUDES: tuple[str, ...] = (
|
|
@@ -113,6 +124,42 @@ class ModuleHygieneConfig:
|
|
|
113
124
|
internal_visibility_full_ratio: float = 0.10
|
|
114
125
|
|
|
115
126
|
|
|
127
|
+
@dataclass(frozen=True)
|
|
128
|
+
class BootstrapSignalsConfig:
|
|
129
|
+
"""Settings for the bootstrap_signals component."""
|
|
130
|
+
install_command_patterns: tuple[str, ...] = (
|
|
131
|
+
"pip install",
|
|
132
|
+
"uv pip",
|
|
133
|
+
"npm install",
|
|
134
|
+
"npm ci",
|
|
135
|
+
"yarn install",
|
|
136
|
+
"cargo install",
|
|
137
|
+
"cargo build",
|
|
138
|
+
"go install",
|
|
139
|
+
"go get",
|
|
140
|
+
"dotnet add",
|
|
141
|
+
"dotnet restore",
|
|
142
|
+
)
|
|
143
|
+
test_command_patterns: tuple[str, ...] = (
|
|
144
|
+
"pytest",
|
|
145
|
+
"npm test",
|
|
146
|
+
"npm run test",
|
|
147
|
+
"go test",
|
|
148
|
+
"cargo test",
|
|
149
|
+
"dotnet test",
|
|
150
|
+
"make test",
|
|
151
|
+
"mvn test",
|
|
152
|
+
"gradle test",
|
|
153
|
+
)
|
|
154
|
+
ci_workflow_globs: tuple[str, ...] = (
|
|
155
|
+
".github/workflows/*.yml",
|
|
156
|
+
".github/workflows/*.yaml",
|
|
157
|
+
".gitlab-ci.yml",
|
|
158
|
+
".circleci/config.yml",
|
|
159
|
+
)
|
|
160
|
+
readme_head_lines: int = 100
|
|
161
|
+
|
|
162
|
+
|
|
116
163
|
@dataclass(frozen=True)
|
|
117
164
|
class PathConfig:
|
|
118
165
|
"""File and directory paths used by scoring components."""
|
|
@@ -139,6 +186,7 @@ class Config:
|
|
|
139
186
|
error_quality: ErrorQualityConfig = field(default_factory=ErrorQualityConfig)
|
|
140
187
|
test_quality: TestQualityConfig = field(default_factory=TestQualityConfig)
|
|
141
188
|
module_hygiene: ModuleHygieneConfig = field(default_factory=ModuleHygieneConfig)
|
|
189
|
+
bootstrap_signals: BootstrapSignalsConfig = field(default_factory=BootstrapSignalsConfig)
|
|
142
190
|
|
|
143
191
|
|
|
144
192
|
class ConfigError(ValueError):
|
|
@@ -168,12 +216,29 @@ def load_config(repo_root: Path, config_path: Path | None = None) -> Config:
|
|
|
168
216
|
def _build_config_from_dict(raw: dict[str, Any]) -> Config:
|
|
169
217
|
"""Merge a parsed TOML dict into a Config with defaults applied."""
|
|
170
218
|
schema_version = int(raw.get("schema_version", CURRENT_SCHEMA_VERSION))
|
|
171
|
-
if schema_version
|
|
219
|
+
if schema_version > CURRENT_SCHEMA_VERSION:
|
|
172
220
|
msg = f"Unsupported schema_version {schema_version}. This tool supports schema_version {CURRENT_SCHEMA_VERSION}."
|
|
173
221
|
raise ConfigError(f"{msg} Try updating agentrepocoach or check the config file format at docs/configuration.md.")
|
|
174
222
|
|
|
223
|
+
if schema_version < CURRENT_SCHEMA_VERSION:
|
|
224
|
+
if schema_version not in _warned_schemas:
|
|
225
|
+
_warned_schemas.add(schema_version)
|
|
226
|
+
print(
|
|
227
|
+
f"agentrepocoach: WARNING: .agentrepocoach.toml uses schema_version {schema_version}; "
|
|
228
|
+
f"this tool ships schema_version {CURRENT_SCHEMA_VERSION}. Auto-upgrading in-memory; "
|
|
229
|
+
f"please bump your config and rebalance [weights]. See docs/configuration.md.",
|
|
230
|
+
file=sys.stderr,
|
|
231
|
+
)
|
|
232
|
+
|
|
175
233
|
weights = dict(DEFAULT_WEIGHTS)
|
|
176
234
|
weights.update(raw.get("weights", {}))
|
|
235
|
+
|
|
236
|
+
if schema_version < CURRENT_SCHEMA_VERSION:
|
|
237
|
+
current_sum = sum(weights.values())
|
|
238
|
+
if abs(current_sum - 1.0) > 0.01:
|
|
239
|
+
for k in weights:
|
|
240
|
+
weights[k] = weights[k] / current_sum
|
|
241
|
+
|
|
177
242
|
_validate_weights(weights)
|
|
178
243
|
|
|
179
244
|
return Config(
|
|
@@ -190,6 +255,7 @@ def _build_config_from_dict(raw: dict[str, Any]) -> Config:
|
|
|
190
255
|
error_quality=_build_error_quality_config(raw.get("error_quality", {})),
|
|
191
256
|
test_quality=_build_test_quality_config(raw.get("test_quality", {})),
|
|
192
257
|
module_hygiene=_build_module_hygiene_config(raw.get("module_hygiene", {})),
|
|
258
|
+
bootstrap_signals=_build_bootstrap_signals_config(raw.get("bootstrap_signals", {})),
|
|
193
259
|
)
|
|
194
260
|
|
|
195
261
|
|
|
@@ -198,11 +264,11 @@ def _validate_weights(weights: dict[str, float]) -> None:
|
|
|
198
264
|
missing = set(DEFAULT_WEIGHTS) - set(weights)
|
|
199
265
|
if missing:
|
|
200
266
|
msg = f"Missing component weights: {sorted(missing)}."
|
|
201
|
-
raise ConfigError(f"{msg} Check that [weights] in .agentrepocoach.toml includes all
|
|
267
|
+
raise ConfigError(f"{msg} Check that [weights] in .agentrepocoach.toml includes all six components. See docs/configuration.md.")
|
|
202
268
|
total = sum(weights[name] for name in DEFAULT_WEIGHTS)
|
|
203
269
|
if abs(total - 1.0) > 0.01:
|
|
204
270
|
msg = f"Component weights must sum to 1.0 (got {total:.3f})."
|
|
205
|
-
raise ConfigError(f"{msg} Check the [weights] section in .agentrepocoach.toml and ensure the
|
|
271
|
+
raise ConfigError(f"{msg} Check the [weights] section in .agentrepocoach.toml and ensure the six values add up to exactly 1.0.")
|
|
206
272
|
|
|
207
273
|
|
|
208
274
|
def _build_path_config(raw: dict[str, Any]) -> PathConfig:
|
|
@@ -261,3 +327,16 @@ def _build_module_hygiene_config(raw: dict[str, Any]) -> ModuleHygieneConfig:
|
|
|
261
327
|
architecture_doc_fresh_days=int(raw.get("architecture_doc_fresh_days", 60)),
|
|
262
328
|
internal_visibility_full_ratio=float(raw.get("internal_visibility_full_ratio", 0.10)),
|
|
263
329
|
)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _build_bootstrap_signals_config(raw: dict[str, Any]) -> BootstrapSignalsConfig:
|
|
333
|
+
install_patterns = raw.get("install_command_patterns")
|
|
334
|
+
test_patterns = raw.get("test_command_patterns")
|
|
335
|
+
ci_globs = raw.get("ci_workflow_globs")
|
|
336
|
+
defaults = BootstrapSignalsConfig()
|
|
337
|
+
return BootstrapSignalsConfig(
|
|
338
|
+
install_command_patterns=tuple(install_patterns) if install_patterns is not None else defaults.install_command_patterns,
|
|
339
|
+
test_command_patterns=tuple(test_patterns) if test_patterns is not None else defaults.test_command_patterns,
|
|
340
|
+
ci_workflow_globs=tuple(ci_globs) if ci_globs is not None else defaults.ci_workflow_globs,
|
|
341
|
+
readme_head_lines=int(raw.get("readme_head_lines", defaults.readme_head_lines)),
|
|
342
|
+
)
|