diff --git a/CLAUDE.md b/CLAUDE.md
index 00a6653..fda15dd 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -34,6 +34,11 @@ aidocs export-pdf docs/page.md
 # Watch mode (auto-sync on file changes)
 aidocs watch              # Watch docs/ and auto-chunk on changes
 aidocs watch --with-vectors  # Also generate embeddings
+
+# Documentation coverage analysis
+aidocs coverage           # Show coverage report
+aidocs coverage --format json  # Machine-readable output
+aidocs coverage --ci      # Exit code 1 if below 80%
 ```
 
 ## Architecture
@@ -41,9 +46,10 @@ aidocs watch --with-vectors  # Also generate embeddings
 ```
 src/aidocs_cli/
 ├── __init__.py       # Version and entry point
-├── cli.py            # Typer CLI commands (init, check, serve, rag-*, export-pdf, watch)
+├── cli.py            # Typer CLI commands (init, check, serve, rag-*, export-pdf, watch, coverage)
 ├── installer.py      # Copies templates to target project (.claude/commands/, .claude/workflows/)
 ├── chunker.py        # Splits markdown at ## headings for RAG
+├── coverage.py       # Documentation coverage analysis (routes, components, models detection)
 ├── embeddings.py     # OpenAI embeddings + SQL generation for pgvector
 ├── server.py         # MkDocs config generation and nav discovery
 ├── pdf_exporter.py   # Markdown→HTML→PDF with Chrome/Playwright
@@ -59,6 +65,7 @@ src/aidocs_cli/
 - **CLI (cli.py)**: Uses Typer with Rich for terminal UI. Entry point is `app()`.
 - **Installer**: Copies command/workflow templates to target project's `.claude/` directory (or `.cursor/` for Cursor).
 - **Chunker**: Creates `.chunks.json` files alongside markdown, tracks changes via `docs/.chunks/manifest.json`.
+- **Coverage**: Analyzes codebase for routes/components/models, matches against docs, reports coverage with visual progress bars.
 - **Embeddings**: Calls OpenAI API (text-embedding-3-small, 1536 dimensions), outputs `docs/.chunks/sync.sql` for pgvector import.
 - **Server**: Auto-discovers nav structure from folder hierarchy, generates ephemeral `mkdocs.yml`.
 
diff --git a/README.md b/README.md
index f7ec0ac..c6f85a8 100644
--- a/README.md
+++ b/README.md
@@ -548,6 +548,69 @@ aidocs serve
 # Edit docs in your editor - changes auto-sync!
 ```
 
+### `aidocs coverage`
+
+Analyze documentation coverage for your codebase. Scans for routes, components, and models, then checks which items are mentioned in your documentation.
+
+```bash
+aidocs coverage                     # Show coverage summary
+aidocs coverage --format json       # Machine-readable output
+aidocs coverage --format csv        # CSV export
+aidocs coverage --ci                # Exit code 1 if below 80%
+aidocs coverage --threshold 70      # Custom threshold
+aidocs coverage -c ./src            # Specify codebase path
+aidocs coverage --all               # Show all items
+```
+
+**Options:**
+| Option | Description |
+|--------|-------------|
+| `--codebase, -c` | Path to codebase root (default: parent of docs dir) |
+| `--format, -f` | Output format: `summary`, `json`, or `csv` |
+| `--threshold, -t` | Minimum coverage percentage (exit 1 if below) |
+| `--ci` | CI mode: exit 1 if coverage below 80% |
+| `--save/--no-save` | Save report to `.chunks/coverage.json` (default: save) |
+| `--all, -a` | Show all items (documented and undocumented) |
+
+**Example output:**
+```
+╭───────────────────────────────────────────────╮
+│ Documentation Coverage Report                 │
+│ ───────────────────────────────────────────── │
+│                                               │
+│   Routes:       12/15  ( 80%)  █████████░░░   │
+│   Components:    8/20  ( 40%)  ████░░░░░░░░   │
+│   Models:        5/5   (100%)  ████████████   │
+│                                               │
+│   Overall:    25/40  (63%)  ███████░░░░░      │
+╰───────────────────────────────────────────────╯
+
+Missing documentation:
+  Routes:
+    ✗ POST /api/webhooks/stripe
+      src/app/api/webhooks/stripe/route.ts:1
+  Components:
+    ✗ PaymentForm
+      src/components/PaymentForm.tsx:1
+```
+
+**Supported frameworks:**
+- **Next.js** - App Router routes and pages
+- **React** - Function and class components
+- **Vue/Svelte** - Single-file components
+- **Express** - Route handlers
+- **FastAPI/Flask** - Python API routes
+- **Laravel** - PHP routes
+- **Prisma** - Database models
+- **TypeScript** - Interfaces and types
+
+**CI/CD integration:**
+```yaml
+# GitHub Actions example
+- name: Check documentation coverage
+  run: aidocs coverage --ci
+```
+
 ## Slash Commands
 
 After running `aidocs init`, these commands are available in Claude Code:
diff --git a/pyproject.toml b/pyproject.toml
index e4fa8ed..3bf75ca 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
 [project]
 name = "aidocs"
-version = "0.17.0"
+version = "0.18.0"
 description = "AI-powered documentation generator for web applications. Install docs commands into your Claude Code project."
 readme = "README.md"
 license = { text = "MIT" }
diff --git a/src/aidocs_cli/cli.py b/src/aidocs_cli/cli.py
index 38dec26..5655bc1 100644
--- a/src/aidocs_cli/cli.py
+++ b/src/aidocs_cli/cli.py
@@ -12,6 +12,7 @@
 
 from . import __version__
 from .chunker import chunk_directory
+from .coverage import analyze_coverage, save_coverage_report
 from .embeddings import generate_sync_sql, get_openai_api_key
 from .installer import check_tools, install_docs_module
 from .pdf_exporter import export_markdown_to_pdf
@@ -906,5 +907,293 @@ def watch(
         raise typer.Exit(1)
 
 
+def _render_progress_bar(percent: float, width: int = 12) -> str:
+    """Render a text-based progress bar."""
+    filled = int(width * percent / 100)
+    empty = width - filled
+    return "█" * filled + "░" * empty
+
+
+def _get_coverage_color(percent: float) -> str:
+    """Get color based on coverage percentage."""
+    if percent >= 80:
+        return "green"
+    elif percent >= 50:
+        return "yellow"
+    else:
+        return "red"
+
+
+@app.command("coverage")
+def coverage(
+    docs_dir: Optional[str] = typer.Argument(
+        "docs",
+        help="Directory containing documentation.",
+    ),
+    codebase: Optional[str] = typer.Option(
+        None,
+        "--codebase",
+        "-c",
+        help="Path to codebase root (default: parent of docs dir).",
+    ),
+    format_output: str = typer.Option(
+        "summary",
+        "--format",
+        "-f",
+        help="Output format: summary, json, or csv.",
+    ),
+    threshold: Optional[float] = typer.Option(
+        None,
+        "--threshold",
+        "-t",
+        help="Minimum coverage percentage (exit 1 if below).",
+    ),
+    ci: bool = typer.Option(
+        False,
+        "--ci",
+        help="CI mode: exit 1 if coverage below 80%% (shorthand for --threshold 80).",
+    ),
+    save: bool = typer.Option(
+        True,
+        "--save/--no-save",
+        help="Save report to .chunks/coverage.json.",
+    ),
+    show_all: bool = typer.Option(
+        False,
+        "--all",
+        "-a",
+        help="Show all items (documented and undocumented).",
+    ),
+) -> None:
+    """Analyze documentation coverage for your codebase.
+
+    Scans your codebase for routes, components, and models, then checks
+    which items are mentioned in your documentation.
+
+    Examples:
+        aidocs coverage                     # Show coverage summary
+        aidocs coverage --format json       # Machine-readable output
+        aidocs coverage --ci                # Exit 1 if below 80%
+        aidocs coverage --threshold 70      # Custom threshold
+        aidocs coverage -c ./src            # Specify codebase path
+        aidocs coverage --all               # Show all items
+    """
+    import json as json_module
+
+    target_docs = Path(docs_dir)
+
+    if not target_docs.exists():
+        console.print(f"[red]Error: Documentation directory not found: {docs_dir}[/red]")
+        raise typer.Exit(1)
+
+    if not target_docs.is_dir():
+        console.print(f"[red]Error: Not a directory: {docs_dir}[/red]")
+        raise typer.Exit(1)
+
+    # Determine codebase directory
+    if codebase:
+        codebase_dir = Path(codebase)
+    else:
+        # Default: parent of docs dir (or current dir if docs is at root)
+        codebase_dir = target_docs.parent
+        if codebase_dir == target_docs:
+            codebase_dir = Path.cwd()
+
+    if not codebase_dir.exists():
+        console.print(f"[red]Error: Codebase directory not found: {codebase_dir}[/red]")
+        raise typer.Exit(1)
+
+    # Set threshold for CI mode
+    effective_threshold = threshold
+    if ci and threshold is None:
+        effective_threshold = 80.0
+
+    # JSON format - minimal output
+    if format_output == "json":
+        def on_status(msg: str) -> None:
+            pass  # Suppress status messages for JSON output
+
+        try:
+            report = analyze_coverage(codebase_dir, target_docs, on_status=on_status)
+
+            if save:
+                save_coverage_report(report, target_docs)
+
+            console.print(json_module.dumps(report.to_dict(), indent=2))
+
+            # Check threshold
+            if effective_threshold is not None:
+                if report.overall_coverage < effective_threshold:
+                    raise typer.Exit(1)
+
+        except typer.Exit:
+            raise
+        except Exception as e:
+            error_output = {"success": False, "error": str(e)}
+            console.print(json_module.dumps(error_output))
+            raise typer.Exit(1)
+
+        return
+
+    # CSV format
+    if format_output == "csv":
+        import csv
+        import io
+
+        def on_status(msg: str) -> None:
+            pass
+
+        try:
+            report = analyze_coverage(codebase_dir, target_docs, on_status=on_status)
+
+            if save:
+                save_coverage_report(report, target_docs)
+
+            # Use csv module for proper RFC 4180 escaping
+            output = io.StringIO()
+            writer = csv.writer(output)
+
+            # Write header
+            writer.writerow(["category", "name", "file", "line", "documented"])
+
+            for cat_name, category in report.categories.items():
+                for item in category.items:
+                    doc_status = "yes" if item.documented else "no"
+                    writer.writerow([cat_name, item.name, item.file_path, item.line_number, doc_status])
+
+            console.print(output.getvalue().rstrip())
+
+            # Check threshold
+            if effective_threshold is not None:
+                if report.overall_coverage < effective_threshold:
+                    raise typer.Exit(1)
+
+        except typer.Exit:
+            raise
+        except Exception as e:
+            console.print(f"[red]Error: {e}[/red]")
+            raise typer.Exit(1)
+
+        return
+
+    # Summary format (default) - rich output
+    console.print("[blue]Analyzing documentation coverage...[/blue]")
+    console.print()
+
+    def on_status(msg: str) -> None:
+        console.print(f"  [dim]{msg}[/dim]")
+
+    try:
+        report = analyze_coverage(codebase_dir, target_docs, on_status=on_status)
+        console.print()
+
+        if save:
+            report_path = save_coverage_report(report, target_docs)
+
+        # Check if we found anything
+        if not report.categories:
+            console.print(Panel.fit(
+                "[yellow]No documentable items found in codebase[/yellow]\n\n"
+                f"Searched: {codebase_dir}\n\n"
+                "[dim]Ensure the codebase path is correct and contains\n"
+                "routes, components, or models.[/dim]",
+                title="Coverage Analysis",
+                border_style="yellow",
+            ))
+            return
+
+        # Build summary output
+        summary_lines = []
+        overall_color = _get_coverage_color(report.overall_coverage)
+
+        for cat_name, category in report.categories.items():
+            color = _get_coverage_color(category.coverage_percent)
+            bar = _render_progress_bar(category.coverage_percent)
+            percent = f"{category.coverage_percent:.0f}%"
+            ratio = f"{category.documented}/{category.total}"
+
+            # Pad category name for alignment
+            display_name = category.name + ":"
+            summary_lines.append(
+                f"  [{color}]{display_name:<12}[/{color}] {ratio:>6}  ({percent:>4})  {bar}"
+            )
+
+        summary_text = "\n".join(summary_lines)
+
+        # Overall coverage line
+        overall_bar = _render_progress_bar(report.overall_coverage)
+        overall_text = (
+            f"\n  [bold]Overall:[/bold]     "
+            f"{report.total_documented}/{report.total_items}  "
+            f"([{overall_color}]{report.overall_coverage:.0f}%[/{overall_color}])  {overall_bar}"
+        )
+
+        console.print(Panel.fit(
+            f"[bold]Documentation Coverage Report[/bold]\n"
+            f"{'─' * 45}\n\n"
+            f"{summary_text}\n"
+            f"{overall_text}",
+            border_style=overall_color,
+        ))
+
+        # Show undocumented items
+        has_undocumented = any(cat.undocumented for cat in report.categories.values())
+
+        if has_undocumented:
+            console.print()
+            console.print("[bold]Missing documentation:[/bold]")
+
+            for cat_name, category in report.categories.items():
+                if category.undocumented:
+                    console.print(f"\n  [dim]{category.name}:[/dim]")
+                    # Show up to 10 items per category
+                    items_to_show = category.undocumented[:10]
+                    for item in items_to_show:
+                        console.print(f"    [red]✗[/red] {item.name}")
+                        console.print(f"      [dim]{item.file_path}:{item.line_number}[/dim]")
+
+                    remaining = len(category.undocumented) - 10
+                    if remaining > 0:
+                        console.print(f"    [dim]... and {remaining} more[/dim]")
+
+        # Show all items if requested
+        if show_all:
+            console.print()
+            console.print("[bold]All items:[/bold]")
+
+            for cat_name, category in report.categories.items():
+                console.print(f"\n  [dim]{category.name}:[/dim]")
+                for item in category.items:
+                    icon = "[green]✓[/green]" if item.documented else "[red]✗[/red]"
+                    console.print(f"    {icon} {item.name}")
+                    console.print(f"      [dim]{item.file_path}:{item.line_number}[/dim]")
+
+        # Show where report was saved
+        if save:
+            console.print()
+            console.print(f"[dim]Report saved to: {report_path}[/dim]")
+
+        # Check threshold
+        if effective_threshold is not None:
+            console.print()
+            if report.overall_coverage >= effective_threshold:
+                console.print(
+                    f"[green]✓ Coverage {report.overall_coverage:.1f}% "
+                    f"meets threshold of {effective_threshold}%[/green]"
+                )
+            else:
+                console.print(
+                    f"[red]✗ Coverage {report.overall_coverage:.1f}% "
+                    f"is below threshold of {effective_threshold}%[/red]"
+                )
+                raise typer.Exit(1)
+
+    except typer.Exit:
+        raise
+    except Exception as e:
+        console.print(f"[red]Error: {e}[/red]")
+        raise typer.Exit(1)
+
+
 if __name__ == "__main__":
     app()
diff --git a/src/aidocs_cli/coverage.py b/src/aidocs_cli/coverage.py
new file mode 100644
index 0000000..bf001ac
--- /dev/null
+++ b/src/aidocs_cli/coverage.py
@@ -0,0 +1,787 @@
+"""Documentation coverage analysis for codebases."""
+
+import json
+import re
+from dataclasses import dataclass, field
+from datetime import datetime, timezone
+from pathlib import Path
+from typing import Optional, Callable
+
+
+@dataclass
+class CoverageItem:
+    """Represents a documentable item in the codebase."""
+    category: str  # routes, components, models, functions, etc.
+    name: str
+    file_path: str
+    line_number: int
+    documented: bool = False
+    doc_file: Optional[str] = None
+
+
+@dataclass
+class CoverageCategory:
+    """Coverage statistics for a category."""
+    name: str
+    total: int = 0
+    documented: int = 0
+    items: list[CoverageItem] = field(default_factory=list)
+
+    @property
+    def coverage_percent(self) -> float:
+        if self.total == 0:
+            return 100.0
+        return (self.documented / self.total) * 100
+
+    @property
+    def undocumented(self) -> list[CoverageItem]:
+        return [item for item in self.items if not item.documented]
+
+
+@dataclass
+class CoverageReport:
+    """Complete coverage report."""
+    categories: dict[str, CoverageCategory] = field(default_factory=dict)
+    analyzed_at: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
+    codebase_dir: str = ""
+    docs_dir: str = ""
+
+    @property
+    def total_items(self) -> int:
+        return sum(cat.total for cat in self.categories.values())
+
+    @property
+    def total_documented(self) -> int:
+        return sum(cat.documented for cat in self.categories.values())
+
+    @property
+    def overall_coverage(self) -> float:
+        if self.total_items == 0:
+            return 100.0
+        return (self.total_documented / self.total_items) * 100
+
+    def to_dict(self) -> dict:
+        """Convert to dictionary for JSON export."""
+        return {
+            "analyzed_at": self.analyzed_at,
+            "codebase_dir": self.codebase_dir,
+            "docs_dir": self.docs_dir,
+            "summary": {
+                "total_items": self.total_items,
+                "documented": self.total_documented,
+                "coverage_percent": round(self.overall_coverage, 1),
+            },
+            "categories": {
+                name: {
+                    "total": cat.total,
+                    "documented": cat.documented,
+                    "coverage_percent": round(cat.coverage_percent, 1),
+                    "undocumented": [
+                        {
+                            "name": item.name,
+                            "file": item.file_path,
+                            "line": item.line_number,
+                        }
+                        for item in cat.undocumented
+                    ]
+                }
+                for name, cat in self.categories.items()
+            }
+        }
+
+
+# === Route Detection Patterns ===
+
+# Next.js App Router patterns
+NEXTJS_APP_ROUTE_PATTERNS = [
+    # route.ts/js handlers
+    (r"export\s+(?:async\s+)?function\s+(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)", "method"),
+    # page.tsx exports
+    (r"export\s+default\s+(?:async\s+)?function\s+(\w+Page|\w+)", "page"),
+]
+
+# Next.js Pages Router patterns
+NEXTJS_PAGES_ROUTE_PATTERNS = [
+    (r"export\s+default\s+(?:async\s+)?function\s+(\w+)", "page"),
+    (r"export\s+(?:const|async\s+function)\s+(getServerSideProps|getStaticProps)", "data"),
+]
+
+# Express/Node.js patterns
+EXPRESS_ROUTE_PATTERNS = [
+    (r"(?:app|router)\.(get|post|put|patch|delete|all)\s*\(\s*['\"`]([^'\"`]+)['\"`]", "route"),
+    (r"@(Get|Post|Put|Patch|Delete|All)\s*\(\s*['\"`]?([^'\"`\)]*)['\"`]?\s*\)", "decorator"),
+]
+
+# Python FastAPI/Flask patterns
+PYTHON_ROUTE_PATTERNS = [
+    (r"@(?:app|router|api)\.(get|post|put|patch|delete)\s*\(\s*['\"`]([^'\"`]+)['\"`]", "route"),
+    (r"@(?:app|router)\.route\s*\(\s*['\"`]([^'\"`]+)['\"`]", "route"),
+]
+
+# Laravel patterns
+LARAVEL_ROUTE_PATTERNS = [
+    (r"Route::(get|post|put|patch|delete|any)\s*\(\s*['\"`]([^'\"`]+)['\"`]", "route"),
+]
+
+
+# === Component Detection Patterns ===
+
+REACT_COMPONENT_PATTERNS = [
+    # Function components
+    (r"export\s+(?:default\s+)?function\s+([A-Z]\w+)", "function"),
+    # Arrow function components
+    (r"export\s+(?:default\s+)?(?:const|let)\s+([A-Z]\w+)\s*[=:]\s*(?:\([^)]*\)|[^=])*\s*=>", "arrow"),
+    # Class components
+    (r"export\s+(?:default\s+)?class\s+([A-Z]\w+)\s+extends\s+(?:React\.)?(?:Component|PureComponent)", "class"),
+]
+
+VUE_COMPONENT_PATTERNS = [
+    (r"export\s+default\s+(?:defineComponent\s*\(\s*)?{", "options"),
+    (r"<script[^>]*setup[^>]*>", "setup"),
+]
+
+SVELTE_COMPONENT_PATTERNS = [
+    (r"<script[^>]*>", "script"),
+]
+
+
+# === Model Detection Patterns ===
+
+# TypeScript/JavaScript
+TS_MODEL_PATTERNS = [
+    (r"(?:export\s+)?(?:interface|type)\s+([A-Z]\w+)", "type"),
+    (r"(?:export\s+)?class\s+([A-Z]\w+)(?:\s+extends|\s+implements|\s*{)", "class"),
+]
+
+# Python
+PYTHON_MODEL_PATTERNS = [
+    (r"class\s+([A-Z]\w+)\s*\([^)]*(?:Model|Base|Schema|BaseModel)[^)]*\)", "model"),
+    (r"class\s+([A-Z]\w+)\s*\(.*\):", "class"),
+]
+
+# Prisma
+PRISMA_MODEL_PATTERNS = [
+    (r"model\s+([A-Z]\w+)\s*{", "model"),
+]
+
+
+def detect_framework(codebase_dir: Path) -> dict[str, bool]:
+    """Detect which frameworks are used in the codebase."""
+    frameworks = {
+        "nextjs": False,
+        "react": False,
+        "vue": False,
+        "svelte": False,
+        "express": False,
+        "fastapi": False,
+        "flask": False,
+        "laravel": False,
+        "prisma": False,
+    }
+
+    # Check package.json
+    pkg_json = codebase_dir / "package.json"
+    if pkg_json.exists():
+        try:
+            pkg = json.loads(pkg_json.read_text())
+            deps = {**pkg.get("dependencies", {}), **pkg.get("devDependencies", {})}
+
+            if "next" in deps:
+                frameworks["nextjs"] = True
+                frameworks["react"] = True
+            elif "react" in deps:
+                frameworks["react"] = True
+            if "vue" in deps:
+                frameworks["vue"] = True
+            if "svelte" in deps:
+                frameworks["svelte"] = True
+            if "express" in deps:
+                frameworks["express"] = True
+        except Exception:
+            pass
+
+    # Check pyproject.toml or requirements.txt
+    pyproject = codebase_dir / "pyproject.toml"
+    requirements = codebase_dir / "requirements.txt"
+
+    py_deps = ""
+    if pyproject.exists():
+        py_deps = pyproject.read_text().lower()
+    if requirements.exists():
+        py_deps += requirements.read_text().lower()
+
+    if "fastapi" in py_deps:
+        frameworks["fastapi"] = True
+    if "flask" in py_deps:
+        frameworks["flask"] = True
+
+    # Check composer.json for Laravel
+    composer = codebase_dir / "composer.json"
+    if composer.exists():
+        try:
+            comp = json.loads(composer.read_text())
+            if "laravel/framework" in comp.get("require", {}):
+                frameworks["laravel"] = True
+        except Exception:
+            pass
+
+    # Check for Prisma
+    if (codebase_dir / "prisma" / "schema.prisma").exists():
+        frameworks["prisma"] = True
+
+    return frameworks
+
+
+def find_routes(codebase_dir: Path, frameworks: dict[str, bool]) -> list[CoverageItem]:
+    """Find all routes/endpoints in the codebase."""
+    routes = []
+
+    # Next.js App Router
+    if frameworks.get("nextjs"):
+        app_dir = codebase_dir / "app"
+        if not app_dir.exists():
+            app_dir = codebase_dir / "src" / "app"
+
+        if app_dir.exists():
+            # Find route.ts/js files
+            route_extensions = ["ts", "tsx", "js", "jsx"]
+            for ext in route_extensions:
+                for route_file in app_dir.rglob(f"route.{ext}"):
+                    try:
+                        content = route_file.read_text(encoding="utf-8")
+                    except Exception:
+                        continue
+
+                    rel_path = route_file.relative_to(app_dir)
+
+                    # Extract route path from file location (parent excludes filename)
+                    route_path = "/" + str(rel_path.parent).replace("\\", "/")
+                    route_path = re.sub(r"\([^)]+\)/", "", route_path)  # Remove route groups
+                    # Normalize root path
+                    if route_path in ("", "/.", "/."):
+                        route_path = "/"
+
+                    for pattern, _ in NEXTJS_APP_ROUTE_PATTERNS:
+                        for match in re.finditer(pattern, content):
+                            method = match.group(1)
+                            line_num = content[:match.start()].count("\n") + 1
+                            routes.append(CoverageItem(
+                                category="routes",
+                                name=f"{method} {route_path}",
+                                file_path=str(route_file.relative_to(codebase_dir)),
+                                line_number=line_num,
+                            ))
+
+            # Find page.tsx files (GET routes implicitly)
+            for ext in route_extensions:
+                for page_file in app_dir.rglob(f"page.{ext}"):
+                    rel_path = page_file.relative_to(app_dir)
+                    # Extract route path from file location (parent excludes filename)
+                    route_path = "/" + str(rel_path.parent).replace("\\", "/")
+                    route_path = re.sub(r"\([^)]+\)/", "", route_path)  # Remove route groups
+                    # Normalize root path
+                    if route_path in ("", "/", "/."):
+                        route_path = "/"
+
+                    routes.append(CoverageItem(
+                        category="routes",
+                        name=f"PAGE {route_path}",
+                        file_path=str(page_file.relative_to(codebase_dir)),
+                        line_number=1,
+                    ))
+
+    # Express routes
+    if frameworks.get("express"):
+        for ext in ["ts", "js"]:
+            for file in codebase_dir.rglob(f"*.{ext}"):
+                if "node_modules" in str(file):
+                    continue
+                try:
+                    content = file.read_text()
+                    for pattern, _ in EXPRESS_ROUTE_PATTERNS:
+                        for match in re.finditer(pattern, content):
+                            method = match.group(1).upper()
+                            path = match.group(2)
+                            line_num = content[:match.start()].count("\n") + 1
+                            routes.append(CoverageItem(
+                                category="routes",
+                                name=f"{method} {path}",
+                                file_path=str(file.relative_to(codebase_dir)),
+                                line_number=line_num,
+                            ))
+                except Exception:
+                    continue
+
+    # Python routes (FastAPI/Flask)
+    if frameworks.get("fastapi") or frameworks.get("flask"):
+        for file in codebase_dir.rglob("*.py"):
+            if "__pycache__" in str(file) or ".venv" in str(file):
+                continue
+            try:
+                content = file.read_text()
+                for pattern, _ in PYTHON_ROUTE_PATTERNS:
+                    for match in re.finditer(pattern, content):
+                        if len(match.groups()) == 2:
+                            method = match.group(1).upper()
+                            path = match.group(2)
+                        else:
+                            method = "ANY"
+                            path = match.group(1)
+                        line_num = content[:match.start()].count("\n") + 1
+                        routes.append(CoverageItem(
+                            category="routes",
+                            name=f"{method} {path}",
+                            file_path=str(file.relative_to(codebase_dir)),
+                            line_number=line_num,
+                        ))
+            except Exception:
+                continue
+
+    # Laravel routes
+    if frameworks.get("laravel"):
+        routes_dir = codebase_dir / "routes"
+        if routes_dir.exists():
+            for file in routes_dir.rglob("*.php"):
+                try:
+                    content = file.read_text()
+                    for pattern, _ in LARAVEL_ROUTE_PATTERNS:
+                        for match in re.finditer(pattern, content):
+                            method = match.group(1).upper()
+                            path = match.group(2)
+                            line_num = content[:match.start()].count("\n") + 1
+                            routes.append(CoverageItem(
+                                category="routes",
+                                name=f"{method} {path}",
+                                file_path=str(file.relative_to(codebase_dir)),
+                                line_number=line_num,
+                            ))
+                except Exception:
+                    continue
+
+    return routes
+
+
+def find_components(codebase_dir: Path, frameworks: dict[str, bool]) -> list[CoverageItem]:
+    """Find all UI components in the codebase."""
+    components = []
+
+    # Directories to search for components
+    component_dirs = [
+        "components", "src/components", "app/components",
+        "lib/components", "src/lib/components",
+        "ui", "src/ui",
+    ]
+
+    search_dirs = []
+    for dir_name in component_dirs:
+        comp_dir = codebase_dir / dir_name
+        if comp_dir.exists():
+            search_dirs.append(comp_dir)
+
+    # If no component directories found, search more broadly
+    if not search_dirs:
+        search_dirs = [codebase_dir]
+
+    # React/Next.js components
+    if frameworks.get("react") or frameworks.get("nextjs"):
+        for search_dir in search_dirs:
+            for ext in ["tsx", "jsx", "ts", "js"]:
+                for file in search_dir.rglob(f"*.{ext}"):
+                    if "node_modules" in str(file) or ".next" in str(file):
+                        continue
+
+                    # Skip test files
+                    if ".test." in str(file) or ".spec." in str(file) or "__tests__" in str(file):
+                        continue
+
+                    try:
+                        content = file.read_text()
+                        for pattern, comp_type in REACT_COMPONENT_PATTERNS:
+                            for match in re.finditer(pattern, content):
+                                name = match.group(1)
+                                line_num = content[:match.start()].count("\n") + 1
+                                components.append(CoverageItem(
+                                    category="components",
+                                    name=name,
+                                    file_path=str(file.relative_to(codebase_dir)),
+                                    line_number=line_num,
+                                ))
+                    except Exception:
+                        continue
+
+    # Vue components
+    if frameworks.get("vue"):
+        for search_dir in search_dirs:
+            for file in search_dir.rglob("*.vue"):
+                if "node_modules" in str(file):
+                    continue
+                components.append(CoverageItem(
+                    category="components",
+                    name=file.stem,
+                    file_path=str(file.relative_to(codebase_dir)),
+                    line_number=1,
+                ))
+
+    # Svelte components
+    if frameworks.get("svelte"):
+        for search_dir in search_dirs:
+            for file in search_dir.rglob("*.svelte"):
+                if "node_modules" in str(file):
+                    continue
+                components.append(CoverageItem(
+                    category="components",
+                    name=file.stem,
+                    file_path=str(file.relative_to(codebase_dir)),
+                    line_number=1,
+                ))
+
+    return components
+
+
+def find_models(codebase_dir: Path, frameworks: dict[str, bool]) -> list[CoverageItem]:
+    """Find all data models in the codebase."""
+    models = []
+
+    # Prisma models
+    if frameworks.get("prisma"):
+        schema_path = codebase_dir / "prisma" / "schema.prisma"
+        if schema_path.exists():
+            content = schema_path.read_text()
+            for pattern, _ in PRISMA_MODEL_PATTERNS:
+                for match in re.finditer(pattern, content):
+                    name = match.group(1)
+                    line_num = content[:match.start()].count("\n") + 1
+                    models.append(CoverageItem(
+                        category="models",
+                        name=name,
+                        file_path="prisma/schema.prisma",
+                        line_number=line_num,
+                    ))
+
+    # TypeScript types/interfaces
+    type_dirs = [
+        "types", "src/types", "lib/types",
+        "models", "src/models", "lib/models",
+        "schemas", "src/schemas",
+    ]
+
+    for dir_name in type_dirs:
+        type_dir = codebase_dir / dir_name
+        if type_dir.exists():
+            for file in type_dir.rglob("*.ts"):
+                if "node_modules" in str(file):
+                    continue
+                try:
+                    content = file.read_text()
+                    for pattern, _ in TS_MODEL_PATTERNS:
+                        for match in re.finditer(pattern, content):
+                            name = match.group(1)
+                            # Skip common utility types
+                            if name in ["Props", "State", "Context", "Config"]:
+                                continue
+                            line_num = content[:match.start()].count("\n") + 1
+                            models.append(CoverageItem(
+                                category="models",
+                                name=name,
+                                file_path=str(file.relative_to(codebase_dir)),
+                                line_number=line_num,
+                            ))
+                except Exception:
+                    continue
+
+    # Python models
+    if frameworks.get("fastapi") or frameworks.get("flask"):
+        model_dirs = ["models", "src/models", "app/models", "schemas", "src/schemas"]
+        for dir_name in model_dirs:
+            model_dir = codebase_dir / dir_name
+            if model_dir.exists():
+                for file in model_dir.rglob("*.py"):
+                    if "__pycache__" in str(file):
+                        continue
+                    try:
+                        content = file.read_text()
+                        for pattern, _ in PYTHON_MODEL_PATTERNS:
+                            for match in re.finditer(pattern, content):
+                                name = match.group(1)
+                                line_num = content[:match.start()].count("\n") + 1
+                                models.append(CoverageItem(
+                                    category="models",
+                                    name=name,
+                                    file_path=str(file.relative_to(codebase_dir)),
+                                    line_number=line_num,
+                                ))
+                    except Exception:
+                        continue
+
+    return models
+
+
+def extract_doc_references(docs_dir: Path) -> set[str]:
+    """Extract all codebase references from documentation.
+
+    Looks for patterns like:
+    - Route mentions: GET /api/users, POST /users
+    - Component mentions: PaymentForm, UserDashboard
+    - File path mentions: components/Button.tsx
+    - Code blocks with file references
+    """
+    references = set()
+
+    # Common English words to skip (not component names)
+    SKIP_WORDS = {
+        "The", "This", "That", "These", "Those", "There", "Here",
+        "What", "When", "Where", "Which", "Who", "Why", "How",
+        "And", "But", "For", "Not", "With", "You", "Your",
+        "All", "Any", "Can", "May", "Will", "Should", "Would", "Could",
+        "API", "URL", "HTTP", "HTML", "CSS", "JSON", "XML", "SQL",
+        "TODO", "FIXME", "NOTE", "WARNING", "ERROR", "INFO", "DEBUG",
+        "GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS",
+        "New", "Old", "See", "Use", "Run", "Set", "Add", "Try",
+        "Example", "Examples", "Documentation", "Overview", "Introduction",
+        "Setup", "Install", "Usage", "Configuration", "Settings",
+        "Returns", "Creates", "Updates", "Deletes", "Displays",
+    }
+
+    if not docs_dir.exists():
+        return references
+
+    for md_file in docs_dir.rglob("*.md"):
+        # Skip hidden dirs and chunks
+        parts = md_file.relative_to(docs_dir).parts
+        if any(p.startswith(".") for p in parts):
+            continue
+
+        try:
+            content = md_file.read_text()
+
+            # Find route mentions
+            route_patterns = [
+                r"(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s+(/[^\s\)`]+)",
+                r"`(GET|POST|PUT|PATCH|DELETE)\s+(/[^`]+)`",
+            ]
+            for pattern in route_patterns:
+                for match in re.finditer(pattern, content, re.IGNORECASE):
+                    method = match.group(1).upper()
+                    path = match.group(2).rstrip(")").rstrip("`")
+                    references.add(f"{method} {path}")
+
+            # Find page mentions in Next.js style
+            page_patterns = [
+                r"(?:PAGE|page|route)\s+[`'\"]?(/[^\s`'\"]+)[`'\"]?",
+            ]
+            for pattern in page_patterns:
+                for match in re.finditer(pattern, content):
+                    references.add(f"PAGE {match.group(1)}")
+
+            # Find JSX component references: <ComponentName> or <ComponentName />
+            jsx_pattern = r"<([A-Z][a-zA-Z0-9]+)(?:\s|/|>)"
+            for match in re.finditer(jsx_pattern, content):
+                name = match.group(1)
+                if name not in SKIP_WORDS:
+                    references.add(name)
+
+            # Find backtick component references: `ComponentName`
+            backtick_pattern = r"`([A-Z][a-zA-Z0-9]+)`"
+            for match in re.finditer(backtick_pattern, content):
+                name = match.group(1)
+                if name not in SKIP_WORDS:
+                    references.add(name)
+
+            # Find PascalCase compound names (2+ capital letters = likely component)
+            # e.g., UserProfile, PaymentForm, DataTable
+            pascal_pattern = r"\b([A-Z][a-z]+[A-Z][a-zA-Z0-9]*)\b"
+            for match in re.finditer(pascal_pattern, content):
+                name = match.group(1)
+                if name not in SKIP_WORDS:
+                    references.add(name)
+
+            # Find explicit component mentions: "the UserProfile component"
+            explicit_pattern = r"(?:the\s+)?([A-Z][a-zA-Z0-9]+)\s+(?:component|widget|form|button|modal|dialog)"
+            for match in re.finditer(explicit_pattern, content, re.IGNORECASE):
+                name = match.group(1)
+                if name not in SKIP_WORDS:
+                    references.add(name)
+
+            # Find section headers that name components: "## Dashboard" or "### UserProfile"
+            header_pattern = r"^#{1,6}\s+([A-Z][a-zA-Z0-9]+)(?:\s+Component)?(?:\s|$)"
+            for match in re.finditer(header_pattern, content, re.MULTILINE):
+                name = match.group(1)
+                if name not in SKIP_WORDS:
+                    references.add(name)
+
+            # Find model/type mentions
+            model_patterns = [
+                r"(?:model|type|interface|schema|entity)\s+[`'\"]?([A-Z][a-zA-Z0-9]+)[`'\"]?",
+            ]
+            for pattern in model_patterns:
+                for match in re.finditer(pattern, content, re.IGNORECASE):
+                    name = match.group(1)
+                    if name not in SKIP_WORDS:
+                        references.add(name)
+
+            # Find file path mentions
+            file_pattern = r"(?:^|\s|`)[a-zA-Z0-9_\-/]+\.(tsx?|jsx?|vue|svelte|py|php)(?:\s|`|$)"
+            for match in re.finditer(file_pattern, content):
+                # Extract just the filename for matching
+                file_path = match.group(0).strip().strip("`")
+                references.add(file_path)
+
+        except Exception:
+            continue
+
+    return references
+
+
+def match_documentation(
+    items: list[CoverageItem],
+    doc_references: set[str],
+) -> list[CoverageItem]:
+    """Match codebase items against documentation references."""
+    for item in items:
+        # Normalize item name for matching
+        item_name = item.name
+
+        # Check direct match
+        if item_name in doc_references:
+            item.documented = True
+            continue
+
+        # Check partial match for routes (ignore dynamic segments)
+        if item.category == "routes":
+            # Normalize route for comparison
+            normalized = re.sub(r"\[[^\]]+\]", "*", item_name)  # [id] -> *
+            normalized = re.sub(r":\w+", "*", normalized)  # :id -> *
+
+            for ref in doc_references:
+                ref_normalized = re.sub(r"\[[^\]]+\]", "*", ref)
+                ref_normalized = re.sub(r":\w+", "*", ref_normalized)
+                ref_normalized = re.sub(r"\{[^}]+\}", "*", ref_normalized)  # {id} -> *
+
+                if normalized == ref_normalized:
+                    item.documented = True
+                    break
+
+        # Check component name match (case-insensitive partial)
+        if item.category == "components":
+            name_lower = item.name.lower()
+            for ref in doc_references:
+                if ref.lower() == name_lower or name_lower in ref.lower():
+                    item.documented = True
+                    break
+
+        # Check model name match
+        if item.category == "models":
+            for ref in doc_references:
+                if item.name.lower() == ref.lower():
+                    item.documented = True
+                    break
+
+    return items
+
+
+def analyze_coverage(
+    codebase_dir: Path,
+    docs_dir: Path,
+    on_status: Optional[Callable[[str], None]] = None,
+) -> CoverageReport:
+    """Analyze documentation coverage for a codebase.
+
+    Args:
+        codebase_dir: Path to the codebase root
+        docs_dir: Path to the documentation directory
+        on_status: Optional callback for status messages
+
+    Returns:
+        CoverageReport with detailed coverage analysis
+    """
+    report = CoverageReport(
+        codebase_dir=str(codebase_dir),
+        docs_dir=str(docs_dir),
+    )
+
+    def status(msg: str) -> None:
+        if on_status:
+            on_status(msg)
+
+    # Detect frameworks
+    status("Detecting frameworks...")
+    frameworks = detect_framework(codebase_dir)
+    detected = [name for name, found in frameworks.items() if found]
+    if detected:
+        status(f"Detected: {', '.join(detected)}")
+    else:
+        status("No specific frameworks detected, using generic analysis")
+
+    # Extract documentation references
+    status("Scanning documentation...")
+    doc_references = extract_doc_references(docs_dir)
+    status(f"Found {len(doc_references)} documentation references")
+
+    # Find routes
+    status("Scanning for routes/endpoints...")
+    routes = find_routes(codebase_dir, frameworks)
+    routes = match_documentation(routes, doc_references)
+
+    if routes:
+        routes_cat = CoverageCategory(name="Routes", items=routes)
+        routes_cat.total = len(routes)
+        routes_cat.documented = sum(1 for r in routes if r.documented)
+        report.categories["routes"] = routes_cat
+        status(f"Found {len(routes)} routes")
+
+    # Find components
+    status("Scanning for components...")
+    components = find_components(codebase_dir, frameworks)
+    components = match_documentation(components, doc_references)
+
+    if components:
+        comp_cat = CoverageCategory(name="Components", items=components)
+        comp_cat.total = len(components)
+        comp_cat.documented = sum(1 for c in components if c.documented)
+        report.categories["components"] = comp_cat
+        status(f"Found {len(components)} components")
+
+    # Find models
+    status("Scanning for models/types...")
+    models = find_models(codebase_dir, frameworks)
+    models = match_documentation(models, doc_references)
+
+    if models:
+        models_cat = CoverageCategory(name="Models", items=models)
+        models_cat.total = len(models)
+        models_cat.documented = sum(1 for m in models if m.documented)
+        report.categories["models"] = models_cat
+        status(f"Found {len(models)} models/types")
+
+    return report
+
+
+def save_coverage_report(report: CoverageReport, docs_dir: Path) -> Path:
+    """Save coverage report to .chunks/coverage.json."""
+    chunks_dir = docs_dir / ".chunks"
+    chunks_dir.mkdir(parents=True, exist_ok=True)
+
+    report_path = chunks_dir / "coverage.json"
+    report_path.write_text(
+        json.dumps(report.to_dict(), indent=2),
+        encoding="utf-8",
+    )
+
+    return report_path
+
+
+def load_coverage_report(docs_dir: Path) -> Optional[dict]:
+    """Load previous coverage report if it exists.
+
+    Returns the raw dict from the JSON file. Use this for comparing
+    coverage between runs or displaying historical data.
+    """
+    report_path = docs_dir / ".chunks" / "coverage.json"
+
+    if not report_path.exists():
+        return None
+
+    try:
+        return json.loads(report_path.read_text(encoding="utf-8"))
+    except Exception:
+        return None
diff --git a/uv.lock b/uv.lock
index 7a805be..9767f3e 100644
--- a/uv.lock
+++ b/uv.lock
@@ -4,16 +4,18 @@ requires-python = ">=3.11"
 
 [[package]]
 name = "aidocs"
-version = "0.15.5"
+version = "0.18.0"
 source = { editable = "." }
 dependencies = [
     { name = "httpx" },
     { name = "mcp" },
     { name = "mkdocs" },
     { name = "mkdocs-material" },
+    { name = "python-dotenv" },
     { name = "pyyaml" },
     { name = "rich" },
     { name = "typer" },
+    { name = "watchdog" },
 ]
 
 [package.metadata]
@@ -22,9 +24,11 @@ requires-dist = [
     { name = "mcp", specifier = ">=1.0.0" },
     { name = "mkdocs", specifier = ">=1.6.0" },
     { name = "mkdocs-material", specifier = ">=9.5.0" },
+    { name = "python-dotenv", specifier = ">=1.0.0" },
     { name = "pyyaml", specifier = ">=6.0" },
     { name = "rich", specifier = ">=13.0.0" },
     { name = "typer", specifier = ">=0.9.0" },
+    { name = "watchdog", specifier = ">=4.0.0" },
 ]
 
 [[package]]
