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.
Files changed (45) hide show
  1. {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/PKG-INFO +48 -14
  2. {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/README.md +47 -13
  3. {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/pyproject.toml +1 -1
  4. {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/src/agentrepocoach/__init__.py +1 -1
  5. {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/src/agentrepocoach/components/__init__.py +3 -0
  6. agentrepocoach-0.4.0/src/agentrepocoach/components/bootstrap_signals.py +204 -0
  7. {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/src/agentrepocoach/compute.py +2 -0
  8. {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/src/agentrepocoach/config.py +89 -10
  9. {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/src/agentrepocoach.egg-info/PKG-INFO +48 -14
  10. {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/src/agentrepocoach.egg-info/SOURCES.txt +2 -0
  11. agentrepocoach-0.4.0/tests/test_bootstrap_signals_security.py +33 -0
  12. {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/tests/test_components.py +89 -1
  13. agentrepocoach-0.4.0/tests/test_config.py +197 -0
  14. agentrepocoach-0.3.1/tests/test_config.py +0 -101
  15. {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/LICENSE +0 -0
  16. {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/setup.cfg +0 -0
  17. {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/src/agentrepocoach/__main__.py +0 -0
  18. {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/src/agentrepocoach/adapters/__init__.py +0 -0
  19. {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/src/agentrepocoach/adapters/base.py +0 -0
  20. {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/src/agentrepocoach/adapters/csharp.py +0 -0
  21. {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/src/agentrepocoach/adapters/go.py +0 -0
  22. {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/src/agentrepocoach/adapters/python.py +0 -0
  23. {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/src/agentrepocoach/adapters/rust.py +0 -0
  24. {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/src/agentrepocoach/adapters/typescript.py +0 -0
  25. {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/src/agentrepocoach/cli.py +0 -0
  26. {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/src/agentrepocoach/components/decision_queryability.py +0 -0
  27. {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/src/agentrepocoach/components/documentation.py +0 -0
  28. {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/src/agentrepocoach/components/error_quality.py +0 -0
  29. {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/src/agentrepocoach/components/module_hygiene.py +0 -0
  30. {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/src/agentrepocoach/components/test_quality.py +0 -0
  31. {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/src/agentrepocoach/output.py +0 -0
  32. {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/src/agentrepocoach/pr_bot.py +0 -0
  33. {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/src/agentrepocoach/regex_safety.py +0 -0
  34. {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/src/agentrepocoach/scoring.py +0 -0
  35. {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/src/agentrepocoach.egg-info/dependency_links.txt +0 -0
  36. {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/src/agentrepocoach.egg-info/entry_points.txt +0 -0
  37. {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/src/agentrepocoach.egg-info/requires.txt +0 -0
  38. {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/src/agentrepocoach.egg-info/top_level.txt +0 -0
  39. {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/tests/test_adapters.py +0 -0
  40. {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/tests/test_cli.py +0 -0
  41. {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/tests/test_cli_compare.py +0 -0
  42. {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/tests/test_multi_language.py +0 -0
  43. {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/tests/test_output.py +0 -0
  44. {agentrepocoach-0.3.1 → agentrepocoach-0.4.0}/tests/test_pr_bot.py +0 -0
  45. {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.1
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 five statically-measurable components:
46
+ It blends six statically-measurable components:
47
47
 
48
- - **Navigability** (25%) — `AGENTS.md`, codebase map, CLI manifest, root cleanliness
49
- - **Error quality** (25%) — fix-hint coverage, exception typing, generic-exception dominance
50
- - **Decision queryability** (20%) — ADR catalog, inline reference resolution
51
- - **Test quality** (15%) — naming convention, helper presence, fixture duplication
52
- - **Module hygiene** (15%) — internal visibility, god files, doc coverage, architecture doc freshness
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.25
148
- error_quality = 0.25
149
- decision_queryability = 0.20
150
- test_quality = 0.15
151
- module_hygiene = 0.15
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 five component scorers against the adapter's view of
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 five statically-measurable components:
12
+ It blends six statically-measurable components:
13
13
 
14
- - **Navigability** (25%) — `AGENTS.md`, codebase map, CLI manifest, root cleanliness
15
- - **Error quality** (25%) — fix-hint coverage, exception typing, generic-exception dominance
16
- - **Decision queryability** (20%) — ADR catalog, inline reference resolution
17
- - **Test quality** (15%) — naming convention, helper presence, fixture duplication
18
- - **Module hygiene** (15%) — internal visibility, god files, doc coverage, architecture doc freshness
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.25
114
- error_quality = 0.25
115
- decision_queryability = 0.20
116
- test_quality = 0.15
117
- module_hygiene = 0.15
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 five component scorers against the adapter's view of
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.3.1"
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" }
@@ -9,6 +9,6 @@ from __future__ import annotations
9
9
 
10
10
  from .compute import compute_cah
11
11
 
12
- VERSION = "0.3.1"
12
+ VERSION = "0.4.0"
13
13
 
14
14
  __all__ = ["compute_cah", "VERSION"]
@@ -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
- CURRENT_SCHEMA_VERSION = 1
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,
25
- "error_quality": 0.25,
26
- "decision_queryability": 0.20,
27
- "test_quality": 0.15,
28
- "module_hygiene": 0.15,
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 != CURRENT_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 five components. See docs/configuration.md.")
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 five values add up to exactly 1.0.")
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
+ )