# 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(''' Documentation Coverage Report

Documentation Coverage Report

Generated: {{ timestamp }}

TypeScript

{{ "%.1f"|format(ts_coverage.coverage) }}%

{{ ts_coverage.documented }} / {{ ts_coverage.total }}

Python

{{ "%.1f"|format(py_coverage.coverage) }}%

{{ py_coverage.documented }} / {{ py_coverage.total }}

API

{{ "%.1f"|format(api_coverage.coverage) }}%

{{ api_coverage.documented }} / {{ api_coverage.total_endpoints }}

{% for section in [ts_coverage, py_coverage] %} {% if section.undocumented %}

{{ section.name }} - Missing Documentation

{% endif %} {% endfor %} ''') 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)