Skip to content
Open
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
91 changes: 91 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,91 @@ 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 "_" 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 on lines +440 to +454
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Guard against empty string to prevent IndexError.

Lines 450 and 452 index name[0] without checking if name is non-empty. While Path.stem rarely returns an empty string, it can happen with edge-case paths (e.g., Path(".") yields empty stem), and if git ls-files returns unexpected output, this will crash.

🛡️ Add guard clause
 `@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"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/agentready/assessors/structure.py` around lines 440 - 454, The
_classify_naming_convention static method can IndexError on empty strings
because it accesses name[0]; add a guard at the top of
_classify_naming_convention to return None if not name (empty string) before any
indexing, then proceed with existing checks (snake_case, kebab-case, PascalCase,
camelCase) unchanged; update any callers/tests if they expect different behavior
for empty stems.


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