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
103 changes: 72 additions & 31 deletions src/agentready/assessors/documentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -400,9 +400,37 @@ def assess(self, repository: Repository) -> Finding:
Pass criteria: README.md exists with essential sections
Scoring: Proportional based on section count
"""
readme_path = repository.path / "README.md"
# Case-insensitive README lookup — handles readme.md, README.md, Readme.rst, etc.
# sorted() ensures deterministic selection when multiple variants exist.
readme_names = {"readme.md", "readme.rst", "readme.txt", "readme"}
try:
readme_path = next(
(
f
for f in sorted(
repository.path.iterdir(), key=lambda f: f.name.lower()
)
if f.is_file() and f.name.lower() in readme_names
),
None,
)
except OSError as e:
return Finding.error(
self.attribute, reason=f"Could not scan repository root: {e}"
)

if readme_path is None:
return Finding(
attribute=self.attribute,
status="fail",
score=0.0,
measured_value="missing",
threshold="present with sections",
evidence=["README not found"],
remediation=self._create_remediation(),
error_message=None,
)

# Fix TOCTOU: Use try-except around file read instead of existence check
try:
with open(readme_path, "r", encoding="utf-8") as f:
content = f.read().lower()
Expand Down Expand Up @@ -450,20 +478,9 @@ def assess(self, repository: Repository) -> Finding:
error_message=None,
)

except FileNotFoundError:
return Finding(
attribute=self.attribute,
status="fail",
score=0.0,
measured_value="missing",
threshold="present with sections",
evidence=["README.md not found"],
remediation=self._create_remediation(),
error_message=None,
)
except OSError as e:
return Finding.error(
self.attribute, reason=f"Could not read README.md: {str(e)}"
self.attribute, reason=f"Could not read {readme_path.name}: {str(e)}"
)

def _create_remediation(self) -> Remediation:
Expand Down Expand Up @@ -549,23 +566,47 @@ def assess(self, repository: Repository) -> Finding:
- ADR count (40%, up to 5 ADRs)
- Template compliance (20%)
"""
# Check for ADR directory in common locations
adr_paths = [
repository.path / "docs" / "adr",
repository.path / ".adr",
repository.path / "adr",
repository.path / "docs" / "decisions",
repository.path / "specs",
repository.path / "docs" / "specs",
repository.path / "docs" / "architecture",
repository.path / "docs" / "design",
]

# Case-insensitive ADR directory scan — handles docs/adr, docs/ADRs, docs/Adr, adr/, etc.
adr_target_names = {
"adr",
"adrs",
"decisions",
"architecture-decisions",
"architecture",
"design",
"specs",
}
adr_dir = None
for path in adr_paths:
if path.exists() and path.is_dir():
adr_dir = path
break

# Search docs/ first (most common location)
docs_dir = repository.path / "docs"
if docs_dir.is_dir():
try:
for candidate in sorted(docs_dir.iterdir()):
if (
candidate.is_dir()
and candidate.name.lower() in adr_target_names
):
adr_dir = candidate
break
except OSError:
pass # docs/ unreadable — fall through to root scan

# Fall back to repo root and hidden .adr
if not adr_dir:
if (repository.path / ".adr").is_dir():
adr_dir = repository.path / ".adr"
else:
try:
for candidate in sorted(repository.path.iterdir()):
if (
candidate.is_dir()
and candidate.name.lower() in adr_target_names
):
adr_dir = candidate
break
except OSError:
pass # root unreadable — adr_dir stays None, fail finding follows

if not adr_dir:
return Finding(
Expand All @@ -575,7 +616,7 @@ def assess(self, repository: Repository) -> Finding:
measured_value="no ADR directory",
threshold="ADR directory with decisions",
evidence=[
"No ADR directory found (checked docs/adr/, .adr/, adr/, docs/decisions/, specs/, docs/specs/, docs/architecture/, docs/design/)"
"No ADR directory found (checked docs/adr/, docs/architecture/, docs/specs/, specs/, adr/, and variants — all case-insensitive)"
],
remediation=self._create_remediation(),
error_message=None,
Expand Down
2 changes: 2 additions & 0 deletions src/agentready/assessors/structure.py
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,8 @@ def _check_setup_files(self, repository: Repository) -> list:
# Check for common setup files
files_to_check = {
"Makefile": "Makefile",
"GNUmakefile": "GNUmakefile",
"makefile": "Makefile",
"setup.sh": "shell script",
"bootstrap.sh": "bootstrap script",
"package.json": "npm/yarn",
Expand Down
Loading