Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/attributes.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@ Type annotations give agents reliable information about what a function expects
- All public functions have parameter and return type hints
- Generic types from `typing` module used appropriately
- Coverage: >80% of functions typed
- **Strict mode bonus** (+15 pts): type checker configured in strict mode. Checked configs: `mypy.ini`/`.mypy.ini` (`strict = true` or `disallow_untyped_defs = true`), `setup.cfg` `[mypy]`, `pyproject.toml` `[tool.mypy]`, `pyrightconfig.json` (`typeCheckingMode: "strict"`), `pyproject.toml` `[tool.pyright]`
- Tools: mypy, pyright

**TypeScript**:
Expand Down Expand Up @@ -459,6 +460,8 @@ project/
└── target/
```

**Naming consistency** (evidence only, no score impact): The assessor checks for mixed file naming conventions (snake_case vs camelCase vs PascalCase vs kebab-case) within the same directory. Inconsistent naming reduces "glob-ability" for agents trying to predict file names. Directories with fewer than 3 classifiable files are skipped.

#### Remediation

**If non-standard layout**:
Expand Down
92 changes: 88 additions & 4 deletions src/agentready/assessors/code_quality.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
"""Code quality assessors for complexity, file length, type annotations, and code smells."""

import ast
import configparser
import logging
import re
import tomllib

from ..models.attribute import Attribute
from ..models.finding import Citation, Finding, Remediation
Expand Down Expand Up @@ -136,6 +138,16 @@ def _assess_python_types(self, repository: Repository) -> Finding:
higher_is_better=True,
)

evidence = [
f"Typed functions: {typed_functions}/{total_functions}",
f"Coverage: {coverage_percent:.1f}%",
]

strict_pts, strict_evidence = self._check_python_strict_mode(repository)
if strict_pts > 0:
score = min(score + strict_pts, 100.0)
evidence.extend(strict_evidence)

status = "pass" if score >= 75 else "fail"

return Finding(
Expand All @@ -144,14 +156,86 @@ def _assess_python_types(self, repository: Repository) -> Finding:
score=score,
measured_value=f"{coverage_percent:.1f}%",
threshold="≥80%",
evidence=[
f"Typed functions: {typed_functions}/{total_functions}",
f"Coverage: {coverage_percent:.1f}%",
],
evidence=evidence,
remediation=self._create_remediation() if status == "fail" else None,
error_message=None,
)

def _check_python_strict_mode(
self, repository: Repository
) -> tuple[float, list[str]]:
"""Check whether a Python type checker is configured in strict mode.

Awards 15 bonus points if strict mode is detected in any of:
mypy.ini, .mypy.ini, setup.cfg [mypy], pyproject.toml [tool.mypy],
pyrightconfig.json, or pyproject.toml [tool.pyright].
"""
import json

# Check INI-style mypy configs
for ini_name in ("mypy.ini", ".mypy.ini", "setup.cfg"):
ini_path = repository.path / ini_name
if not ini_path.exists():
continue
try:
parser = configparser.ConfigParser()
parser.read(str(ini_path), encoding="utf-8")
if parser.has_section("mypy"):
strict = parser.get("mypy", "strict", fallback="").lower()
disallow = parser.get(
"mypy", "disallow_untyped_defs", fallback=""
).lower()
if strict == "true" or disallow == "true":
return 15.0, [
f"mypy strict mode configured in {ini_name} "
"(prevents new type violations)"
]
except (OSError, configparser.Error):
continue

# Check pyproject.toml for [tool.mypy] and [tool.pyright]
pyproject_path = repository.path / "pyproject.toml"
if pyproject_path.exists():
try:
with open(pyproject_path, "rb") as f:
data = tomllib.load(f)

mypy_cfg = data.get("tool", {}).get("mypy", {})
if (
mypy_cfg.get("strict") is True
or mypy_cfg.get("disallow_untyped_defs") is True
):
return 15.0, [
"mypy strict mode configured in pyproject.toml "
"(prevents new type violations)"
]

pyright_cfg = data.get("tool", {}).get("pyright", {})
if pyright_cfg.get("typeCheckingMode") == "strict":
return 15.0, [
"pyright strict mode configured in pyproject.toml "
"(prevents new type violations)"
]
except (OSError, tomllib.TOMLDecodeError):
pass

# Check pyrightconfig.json (supports JSONC comments)
pyright_path = repository.path / "pyrightconfig.json"
if pyright_path.exists():
try:
raw = pyright_path.read_text(encoding="utf-8")
cleaned = self._strip_json_comments(raw)
config = json.loads(cleaned)
if config.get("typeCheckingMode") == "strict":
return 15.0, [
"pyright strict mode configured in pyrightconfig.json "
"(prevents new type violations)"
]
except (OSError, json.JSONDecodeError):
pass

return 0.0, []

def _assess_typescript_types(self, repository: Repository) -> Finding:
"""Assess TypeScript type configuration across all tsconfig.json files.

Expand Down
93 changes: 93 additions & 0 deletions src/agentready/assessors/structure.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,9 @@ def assess(self, repository: Repository) -> Finding:
f"tests/: {'✓' if has_tests else '✗'}",
]

naming_evidence = self._check_naming_consistency(repository)
evidence.extend(naming_evidence)

return Finding(
attribute=self.attribute,
status=status,
Expand Down Expand Up @@ -411,6 +414,9 @@ def _assess_go_layout(self, repository: Repository) -> Finding:
else:
evidence.append("*_test.go files: ✗ (no test files found)")

naming_evidence = self._check_naming_consistency(repository)
evidence.extend(naming_evidence)

score = min(score, 100.0)
status = "pass" if score >= 75 else "fail"

Expand All @@ -431,6 +437,93 @@ def _assess_go_layout(self, repository: Repository) -> Finding:
error_message=None,
)

@staticmethod
def _classify_naming_convention(name: str) -> str | None:
"""Classify a filename (without extension) into a naming convention.

Returns None for single-word lowercase names (neutral, e.g. "main").
"""
if not name:
return None
if "_" in name and name == name.lower():
return "snake_case"
if "-" in name and name == name.lower():
return "kebab-case"
if name[0].isupper() and any(c.islower() for c in name):
return "PascalCase"
if name[0].islower() and any(c.isupper() for c in name) and "_" not in name:
return "camelCase"
return None
Comment thread
coderabbitai[bot] marked this conversation as resolved.

def _check_naming_consistency(self, repository: Repository) -> list[str]:
"""Check for mixed file naming conventions within directories.

Reports as evidence only (no score impact). Mixed conventions
reduce "glob-ability" for agents trying to predict file names.
"""
from collections import defaultdict

from ..utils.subprocess_utils import safe_subprocess_run

try:
result = safe_subprocess_run(
["git", "ls-files"],
cwd=repository.path,
capture_output=True,
text=True,
timeout=30,
)
if result.returncode != 0:
return []
files = [f for f in result.stdout.strip().split("\n") if f]
except Exception:
return []

skip_names = {"__init__", "__main__", "conftest", "setup"}
dir_conventions: dict[str, dict[str, int]] = defaultdict(
lambda: defaultdict(int)
)

for filepath in files:
p = Path(filepath)
parts = p.parts
if any(
part.startswith(".") or part in ("node_modules", "__pycache__")
for part in parts
):
continue

stem = p.stem
if stem.startswith("_") or stem in skip_names:
continue

convention = self._classify_naming_convention(stem)
if convention is None:
continue

parent = str(p.parent) if p.parent != Path(".") else "."
dir_conventions[parent][convention] += 1

mixed_dirs = []
for dirname, conventions in sorted(dir_conventions.items()):
if sum(conventions.values()) < 3:
continue
if len(conventions) >= 2:
mixed_dirs.append(dirname)

if mixed_dirs:
dirs_display = ", ".join(mixed_dirs[:3])
suffix = f" (+{len(mixed_dirs) - 3} more)" if len(mixed_dirs) > 3 else ""
return [
f"Naming consistency: mixed conventions in {dirs_display}{suffix} "
"(reduces glob-ability for agents)"
]

if dir_conventions:
return ["Naming consistency: ✓ (consistent conventions)"]

return []

def _create_go_remediation(
self,
has_go_mod: bool,
Expand Down
Loading
Loading