# 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('''
Generated: {{ timestamp }}
{{ ts_coverage.documented }} / {{ ts_coverage.total }}
{{ py_coverage.documented }} / {{ py_coverage.total }}
{{ api_coverage.documented }} / {{ api_coverage.total_endpoints }}