Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:29:15 +08:00
commit be476a3fea
76 changed files with 12812 additions and 0 deletions

View File

@@ -0,0 +1,411 @@
# Example: Documentation Coverage Validation and Gap Analysis
Complete workflow for analyzing documentation coverage, identifying gaps, and establishing quality gates in CI/CD.
## Context
**Project**: FastAPI + TanStack Start SaaS Platform
**Problem**: Documentation coverage unknown, many functions and API endpoints undocumented
**Goal**: Establish 80% documentation coverage with CI/CD enforcement
**Initial State**:
- No visibility into documentation coverage
- 147 undocumented functions and 23 undocumented API endpoints
- New code merged without documentation requirements
- Partners complained about missing API documentation
## Step 1: TypeScript Documentation Coverage Analysis
```typescript
// scripts/analyze-ts-coverage.ts
import { Project } from "ts-morph";
function analyzeTypeScriptCoverage(projectPath: string) {
const project = new Project({ tsConfigFilePath: `${projectPath}/tsconfig.json` });
const result = { total: 0, documented: 0, undocumented: [] };
project.getSourceFiles().forEach((sourceFile) => {
// Analyze exported functions
sourceFile.getFunctions().filter((fn) => fn.isExported()).forEach((fn) => {
result.total++;
const jsDocs = fn.getJsDocs();
if (jsDocs.length > 0 && jsDocs[0].getDescription().trim().length > 0) {
result.documented++;
} else {
result.undocumented.push({
name: fn.getName() || "(anonymous)",
location: `${sourceFile.getFilePath()}:${fn.getStartLineNumber()}`,
});
}
});
// Analyze interfaces
sourceFile.getInterfaces().forEach((iface) => {
if (!iface.isExported()) return;
result.total++;
if (iface.getJsDocs().length > 0) {
result.documented++;
} else {
result.undocumented.push({
name: iface.getName(),
location: `${sourceFile.getFilePath()}:${iface.getStartLineNumber()}`,
});
}
});
});
const coverage = (result.documented / result.total) * 100;
console.log(`TypeScript Coverage: ${coverage.toFixed(1)}%`);
console.log(`Documented: ${result.documented} / ${result.total}`);
if (result.undocumented.length > 0) {
console.log("\nMissing documentation:");
result.undocumented.forEach((item) => console.log(` - ${item.name} (${item.location})`));
}
if (coverage < 80) {
console.error(`❌ Coverage ${coverage.toFixed(1)}% below threshold 80%`);
process.exit(1);
}
console.log(`✅ Coverage ${coverage.toFixed(1)}% meets threshold`);
}
analyzeTypeScriptCoverage("./app");
```
## Step 2: Python Documentation Coverage Analysis
```python
# scripts/analyze_py_coverage.py
import ast
from pathlib import Path
from typing import List, Dict
class DocstringAnalyzer(ast.NodeVisitor):
def __init__(self):
self.total = 0
self.documented = 0
self.undocumented: List[Dict] = []
self.current_file = ""
def visit_FunctionDef(self, node: ast.FunctionDef):
if node.name.startswith("_"): # Skip private functions
return
self.total += 1
docstring = ast.get_docstring(node)
if docstring and len(docstring.strip()) > 10:
self.documented += 1
else:
self.undocumented.append({
"name": node.name,
"type": "function",
"location": f"{self.current_file}:{node.lineno}"
})
self.generic_visit(node)
def visit_ClassDef(self, node: ast.ClassDef):
self.total += 1
docstring = ast.get_docstring(node)
if docstring and len(docstring.strip()) > 10:
self.documented += 1
else:
self.undocumented.append({
"name": node.name,
"type": "class",
"location": f"{self.current_file}:{node.lineno}"
})
self.generic_visit(node)
def analyze_python_coverage(project_path: str):
analyzer = DocstringAnalyzer()
for py_file in Path(project_path).rglob("*.py"):
if "__pycache__" in str(py_file):
continue
analyzer.current_file = str(py_file)
with open(py_file, "r") as f:
try:
tree = ast.parse(f.read())
analyzer.visit(tree)
except SyntaxError:
print(f"⚠️ Syntax error in {py_file}")
coverage = (analyzer.documented / analyzer.total * 100) if analyzer.total > 0 else 0
print(f"Python Coverage: {coverage:.1f}%")
print(f"Documented: {analyzer.documented} / {analyzer.total}")
if analyzer.undocumented:
print("\nMissing documentation:")
for item in analyzer.undocumented:
print(f" - {item['type']} {item['name']} ({item['location']})")
if coverage < 80:
print(f"❌ Coverage {coverage:.1f}% below threshold 80%")
exit(1)
print(f"✅ Coverage {coverage:.1f}% meets threshold")
analyze_python_coverage("./app")
```
## Step 3: API Endpoint Documentation Coverage
```python
# scripts/analyze_api_coverage.py
from fastapi import FastAPI
def analyze_api_documentation(app: FastAPI):
result = {"total_endpoints": 0, "documented": 0, "undocumented": []}
openapi = app.openapi()
for path, methods in openapi["paths"].items():
for method, details in methods.items():
result["total_endpoints"] += 1
has_summary = bool(details.get("summary"))
has_description = bool(details.get("description"))
if has_summary and has_description:
result["documented"] += 1
else:
missing = []
if not has_summary: missing.append("summary")
if not has_description: missing.append("description")
result["undocumented"].append({
"method": method.upper(),
"path": path,
"missing": missing
})
coverage = (result["documented"] / result["total_endpoints"] * 100)
print(f"API Coverage: {coverage:.1f}%")
print(f"Documented: {result['documented']} / {result['total_endpoints']}")
if result["undocumented"]:
print("\nMissing documentation:")
for endpoint in result["undocumented"]:
missing = ", ".join(endpoint["missing"])
print(f" - {endpoint['method']} {endpoint['path']} (missing: {missing})")
if coverage < 80:
print(f"❌ Coverage {coverage:.1f}% below threshold 80%")
exit(1)
print(f"✅ Coverage {coverage:.1f}% meets threshold")
from app.main import app
analyze_api_documentation(app)
```
## Step 4: Comprehensive HTML Coverage Report
```python
# scripts/generate_coverage_report.py
from jinja2 import Template
from datetime import datetime
def generate_coverage_report(ts_coverage, py_coverage, api_coverage):
template = Template('''
<!DOCTYPE html>
<html>
<head>
<title>Documentation Coverage Report</title>
<style>
body { font-family: Arial; margin: 40px; }
.summary { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; }
.card { border: 1px solid #ddd; padding: 20px; border-radius: 8px; }
.card.pass { border-left: 4px solid #28a745; }
.card.fail { border-left: 4px solid #dc3545; }
.coverage { font-size: 48px; font-weight: bold; margin: 10px 0; }
.undocumented { margin-top: 40px; }
.undocumented li { padding: 8px; background: #f8f9fa; margin: 4px 0; }
</style>
</head>
<body>
<h1>Documentation Coverage Report</h1>
<p>Generated: {{ timestamp }}</p>
<div class="summary">
<div class="card {{ 'pass' if ts_coverage.coverage >= 80 else 'fail' }}">
<h3>TypeScript</h3>
<div class="coverage">{{ "%.1f"|format(ts_coverage.coverage) }}%</div>
<p>{{ ts_coverage.documented }} / {{ ts_coverage.total }}</p>
</div>
<div class="card {{ 'pass' if py_coverage.coverage >= 80 else 'fail' }}">
<h3>Python</h3>
<div class="coverage">{{ "%.1f"|format(py_coverage.coverage) }}%</div>
<p>{{ py_coverage.documented }} / {{ py_coverage.total }}</p>
</div>
<div class="card {{ 'pass' if api_coverage.coverage >= 80 else 'fail' }}">
<h3>API</h3>
<div class="coverage">{{ "%.1f"|format(api_coverage.coverage) }}%</div>
<p>{{ api_coverage.documented }} / {{ api_coverage.total_endpoints }}</p>
</div>
</div>
{% for section in [ts_coverage, py_coverage] %}
{% if section.undocumented %}
<div class="undocumented">
<h2>{{ section.name }} - Missing Documentation</h2>
<ul>
{% for item in section.undocumented %}
<li><strong>{{ item.name }}</strong> - {{ item.location }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% endfor %}
</body>
</html>
''')
html = template.render(
timestamp=datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
ts_coverage=ts_coverage,
py_coverage=py_coverage,
api_coverage=api_coverage
)
with open("docs/coverage-report.html", "w") as f:
f.write(html)
print("📊 Coverage report generated: docs/coverage-report.html")
```
## Step 5: CI/CD Integration
```yaml
# .github/workflows/documentation-coverage.yml
name: Documentation Coverage
on:
pull_request:
push:
branches: [main]
jobs:
documentation-coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install dependencies
run: |
npm install
pip install -r requirements.txt jinja2
- name: Check TypeScript coverage
run: npx ts-node scripts/analyze-ts-coverage.ts
- name: Check Python coverage
run: python scripts/analyze_py_coverage.py
- name: Check API coverage
run: python scripts/analyze_api_coverage.py
- name: Generate report
if: always()
run: python scripts/generate_coverage_report.py
- name: Upload report
if: always()
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: docs/coverage-report.html
- name: Comment on PR
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '📊 Documentation coverage report generated. Check artifacts.'
});
```
## Results
### Before
- Documentation coverage: unknown
- No visibility into gaps
- 147 undocumented functions
- 23 undocumented API endpoints
- New code merged without docs
- Partners complained about missing docs
### After
- TypeScript coverage: 42% → 87%
- Python coverage: 38% → 91%
- API endpoint coverage: 51% → 95%
- CI/CD enforcement (fails build if <80%)
- Automated HTML reports
### Improvements
- Undocumented functions: 147 → 18 (88% reduction)
- Undocumented endpoints: 23 → 1 (96% reduction)
- Time to find function docs: 15 min → instant
- Partner onboarding: 2 weeks → 3 days
- Documentation debt: eliminated weekly
### Developer Feedback
- "Coverage reports made it clear what needed docs"
- "CI/CD enforcement prevented new undocumented code"
- "HTML report showed exactly what was missing"
- "80% threshold is challenging but achievable"
## Key Lessons
1. **Automated Analysis**: Manual tracking doesn't scale
2. **CI/CD Enforcement**: Prevents documentation regression
3. **Visibility**: Reports show exactly what's missing
4. **Threshold-Based**: 80% coverage is achievable and meaningful
5. **Multi-Language**: Each language needs appropriate tooling (ts-morph, AST, OpenAPI)
6. **HTML Reports**: Visual representation drives action
## Prevention Measures
**Implemented**:
- [x] TypeScript coverage analysis (ts-morph)
- [x] Python coverage analysis (AST)
- [x] API endpoint documentation check
- [x] HTML coverage reports
- [x] CI/CD integration (fails below 80%)
- [x] PR comments with coverage status
**Ongoing**:
- [ ] Pre-commit hooks (warn if adding undocumented code)
- [ ] Dashboard showing coverage trends over time
- [ ] Team documentation KPIs (quarterly review)
- [ ] Automated "most undocumented files" weekly report
---
Related: [openapi-generation.md](openapi-generation.md) | [architecture-docs.md](architecture-docs.md) | [Return to INDEX](INDEX.md)