"""Mintlify docs integrity checker — run before merging any docs PR.""" from __future__ import annotations import glob import json import os import re import sys from typing import Any, Callable, cast try: from rich.console import Console as _Console _console = _Console() def _print_pass(label: str) -> None: _console.print(f"[bold green]pass[/bold green] {label}") def _print_fail(label: str, msgs: list[str]) -> None: _console.print(f"[bold red]FAIL[/bold red] {label}") for m in msgs: _console.print(f"[dim] - {m}[/dim]") def _print_summary(failures: list[str]) -> None: _console.print() if failures: _console.print(f"[bold red]FAILED[/bold red] {len(failures)} issue(s) found") else: _console.print("[bold green]All checks passed[/bold green]") except ModuleNotFoundError: def _print_pass(label: str) -> None: # type: ignore[misc] print(f"pass {label}") def _print_fail(label: str, msgs: list[str]) -> None: # type: ignore[misc] print(f"FAIL {label}") for m in msgs: print(f" - {m}") def _print_summary(failures: list[str]) -> None: # type: ignore[misc] print() if failures: print(f"FAILED {len(failures)} issue(s) found") else: print("All checks passed") DOCS = "docs" ALL_MD: list[str] = glob.glob(f"{DOCS}/**/*.md", recursive=True) failures: list[str] = [] def check(label: str) -> Callable[[Callable[[], list[str]]], None]: """Decorator: run a check function, print result, collect failures.""" def decorator(fn: Callable[[], list[str]]) -> None: issues = fn() if issues: _print_fail(label, issues) failures.extend(issues) else: _print_pass(label) return decorator def read(path: str) -> str: return open(path, encoding="utf-8").read() # ── 1. docs.json is valid JSON ──────────────────────────────────────────────── @check("docs.json is valid JSON") def _() -> list[str]: try: json.loads(read(f"{DOCS}/docs.json")) return [] except Exception as e: return [str(e)] # ── 2. Every nav page exists on disk ───────────────────────────────────────── @check("All nav pages exist on disk") def _() -> list[str]: cfg: Any = json.loads(read(f"{DOCS}/docs.json")) def nav_pages(obj: Any) -> list[str]: """Recursively collect strings from 'pages' arrays only.""" result: list[str] = [] if isinstance(obj, dict): d = cast(dict[str, Any], obj) for page in cast(list[Any], d.get("pages", [])): if isinstance(page, str): result.append(page) else: result.extend(nav_pages(page)) for v in d.values(): if isinstance(v, (dict, list)): result.extend(nav_pages(v)) elif isinstance(obj, list): for item in cast(list[Any], obj): result.extend(nav_pages(item)) return result return [ f"missing: {p}" for p in set(nav_pages(cfg)) if not p.startswith("http") and not os.path.exists(f"{DOCS}/{p}.md") ] # ── 3. Internal Card hrefs resolve ─────────────────────────────────────────── @check("All internal Card hrefs resolve") def _() -> list[str]: issues: list[str] = [] for fpath in ALL_MD: for m in re.finditer(r'href=["\'](?!http)([^"\'#]+)["\']', read(fpath)): href = m.group(1).strip() target = os.path.normpath(os.path.join(os.path.dirname(fpath), href)) + ".md" if not os.path.exists(target): issues.append(f"{fpath}: href '{href}'") return issues # ── 4. No stale repo URLs ───────────────────────────────────────────────────── @check("No stale repo URLs") def _() -> list[str]: stale = ["Hawksight-AI/semantica", "semantica-dev/semantica"] files = ALL_MD + [f"{DOCS}/docs.json"] return [ f"{f}: '{pat}'" for f in files for pat in stale if pat in read(f) ] # ── 5. All reference pages have frontmatter ─────────────────────────────────── @check("All reference pages have frontmatter") def _() -> list[str]: return [ os.path.basename(f) for f in glob.glob(f"{DOCS}/reference/*.md") if not read(f).startswith("---") ] # ── 6. No known-wrong class names ──────────────────────────────────────────── @check("No known-wrong class names in reference pages") def _() -> list[str]: banned: list[tuple[str, str]] = [ (r"\bBaseIngestor\b", "docs/architecture.md"), (r"\bBaseExtractor\b", "docs/architecture.md"), (r"\bBasePlugin\b", "docs/architecture.md"), (r"\bDataNormalizer\b", "docs/reference/normalize.md"), (r"\bEntityResolver\b", "docs/reference/deduplication.md"), (r"\bDeductiveEngine\b", "docs/reference/reasoning.md"), (r"\bAbductiveEngine\b", "docs/reference/reasoning.md"), (r"\bGraphMLExporter\b", "docs/reference/export.md"), (r"\bArangoExporter\b(?!.*AQL)", "docs/reference/export.md"), (r"(? list[str]: issues: list[str] = [] for fpath in ALL_MD: in_block = False for i, line in enumerate(open(fpath, encoding="utf-8"), 1): if line.strip().startswith("```"): in_block = not in_block if in_block and re.search(r":\s*(list|dict|tuple|set)\[", line): issues.append(f"{fpath}:{i}: {line.rstrip()}") return issues # ── 8. index.md covers all 27 modules ──────────────────────────────────────── @check("All 27 modules present in index.md") def _() -> list[str]: modules = [ "semantica.ingest", "semantica.parse", "semantica.split", "semantica.normalize", "semantica.semantic_extract", "semantica.kg", "semantica.ontology", "semantica.reasoning", "semantica.embeddings", "semantica.vector_store", "semantica.graph_store", "semantica.triplet_store", "semantica.context", "semantica.provenance", "semantica.change_management", "semantica.deduplication", "semantica.conflicts", "semantica.export", "semantica.visualization", "semantica.pipeline", "semantica.seed", "semantica.llms", "semantica.mcp_server", "semantica.explorer", "semantica.evals", "semantica.utils", "semantica.core", ] index = read(f"{DOCS}/index.md") return [m for m in modules if m not in index] # ── Summary ─────────────────────────────────────────────────────────────────── _print_summary(failures) if failures: sys.exit(1)