#!/usr/bin/env python3 """Verify the public manual boundary, including generated pages and search.""" import argparse import json from pathlib import Path from urllib.parse import unquote, urlsplit import yaml PUBLIC_ASSETS = { "stylesheets/extra.css", "javascripts/layout-init.js", "install/examples/thothii-installation.local.yaml", "install/examples/thothii-installation.server.yaml", "install/examples/workspace-bindings.env.example", } INTERNAL_DIRS = { "adr", "agents", "architecture", "contracts", "maintenance", "plans", "reports", "research", "testing", } INTERNAL_PAGES = { "disambiguazione-iniziale.md", "gestione-memory.md", "installazione-docker-4-contesti.md", "install/authentication-upstream.md", "operations/docker-refresh.md", "operations/shell-and-localization.md", "operations/compose-reference.md", "operations/public-docs-publication.md", } def nav_pages(value): if isinstance(value, str): yield value elif isinstance(value, list): for item in value: yield from nav_pages(item) elif isinstance(value, dict): for item in value.values(): yield from nav_pages(item) def output_path(source): path = Path(source) if path.suffix != ".md": return path return path.with_suffix("") / "index.html" if path.name != "index.md" else path.with_suffix(".html") def check(root, site): # BaseLoader reads configuration without executing custom Python YAML tags. config = yaml.load((root / "mkdocs.yml").read_text(), Loader=yaml.BaseLoader) pages = list(nav_pages(config["nav"])) if len(pages) != len(set(pages)): raise ValueError("duplicate public navigation page") for page in pages: path = Path(page) if path.is_absolute() or ".." in path.parts or path.suffix != ".md": raise ValueError(f"invalid public page: {page}") if (path.parts[0] in INTERNAL_DIRS or page in INTERNAL_PAGES or page.startswith("operations/server-")): raise ValueError(f"internal page in public navigation: {page}") if not (root / "docs" / page).is_file(): raise ValueError(f"missing public source: {page}") rules = config.get("exclude_docs", "").splitlines() if not rules or rules[0] != "*" or any(not r.startswith("!/") for r in rules[1:]): raise ValueError("public docs must use deny-by-default exclusions and explicit exceptions") published = [r[2:] for r in rules[1:]] if len(published) != len(set(published)) or set(published) != set(pages) | PUBLIC_ASSETS: raise ValueError("publication exceptions must match nav pages and approved assets exactly") for source in published: if not (root / "docs" / source).is_file(): raise ValueError(f"missing public source: {source}") if not (site / output_path(source)).is_file(): raise ValueError(f"missing generated public file: {source}") for source in (root / "docs").rglob("*"): if not source.is_file(): continue relative = source.relative_to(root / "docs").as_posix() if relative not in published and (site / output_path(relative)).exists(): raise ValueError(f"internal file leaked into site: {relative}") expected_urls = { "" if page == "index.md" else str(output_path(page).parent).replace("\\", "/") + "/" for page in pages } search = json.loads((site / "search/search_index.json").read_text()) indexed = set() for entry in search["docs"]: location = unquote(urlsplit(entry["location"]).path) if location not in expected_urls: raise ValueError(f"non-public page in search index: {location}") indexed.add(location) if indexed != expected_urls: raise ValueError("search index does not cover exactly the public pages") return len(pages) if __name__ == "__main__": parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--root", type=Path, default=Path(__file__).resolve().parents[1]) parser.add_argument("--site-dir", type=Path) args = parser.parse_args() root = args.root.resolve() try: count = check(root, args.site_dir or root / "site") except (ValueError, KeyError, OSError, yaml.YAMLError) as error: raise SystemExit(f"public docs verification failed: {error}") from error print(f"Public documentation boundary passed: {count} pages; internal files and search excluded")