ref-agents 1.0.0__py3-none-any.whl
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.
- ref_agents/__init__.py +9 -0
- ref_agents/api_keys.json.example +8 -0
- ref_agents/auth.py +129 -0
- ref_agents/codemap/..md +62 -0
- ref_agents/codemap/CODE_MAP.md +37 -0
- ref_agents/codemap/core.md +43 -0
- ref_agents/codemap/models.md +43 -0
- ref_agents/codemap/prompts.md +40 -0
- ref_agents/codemap/security.md +45 -0
- ref_agents/codemap/tools.md +94 -0
- ref_agents/codemap/tools_browser.md +44 -0
- ref_agents/codemap/utils.md +42 -0
- ref_agents/codemap/workflow.md +42 -0
- ref_agents/config/ai_patterns.yaml +101 -0
- ref_agents/config/frameworks/angular.yaml +104 -0
- ref_agents/config/frameworks/aspnet.yaml +84 -0
- ref_agents/config/frameworks/ef_core.yaml +81 -0
- ref_agents/config/frameworks/react.yaml +111 -0
- ref_agents/config/frameworks/spring_boot.yaml +117 -0
- ref_agents/config/languages/csharp.yaml +153 -0
- ref_agents/config/languages/java.yaml +188 -0
- ref_agents/config/languages/javascript.yaml +172 -0
- ref_agents/config/languages/python.yaml +153 -0
- ref_agents/config/languages/typescript.yaml +193 -0
- ref_agents/constants.py +553 -0
- ref_agents/core/__init__.py +15 -0
- ref_agents/core/config_loader.py +160 -0
- ref_agents/core/config_models.py +167 -0
- ref_agents/core/config_parsing.py +84 -0
- ref_agents/core/language_detector.py +388 -0
- ref_agents/core/validation_models.py +66 -0
- ref_agents/core/validation_primitives.py +176 -0
- ref_agents/errors.py +34 -0
- ref_agents/license_client.py +247 -0
- ref_agents/models/__init__.py +22 -0
- ref_agents/models/gherkin.py +45 -0
- ref_agents/models/hierarchy.py +80 -0
- ref_agents/models/invest.py +59 -0
- ref_agents/models/version.py +49 -0
- ref_agents/prompts/__init__.py +9 -0
- ref_agents/prompts/start_agent.py +772 -0
- ref_agents/rules/architecture/backend_patterns.md +43 -0
- ref_agents/rules/architecture/diagramming.md +100 -0
- ref_agents/rules/architecture/frontend_patterns.md +40 -0
- ref_agents/rules/architecture/impact_analysis.md +129 -0
- ref_agents/rules/architecture/migration_strategy.md +208 -0
- ref_agents/rules/architecture/regression_protocol.md +77 -0
- ref_agents/rules/architecture/system_design.md +97 -0
- ref_agents/rules/common/codemap_standard.md +97 -0
- ref_agents/rules/common/core_protocol.md +59 -0
- ref_agents/rules/common/prompt_engineering.md +294 -0
- ref_agents/rules/development/debugging.md +32 -0
- ref_agents/rules/development/implementation.md +205 -0
- ref_agents/rules/operations/completion.md +119 -0
- ref_agents/rules/operations/cutover_protocol.md +218 -0
- ref_agents/rules/operations/discovery.md +179 -0
- ref_agents/rules/operations/fix_workflow.md +87 -0
- ref_agents/rules/operations/forensics.md +278 -0
- ref_agents/rules/operations/platform.md +263 -0
- ref_agents/rules/operations/synchronous_flow.md +25 -0
- ref_agents/rules/product/ac_validation.md +25 -0
- ref_agents/rules/product/brainstorming.md +27 -0
- ref_agents/rules/product/ref_flow.md +101 -0
- ref_agents/rules/product/requirements_std.md +114 -0
- ref_agents/rules/product/spec_writing.md +235 -0
- ref_agents/rules/product/strategy.md +96 -0
- ref_agents/rules/quality/documentation_standards.md +46 -0
- ref_agents/rules/quality/parity_testing.md +234 -0
- ref_agents/rules/quality/project_documentation.md +56 -0
- ref_agents/rules/quality/qa_lead.md +111 -0
- ref_agents/rules/quality/test_design.md +146 -0
- ref_agents/rules/quality/testing_standards.md +293 -0
- ref_agents/rules/review/pr_review.md +116 -0
- ref_agents/rules/security/security_audit.md +83 -0
- ref_agents/security/__init__.py +22 -0
- ref_agents/security/dependency_audit.py +188 -0
- ref_agents/security/file_audit.py +208 -0
- ref_agents/security/network_scan.py +179 -0
- ref_agents/security/report_generator.py +313 -0
- ref_agents/security/secret_scan.py +252 -0
- ref_agents/security/url_scan.py +240 -0
- ref_agents/security_scan.py +236 -0
- ref_agents/server.py +1586 -0
- ref_agents/session.py +100 -0
- ref_agents/tool_names.py +55 -0
- ref_agents/tools/__init__.py +8 -0
- ref_agents/tools/agents_generator.py +315 -0
- ref_agents/tools/ai_pattern_detector.py +815 -0
- ref_agents/tools/brownfield_populator.py +529 -0
- ref_agents/tools/browser/__init__.py +50 -0
- ref_agents/tools/browser/evidence_verifier.py +302 -0
- ref_agents/tools/browser/execution_logger.py +249 -0
- ref_agents/tools/browser/playwright_mcp_client.py +259 -0
- ref_agents/tools/browser/screenshot_utils.py +184 -0
- ref_agents/tools/browser/test_executor.py +537 -0
- ref_agents/tools/code_quality_scanner.py +629 -0
- ref_agents/tools/codemap/..md +93 -0
- ref_agents/tools/codemap/CODE_MAP.md +30 -0
- ref_agents/tools/codemap/browser.md +44 -0
- ref_agents/tools/codemap.py +403 -0
- ref_agents/tools/codemap_freshness.py +234 -0
- ref_agents/tools/comment_smell_scanner.py +346 -0
- ref_agents/tools/complexity.py +436 -0
- ref_agents/tools/complexity_ast.py +333 -0
- ref_agents/tools/compliance.py +246 -0
- ref_agents/tools/compliance_remediation.py +846 -0
- ref_agents/tools/context_graph.py +839 -0
- ref_agents/tools/context_manager.py +550 -0
- ref_agents/tools/context_tools.py +121 -0
- ref_agents/tools/cross_repo_linker.py +393 -0
- ref_agents/tools/dead_code_scanner.py +637 -0
- ref_agents/tools/debt_scanner.py +1092 -0
- ref_agents/tools/dependency_graph.py +272 -0
- ref_agents/tools/discovery_audit.py +372 -0
- ref_agents/tools/docs_scanner.py +600 -0
- ref_agents/tools/evaluate_gate.py +119 -0
- ref_agents/tools/external_detector.py +524 -0
- ref_agents/tools/features_generator.py +282 -0
- ref_agents/tools/flow_gap_detector.py +373 -0
- ref_agents/tools/flow_mapper.py +327 -0
- ref_agents/tools/full_suite_runner.py +740 -0
- ref_agents/tools/gherkin_parser.py +227 -0
- ref_agents/tools/guard_tools.py +139 -0
- ref_agents/tools/handoff_tools.py +282 -0
- ref_agents/tools/health_scanner.py +1211 -0
- ref_agents/tools/hierarchy_manager.py +289 -0
- ref_agents/tools/invest_scorer.py +249 -0
- ref_agents/tools/jira_confluence_export.py +306 -0
- ref_agents/tools/json_output.py +76 -0
- ref_agents/tools/migration_mapper.py +946 -0
- ref_agents/tools/migration_readiness_scanner.py +209 -0
- ref_agents/tools/pattern_learner.py +522 -0
- ref_agents/tools/report_utils.py +155 -0
- ref_agents/tools/requirements_serializer.py +225 -0
- ref_agents/tools/security_audit_tool.py +106 -0
- ref_agents/tools/sequencing_engine.py +288 -0
- ref_agents/tools/summary_generator.py +275 -0
- ref_agents/tools/symbol_resolver.py +306 -0
- ref_agents/tools/symbol_smoke_runner.py +336 -0
- ref_agents/tools/test_plan_validator.py +189 -0
- ref_agents/tools/test_smell_walker.py +902 -0
- ref_agents/tools/tier1_fixer.py +502 -0
- ref_agents/tools/validators/__init__.py +419 -0
- ref_agents/tools/validators/architect.py +268 -0
- ref_agents/tools/validators/cutover_engineer.py +167 -0
- ref_agents/tools/validators/developer.py +180 -0
- ref_agents/tools/validators/discovery.py +150 -0
- ref_agents/tools/validators/forensic_engineer.py +191 -0
- ref_agents/tools/validators/impact_architect.py +181 -0
- ref_agents/tools/validators/migration_planner.py +166 -0
- ref_agents/tools/validators/parity_tester.py +155 -0
- ref_agents/tools/validators/platform_engineer.py +134 -0
- ref_agents/tools/validators/pr_reviewer.py +129 -0
- ref_agents/tools/validators/product_manager.py +291 -0
- ref_agents/tools/validators/qa_lead.py +172 -0
- ref_agents/tools/validators/scrum_master.py +212 -0
- ref_agents/tools/validators/security_owner.py +162 -0
- ref_agents/tools/validators/specifier.py +134 -0
- ref_agents/tools/validators/strategist.py +149 -0
- ref_agents/tools/validators/tester.py +121 -0
- ref_agents/tools/version_manager.py +202 -0
- ref_agents/tools/workflow_tools.py +1549 -0
- ref_agents/utils/__init__.py +21 -0
- ref_agents/utils/git_utils.py +351 -0
- ref_agents/utils/handoff_logger.py +368 -0
- ref_agents/utils/ignore_matcher.py +270 -0
- ref_agents/workflow/__init__.py +19 -0
- ref_agents/workflow/capabilities.py +328 -0
- ref_agents/workflow/state_machine.py +708 -0
- ref_agents/workflow/transitions.py +658 -0
- ref_agents-1.0.0.dist-info/METADATA +365 -0
- ref_agents-1.0.0.dist-info/RECORD +175 -0
- ref_agents-1.0.0.dist-info/WHEEL +4 -0
- ref_agents-1.0.0.dist-info/entry_points.txt +2 -0
- ref_agents-1.0.0.dist-info/licenses/LICENSE +115 -0
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
"""Documentation Quality Scanner for REF Agents.
|
|
2
|
+
|
|
3
|
+
Scans code for documentation anti-patterns based on the Curator Protocol.
|
|
4
|
+
Detects code-echoing comments, missing docstrings, incomplete contracts.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
from ref_agents.tools.docs_scanner import scan_docs
|
|
8
|
+
report = scan_docs(Path("/path/to/project"))
|
|
9
|
+
|
|
10
|
+
Dependencies:
|
|
11
|
+
- structlog for logging
|
|
12
|
+
- pathlib for file operations
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import ast
|
|
16
|
+
import re
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
import structlog
|
|
21
|
+
|
|
22
|
+
from ref_agents.constants import Severity, Status
|
|
23
|
+
from ref_agents.tools.report_utils import write_report
|
|
24
|
+
|
|
25
|
+
logger = structlog.get_logger(__name__)
|
|
26
|
+
|
|
27
|
+
# Code-echoing patterns (comments that restate the code)
|
|
28
|
+
CODE_ECHO_PATTERNS: list[tuple[str, str]] = [
|
|
29
|
+
(r"#\s*increment\s+\w+", "Redundant 'increment' comment"),
|
|
30
|
+
(r"#\s*decrement\s+\w+", "Redundant 'decrement' comment"),
|
|
31
|
+
(r"#\s*return\s+(true|false|none|null)", "Redundant return comment"),
|
|
32
|
+
(r"#\s*set\s+\w+\s*(to|=)", "Redundant assignment comment"),
|
|
33
|
+
(r"#\s*get\s+\w+", "Redundant getter comment"),
|
|
34
|
+
(r"#\s*call\s+\w+", "Redundant call comment"),
|
|
35
|
+
(r"#\s*create\s+(new\s+)?\w+", "Redundant instantiation comment"),
|
|
36
|
+
(r"#\s*initialize", "Redundant init comment"),
|
|
37
|
+
(r"#\s*constructor", "Redundant constructor comment"),
|
|
38
|
+
(r"#\s*loop\s+(through|over)", "Redundant loop comment"),
|
|
39
|
+
(r"#\s*iterate", "Redundant iteration comment"),
|
|
40
|
+
(r"#\s*check\s+if", "Redundant conditional comment"),
|
|
41
|
+
(r"#\s*import\s+\w+", "Redundant import comment"),
|
|
42
|
+
(r"//\s*increment\s+\w+", "Redundant 'increment' comment"),
|
|
43
|
+
(r"//\s*return\s+(true|false|null)", "Redundant return comment"),
|
|
44
|
+
(r"//\s*get\s+\w+", "Redundant getter comment"),
|
|
45
|
+
(r"/\*\*?\s*constructor\s*\*/", "Redundant constructor comment"),
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
# Placeholder docstring patterns
|
|
49
|
+
PLACEHOLDER_PATTERNS: list[tuple[str, str]] = [
|
|
50
|
+
(r'"""TODO"""', "Placeholder TODO docstring"),
|
|
51
|
+
(r"'''TODO'''", "Placeholder TODO docstring"),
|
|
52
|
+
(r'""""""', "Empty docstring"),
|
|
53
|
+
(r"''''''", "Empty docstring"),
|
|
54
|
+
(r'"""[.]{3}"""', "Ellipsis placeholder docstring"),
|
|
55
|
+
(r"/\*\*\s*TODO\s*\*/", "Placeholder TODO Javadoc"),
|
|
56
|
+
(r"/\*\*\s*\*/", "Empty Javadoc"),
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
# File extensions by language
|
|
60
|
+
LANGUAGE_EXTENSIONS: dict[str, list[str]] = {
|
|
61
|
+
"python": [".py"],
|
|
62
|
+
"typescript": [".ts", ".tsx"],
|
|
63
|
+
"javascript": [".js", ".jsx"],
|
|
64
|
+
"java": [".java"],
|
|
65
|
+
"yaml": [".yaml", ".yml"],
|
|
66
|
+
"markdown": [".md"],
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class DocIssue:
|
|
72
|
+
"""Single documentation issue.
|
|
73
|
+
|
|
74
|
+
Attributes:
|
|
75
|
+
file_path: Path to file with issue.
|
|
76
|
+
line: Line number (1-indexed).
|
|
77
|
+
issue_type: Category of issue.
|
|
78
|
+
severity: Issue severity.
|
|
79
|
+
message: Human-readable description.
|
|
80
|
+
suggestion: Recommended fix.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
file_path: str
|
|
84
|
+
line: int
|
|
85
|
+
issue_type: str
|
|
86
|
+
severity: str
|
|
87
|
+
message: str
|
|
88
|
+
suggestion: str
|
|
89
|
+
|
|
90
|
+
def to_dict(self) -> dict[str, str | int]:
|
|
91
|
+
"""Convert to dictionary."""
|
|
92
|
+
return {
|
|
93
|
+
"file": self.file_path,
|
|
94
|
+
"line": self.line,
|
|
95
|
+
"type": self.issue_type,
|
|
96
|
+
"severity": self.severity,
|
|
97
|
+
"message": self.message,
|
|
98
|
+
"suggestion": self.suggestion,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass
|
|
103
|
+
class DocScanResult:
|
|
104
|
+
"""Result of documentation scan.
|
|
105
|
+
|
|
106
|
+
Attributes:
|
|
107
|
+
files_scanned: Number of files analyzed.
|
|
108
|
+
issues: List of documentation issues found.
|
|
109
|
+
languages: Languages detected.
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
files_scanned: int = 0
|
|
113
|
+
issues: list[DocIssue] = field(default_factory=list)
|
|
114
|
+
languages: set[str] = field(default_factory=set)
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def by_severity(self) -> dict[str, int]:
|
|
118
|
+
"""Count issues by severity."""
|
|
119
|
+
counts = {
|
|
120
|
+
Severity.HIGH: 0,
|
|
121
|
+
Severity.MEDIUM: 0,
|
|
122
|
+
Severity.LOW: 0,
|
|
123
|
+
}
|
|
124
|
+
for issue in self.issues:
|
|
125
|
+
if issue.severity in counts:
|
|
126
|
+
counts[issue.severity] += 1
|
|
127
|
+
return counts
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def by_type(self) -> dict[str, int]:
|
|
131
|
+
"""Count issues by type."""
|
|
132
|
+
counts: dict[str, int] = {}
|
|
133
|
+
for issue in self.issues:
|
|
134
|
+
counts[issue.issue_type] = counts.get(issue.issue_type, 0) + 1
|
|
135
|
+
return counts
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class DocScanner:
|
|
139
|
+
"""Documentation quality scanner.
|
|
140
|
+
|
|
141
|
+
Scans Python, TypeScript, Java, YAML, and Markdown files for
|
|
142
|
+
documentation anti-patterns.
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
def __init__(self) -> None:
|
|
146
|
+
"""Initialize scanner with compiled patterns."""
|
|
147
|
+
self._code_echo_patterns = [
|
|
148
|
+
(re.compile(p, re.IGNORECASE), msg) for p, msg in CODE_ECHO_PATTERNS
|
|
149
|
+
]
|
|
150
|
+
self._placeholder_patterns = [
|
|
151
|
+
(re.compile(p, re.IGNORECASE), msg) for p, msg in PLACEHOLDER_PATTERNS
|
|
152
|
+
]
|
|
153
|
+
|
|
154
|
+
def scan_directory(self, directory: Path) -> DocScanResult:
|
|
155
|
+
"""Scan directory for documentation issues.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
directory: Root directory to scan.
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
DocScanResult with all issues found.
|
|
162
|
+
"""
|
|
163
|
+
result = DocScanResult()
|
|
164
|
+
|
|
165
|
+
for ext_list in LANGUAGE_EXTENSIONS.values():
|
|
166
|
+
for ext in ext_list:
|
|
167
|
+
for file_path in directory.rglob(f"*{ext}"):
|
|
168
|
+
if self._should_skip(file_path):
|
|
169
|
+
continue
|
|
170
|
+
self._scan_file(file_path, result)
|
|
171
|
+
|
|
172
|
+
return result
|
|
173
|
+
|
|
174
|
+
def _should_skip(self, file_path: Path) -> bool:
|
|
175
|
+
"""Check if file should be skipped."""
|
|
176
|
+
skip_dirs = {
|
|
177
|
+
"__pycache__",
|
|
178
|
+
".git",
|
|
179
|
+
"node_modules",
|
|
180
|
+
".venv",
|
|
181
|
+
"venv",
|
|
182
|
+
"dist",
|
|
183
|
+
"build",
|
|
184
|
+
".pytest_cache",
|
|
185
|
+
".mypy_cache",
|
|
186
|
+
".ruff_cache",
|
|
187
|
+
}
|
|
188
|
+
return any(part in skip_dirs for part in file_path.parts)
|
|
189
|
+
|
|
190
|
+
def _scan_file(self, file_path: Path, result: DocScanResult) -> None:
|
|
191
|
+
"""Scan single file for issues."""
|
|
192
|
+
try:
|
|
193
|
+
content = file_path.read_text(encoding="utf-8")
|
|
194
|
+
except (OSError, UnicodeDecodeError) as e:
|
|
195
|
+
logger.warning("file_read_error", path=str(file_path), error=str(e))
|
|
196
|
+
return
|
|
197
|
+
|
|
198
|
+
result.files_scanned += 1
|
|
199
|
+
suffix = file_path.suffix.lower()
|
|
200
|
+
|
|
201
|
+
# Detect language
|
|
202
|
+
for lang, exts in LANGUAGE_EXTENSIONS.items():
|
|
203
|
+
if suffix in exts:
|
|
204
|
+
result.languages.add(lang)
|
|
205
|
+
break
|
|
206
|
+
|
|
207
|
+
# Universal checks (all languages)
|
|
208
|
+
self._check_code_echo(file_path, content, result)
|
|
209
|
+
self._check_placeholders(file_path, content, result)
|
|
210
|
+
|
|
211
|
+
# Language-specific checks
|
|
212
|
+
if suffix == ".py":
|
|
213
|
+
self._scan_python(file_path, content, result)
|
|
214
|
+
elif suffix in (".ts", ".tsx", ".js", ".jsx"):
|
|
215
|
+
self._scan_typescript(file_path, content, result)
|
|
216
|
+
elif suffix == ".java":
|
|
217
|
+
self._scan_java(file_path, content, result)
|
|
218
|
+
elif suffix in (".yaml", ".yml"):
|
|
219
|
+
self._scan_yaml(file_path, content, result)
|
|
220
|
+
elif suffix == ".md":
|
|
221
|
+
self._scan_markdown(file_path, content, result)
|
|
222
|
+
|
|
223
|
+
def _check_code_echo(
|
|
224
|
+
self, file_path: Path, content: str, result: DocScanResult
|
|
225
|
+
) -> None:
|
|
226
|
+
"""Check for code-echoing comments."""
|
|
227
|
+
lines = content.split("\n")
|
|
228
|
+
for i, line in enumerate(lines, 1):
|
|
229
|
+
for pattern, message in self._code_echo_patterns:
|
|
230
|
+
if pattern.search(line):
|
|
231
|
+
result.issues.append(
|
|
232
|
+
DocIssue(
|
|
233
|
+
file_path=str(file_path),
|
|
234
|
+
line=i,
|
|
235
|
+
issue_type="code-echo",
|
|
236
|
+
severity=Severity.MEDIUM,
|
|
237
|
+
message=message,
|
|
238
|
+
suggestion="Remove or explain WHY, not WHAT.",
|
|
239
|
+
)
|
|
240
|
+
)
|
|
241
|
+
break # One issue per line
|
|
242
|
+
|
|
243
|
+
def _check_placeholders(
|
|
244
|
+
self, file_path: Path, content: str, result: DocScanResult
|
|
245
|
+
) -> None:
|
|
246
|
+
"""Check for placeholder docstrings."""
|
|
247
|
+
lines = content.split("\n")
|
|
248
|
+
for i, line in enumerate(lines, 1):
|
|
249
|
+
for pattern, message in self._placeholder_patterns:
|
|
250
|
+
if pattern.search(line):
|
|
251
|
+
result.issues.append(
|
|
252
|
+
DocIssue(
|
|
253
|
+
file_path=str(file_path),
|
|
254
|
+
line=i,
|
|
255
|
+
issue_type="placeholder",
|
|
256
|
+
severity=Severity.HIGH,
|
|
257
|
+
message=message,
|
|
258
|
+
suggestion="Replace with proper docstring or remove.",
|
|
259
|
+
)
|
|
260
|
+
)
|
|
261
|
+
break
|
|
262
|
+
|
|
263
|
+
def _scan_python(
|
|
264
|
+
self, file_path: Path, content: str, result: DocScanResult
|
|
265
|
+
) -> None:
|
|
266
|
+
"""Scan Python file for documentation issues."""
|
|
267
|
+
try:
|
|
268
|
+
tree = ast.parse(content)
|
|
269
|
+
except SyntaxError:
|
|
270
|
+
return
|
|
271
|
+
|
|
272
|
+
# Check module docstring
|
|
273
|
+
if not ast.get_docstring(tree):
|
|
274
|
+
result.issues.append(
|
|
275
|
+
DocIssue(
|
|
276
|
+
file_path=str(file_path),
|
|
277
|
+
line=1,
|
|
278
|
+
issue_type="missing-module-doc",
|
|
279
|
+
severity=Severity.MEDIUM,
|
|
280
|
+
message="Missing module docstring",
|
|
281
|
+
suggestion="Add module docstring with Purpose, Usage, Dependencies.",
|
|
282
|
+
)
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
# Check functions and classes
|
|
286
|
+
for node in ast.walk(tree):
|
|
287
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
288
|
+
self._check_python_function(file_path, node, result)
|
|
289
|
+
elif isinstance(node, ast.ClassDef):
|
|
290
|
+
self._check_python_class(file_path, node, result)
|
|
291
|
+
|
|
292
|
+
def _check_python_function(
|
|
293
|
+
self,
|
|
294
|
+
file_path: Path,
|
|
295
|
+
node: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
296
|
+
result: DocScanResult,
|
|
297
|
+
) -> None:
|
|
298
|
+
"""Check Python function documentation."""
|
|
299
|
+
# Skip private/dunder methods for docstring requirement
|
|
300
|
+
if node.name.startswith("_") and not node.name.startswith("__"):
|
|
301
|
+
return
|
|
302
|
+
|
|
303
|
+
docstring = ast.get_docstring(node)
|
|
304
|
+
|
|
305
|
+
# Check for missing docstring on public functions
|
|
306
|
+
if not docstring and not node.name.startswith("_"):
|
|
307
|
+
# Skip simple one-liners
|
|
308
|
+
if len(node.body) > 1 or not isinstance(node.body[0], ast.Return):
|
|
309
|
+
result.issues.append(
|
|
310
|
+
DocIssue(
|
|
311
|
+
file_path=str(file_path),
|
|
312
|
+
line=node.lineno,
|
|
313
|
+
issue_type="missing-docstring",
|
|
314
|
+
severity=Severity.MEDIUM,
|
|
315
|
+
message=f"Function `{node.name}` missing docstring",
|
|
316
|
+
suggestion="Add docstring with Args, Returns, Raises.",
|
|
317
|
+
)
|
|
318
|
+
)
|
|
319
|
+
return
|
|
320
|
+
|
|
321
|
+
if not docstring:
|
|
322
|
+
return
|
|
323
|
+
|
|
324
|
+
# Check docstring completeness
|
|
325
|
+
has_args = bool(node.args.args) and not all(
|
|
326
|
+
arg.arg == "self" for arg in node.args.args
|
|
327
|
+
)
|
|
328
|
+
has_return = any(isinstance(n, ast.Return) and n.value for n in ast.walk(node))
|
|
329
|
+
|
|
330
|
+
if has_args and "Args:" not in docstring and "Parameters:" not in docstring:
|
|
331
|
+
result.issues.append(
|
|
332
|
+
DocIssue(
|
|
333
|
+
file_path=str(file_path),
|
|
334
|
+
line=node.lineno,
|
|
335
|
+
issue_type="incomplete-docstring",
|
|
336
|
+
severity=Severity.LOW,
|
|
337
|
+
message=f"`{node.name}` docstring missing Args section",
|
|
338
|
+
suggestion="Document function parameters.",
|
|
339
|
+
)
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
if has_return and "Returns:" not in docstring and "Return:" not in docstring:
|
|
343
|
+
result.issues.append(
|
|
344
|
+
DocIssue(
|
|
345
|
+
file_path=str(file_path),
|
|
346
|
+
line=node.lineno,
|
|
347
|
+
issue_type="incomplete-docstring",
|
|
348
|
+
severity=Severity.LOW,
|
|
349
|
+
message=f"`{node.name}` docstring missing Returns section",
|
|
350
|
+
suggestion="Document return value.",
|
|
351
|
+
)
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
def _check_python_class(
|
|
355
|
+
self, file_path: Path, node: ast.ClassDef, result: DocScanResult
|
|
356
|
+
) -> None:
|
|
357
|
+
"""Check Python class documentation."""
|
|
358
|
+
docstring = ast.get_docstring(node)
|
|
359
|
+
|
|
360
|
+
if not docstring:
|
|
361
|
+
result.issues.append(
|
|
362
|
+
DocIssue(
|
|
363
|
+
file_path=str(file_path),
|
|
364
|
+
line=node.lineno,
|
|
365
|
+
issue_type="missing-docstring",
|
|
366
|
+
severity=Severity.MEDIUM,
|
|
367
|
+
message=f"Class `{node.name}` missing docstring",
|
|
368
|
+
suggestion="Add docstring with purpose and Attributes.",
|
|
369
|
+
)
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
def _scan_typescript(
|
|
373
|
+
self, file_path: Path, content: str, result: DocScanResult
|
|
374
|
+
) -> None:
|
|
375
|
+
"""Scan TypeScript/JavaScript for documentation issues."""
|
|
376
|
+
lines = content.split("\n")
|
|
377
|
+
|
|
378
|
+
# Check for exported functions without JSDoc
|
|
379
|
+
func_pattern = re.compile(
|
|
380
|
+
r"^export\s+(async\s+)?function\s+(\w+)|"
|
|
381
|
+
r"^export\s+const\s+(\w+)\s*=\s*(async\s+)?\("
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
for i, line in enumerate(lines):
|
|
385
|
+
if func_pattern.match(line.strip()):
|
|
386
|
+
has_jsdoc = False
|
|
387
|
+
for j in range(i - 1, max(0, i - 5), -1):
|
|
388
|
+
prev_line = lines[j].strip()
|
|
389
|
+
if prev_line.endswith("*/"):
|
|
390
|
+
has_jsdoc = True
|
|
391
|
+
break
|
|
392
|
+
if prev_line and not prev_line.startswith("//"):
|
|
393
|
+
break
|
|
394
|
+
|
|
395
|
+
if not has_jsdoc and (func_match := func_pattern.match(line.strip())):
|
|
396
|
+
func_name = func_match.group(2) or func_match.group(3)
|
|
397
|
+
result.issues.append(
|
|
398
|
+
DocIssue(
|
|
399
|
+
file_path=str(file_path),
|
|
400
|
+
line=i + 1,
|
|
401
|
+
issue_type="missing-jsdoc",
|
|
402
|
+
severity=Severity.MEDIUM,
|
|
403
|
+
message=f"Exported function `{func_name}` missing JSDoc",
|
|
404
|
+
suggestion="Add JSDoc with @param, @returns, @throws.",
|
|
405
|
+
)
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
def _scan_java(self, file_path: Path, content: str, result: DocScanResult) -> None:
|
|
409
|
+
"""Scan Java for documentation issues."""
|
|
410
|
+
lines = content.split("\n")
|
|
411
|
+
|
|
412
|
+
# Check for public methods without Javadoc
|
|
413
|
+
method_pattern = re.compile(r"^\s*public\s+(?!class)(\w+)\s+(\w+)\s*\(")
|
|
414
|
+
|
|
415
|
+
for i, line in enumerate(lines):
|
|
416
|
+
method_match = method_pattern.match(line)
|
|
417
|
+
if method_match:
|
|
418
|
+
# Check for preceding Javadoc
|
|
419
|
+
has_javadoc = False
|
|
420
|
+
for j in range(i - 1, max(0, i - 10), -1):
|
|
421
|
+
prev_line = lines[j].strip()
|
|
422
|
+
if prev_line.endswith("*/"):
|
|
423
|
+
has_javadoc = True
|
|
424
|
+
break
|
|
425
|
+
if prev_line and not prev_line.startswith("*"):
|
|
426
|
+
break
|
|
427
|
+
|
|
428
|
+
if not has_javadoc:
|
|
429
|
+
method_name = method_match.group(2)
|
|
430
|
+
result.issues.append(
|
|
431
|
+
DocIssue(
|
|
432
|
+
file_path=str(file_path),
|
|
433
|
+
line=i + 1,
|
|
434
|
+
issue_type="missing-javadoc",
|
|
435
|
+
severity=Severity.MEDIUM,
|
|
436
|
+
message=f"Public method `{method_name}` missing Javadoc",
|
|
437
|
+
suggestion="Add Javadoc with @param, @return, @throws.",
|
|
438
|
+
)
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
def _scan_yaml(self, file_path: Path, content: str, result: DocScanResult) -> None:
|
|
442
|
+
"""Scan YAML for documentation issues."""
|
|
443
|
+
lines = content.split("\n")
|
|
444
|
+
|
|
445
|
+
# Check for file header comment
|
|
446
|
+
if lines and not lines[0].strip().startswith("#"):
|
|
447
|
+
result.issues.append(
|
|
448
|
+
DocIssue(
|
|
449
|
+
file_path=str(file_path),
|
|
450
|
+
line=1,
|
|
451
|
+
issue_type="missing-header",
|
|
452
|
+
severity=Severity.LOW,
|
|
453
|
+
message="YAML file missing header comment",
|
|
454
|
+
suggestion="Add comment explaining config purpose.",
|
|
455
|
+
)
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
def _scan_markdown(
|
|
459
|
+
self, file_path: Path, content: str, result: DocScanResult
|
|
460
|
+
) -> None:
|
|
461
|
+
"""Scan Markdown for documentation issues."""
|
|
462
|
+
lines = content.split("\n")
|
|
463
|
+
|
|
464
|
+
# Check for H1 header
|
|
465
|
+
has_h1 = any(line.strip().startswith("# ") for line in lines[:10])
|
|
466
|
+
if not has_h1:
|
|
467
|
+
result.issues.append(
|
|
468
|
+
DocIssue(
|
|
469
|
+
file_path=str(file_path),
|
|
470
|
+
line=1,
|
|
471
|
+
issue_type="missing-h1",
|
|
472
|
+
severity=Severity.LOW,
|
|
473
|
+
message="Markdown file missing H1 title",
|
|
474
|
+
suggestion="Add # Title at start of file.",
|
|
475
|
+
)
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def scan_docs(directory: Path) -> DocScanResult:
|
|
480
|
+
"""Scan directory for documentation issues.
|
|
481
|
+
|
|
482
|
+
Args:
|
|
483
|
+
directory: Root directory to scan.
|
|
484
|
+
|
|
485
|
+
Returns:
|
|
486
|
+
DocScanResult with all issues found.
|
|
487
|
+
"""
|
|
488
|
+
scanner = DocScanner()
|
|
489
|
+
return scanner.scan_directory(directory)
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def generate_docs_report(result: DocScanResult, directory: Path) -> str:
|
|
493
|
+
"""Generate markdown report from scan result.
|
|
494
|
+
|
|
495
|
+
Args:
|
|
496
|
+
result: Scan result to format.
|
|
497
|
+
directory: Scanned directory (for report header).
|
|
498
|
+
|
|
499
|
+
Returns:
|
|
500
|
+
Formatted markdown report.
|
|
501
|
+
"""
|
|
502
|
+
lines = [
|
|
503
|
+
"# Documentation Quality Report",
|
|
504
|
+
"",
|
|
505
|
+
f"**Directory:** `{directory}`",
|
|
506
|
+
f"**Files Scanned:** {result.files_scanned}",
|
|
507
|
+
f"**Languages:** {', '.join(sorted(result.languages)) or 'None detected'}",
|
|
508
|
+
"",
|
|
509
|
+
"## Summary",
|
|
510
|
+
"",
|
|
511
|
+
"| Severity | Count |",
|
|
512
|
+
"|----------|-------|",
|
|
513
|
+
]
|
|
514
|
+
|
|
515
|
+
for severity, count in result.by_severity.items():
|
|
516
|
+
lines.append(f"| {severity} | {count} |")
|
|
517
|
+
|
|
518
|
+
lines.extend(
|
|
519
|
+
[
|
|
520
|
+
"",
|
|
521
|
+
f"**Total Issues:** {len(result.issues)}",
|
|
522
|
+
"",
|
|
523
|
+
]
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
if not result.issues:
|
|
527
|
+
lines.append(f"{Status.SUCCESS} **No documentation issues found.**")
|
|
528
|
+
return "\n".join(lines)
|
|
529
|
+
|
|
530
|
+
# Group by type
|
|
531
|
+
lines.extend(["## Issues by Type", ""])
|
|
532
|
+
|
|
533
|
+
for issue_type, count in sorted(result.by_type.items()):
|
|
534
|
+
lines.append(f"### {issue_type} ({count})")
|
|
535
|
+
lines.append("")
|
|
536
|
+
lines.append("| File | Line | Message | Suggestion |")
|
|
537
|
+
lines.append("|------|------|---------|------------|")
|
|
538
|
+
|
|
539
|
+
type_issues = [i for i in result.issues if i.issue_type == issue_type]
|
|
540
|
+
for issue in type_issues[:50]: # Limit per type
|
|
541
|
+
file_short = Path(issue.file_path).name
|
|
542
|
+
lines.append(
|
|
543
|
+
f"| `{file_short}` | {issue.line} | {issue.message} | {issue.suggestion} |"
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
if len(type_issues) > 50: # noqa: PLR2004 # TECH_DEBT: extract to named constant — G25
|
|
547
|
+
lines.append(f"| ... | ... | *{len(type_issues) - 50} more* | ... |")
|
|
548
|
+
|
|
549
|
+
lines.append("")
|
|
550
|
+
|
|
551
|
+
return "\n".join(lines)
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def validate_docs_tool(directory: str) -> str:
|
|
555
|
+
"""MCP tool: Scan documentation quality.
|
|
556
|
+
|
|
557
|
+
Scans Python, TypeScript, Java, YAML, and Markdown files
|
|
558
|
+
for documentation anti-patterns.
|
|
559
|
+
|
|
560
|
+
Args:
|
|
561
|
+
directory: Path to directory to scan.
|
|
562
|
+
|
|
563
|
+
Returns:
|
|
564
|
+
Summary of documentation issues.
|
|
565
|
+
"""
|
|
566
|
+
dir_path = Path(directory)
|
|
567
|
+
if not dir_path.exists():
|
|
568
|
+
return f"{Status.ERROR} Directory not found: {directory}"
|
|
569
|
+
|
|
570
|
+
result = scan_docs(dir_path)
|
|
571
|
+
report_content = generate_docs_report(result, dir_path)
|
|
572
|
+
|
|
573
|
+
# Auto-set project root from scan directory
|
|
574
|
+
try:
|
|
575
|
+
from ref_agents.session import SessionManager
|
|
576
|
+
|
|
577
|
+
if not SessionManager.get().project_root:
|
|
578
|
+
SessionManager.get().project_root = dir_path.resolve()
|
|
579
|
+
except Exception:
|
|
580
|
+
pass
|
|
581
|
+
|
|
582
|
+
# Save report
|
|
583
|
+
report_path = write_report(
|
|
584
|
+
agent="docs_curator",
|
|
585
|
+
report_name="docs_quality",
|
|
586
|
+
content=report_content,
|
|
587
|
+
title="Documentation Quality Report",
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
if not result.issues:
|
|
591
|
+
return f"{Status.SUCCESS} No documentation issues found in {result.files_scanned} files."
|
|
592
|
+
|
|
593
|
+
by_sev = result.by_severity
|
|
594
|
+
return (
|
|
595
|
+
f"{Status.WARNING} Found {len(result.issues)} documentation issues:\n"
|
|
596
|
+
f" {Severity.HIGH_ICON} High: {by_sev.get(Severity.HIGH, 0)}\n"
|
|
597
|
+
f" {Severity.MEDIUM_ICON} Medium: {by_sev.get(Severity.MEDIUM, 0)}\n"
|
|
598
|
+
f" {Severity.LOW_ICON} Low: {by_sev.get(Severity.LOW, 0)}\n\n"
|
|
599
|
+
f"Report: {report_path}"
|
|
600
|
+
)
|