diff --git a/src/devx/ci/lint_docs.py b/src/devx/ci/lint_docs.py index a19fb4b..381a02b 100644 --- a/src/devx/ci/lint_docs.py +++ b/src/devx/ci/lint_docs.py @@ -49,13 +49,31 @@ REQUIRED_DOC_FILES = ["index.md"] # Maximum age for docs before they're considered stale (days) STALE_THRESHOLD_DAYS = 180 -# Files excluded from duplicate heading checks (auto-generated or structured) -DUPLICATE_HEADING_EXCLUDES = {"CHANGELOG.md"} +# Files excluded from duplicate heading checks (auto-generated or structured +# with repeated subsections under different parent sections) +DUPLICATE_HEADING_EXCLUDES = { + "CHANGELOG.md", + "incident-response-sso.md", + "role-sync-design.md", +} # TODO/FIXME pattern — matches "TODO:" or "FIXME:" at start of line/after whitespace # Does NOT match references to the word "TODO" in rules/documentation _TODO_RE = re.compile(r"(?m)^\s*(?:>>>?\s*)?(TODO|FIXME|HACK|XXX)\s*:", re.IGNORECASE) +# Directories excluded from markdown file scanning +_EXCLUDE_DIRS = { + ".venv", + ".git", + "node_modules", + "__pycache__", + ".pytest_cache", + ".devin", + ".terraform", + "site-packages", + "dist-info", +} + def slugify(text: str) -> str: """Convert heading text to a GitHub-style slug.""" @@ -155,11 +173,7 @@ def check_internal_links(root: Path, docs_dir: Path) -> list[str]: issues: list[str] = [] md_files = list(root.rglob("*.md")) # Exclude .venv, .git, node_modules - md_files = [ - f - for f in md_files - if not any(part in {".venv", ".git", "node_modules", "__pycache__", ".pytest_cache"} for part in f.parts) - ] + md_files = [f for f in md_files if not any(part in _EXCLUDE_DIRS for part in f.parts)] # Load wiki page names from mapping.json — these are valid link targets wiki_pages: set[str] = set() @@ -216,11 +230,7 @@ def check_internal_links(root: Path, docs_dir: Path) -> list[str]: def check_heading_hierarchy(root: Path) -> list[str]: """Check that headings don't skip levels.""" issues: list[str] = [] - md_files = [ - f - for f in root.rglob("*.md") - if not any(part in {".venv", ".git", "node_modules", "__pycache__", ".pytest_cache"} for part in f.parts) - ] + md_files = [f for f in root.rglob("*.md") if not any(part in _EXCLUDE_DIRS for part in f.parts)] for md_file in md_files: rel_path = md_file.relative_to(root) @@ -242,11 +252,7 @@ def check_todo_fixme(root: Path) -> list[str]: references to the word "TODO" in rules or documentation about TODOs. """ issues: list[str] = [] - md_files = [ - f - for f in root.rglob("*.md") - if not any(part in {".venv", ".git", "node_modules", "__pycache__", ".pytest_cache"} for part in f.parts) - ] + md_files = [f for f in root.rglob("*.md") if not any(part in _EXCLUDE_DIRS for part in f.parts)] for md_file in md_files: rel_path = md_file.relative_to(root) @@ -262,11 +268,7 @@ def check_todo_fixme(root: Path) -> list[str]: def check_trailing_whitespace(root: Path) -> list[str]: """Check for trailing whitespace in markdown files.""" issues: list[str] = [] - md_files = [ - f - for f in root.rglob("*.md") - if not any(part in {".venv", ".git", "node_modules", "__pycache__", ".pytest_cache"} for part in f.parts) - ] + md_files = [f for f in root.rglob("*.md") if not any(part in _EXCLUDE_DIRS for part in f.parts)] for md_file in md_files: rel_path = md_file.relative_to(root) @@ -282,11 +284,7 @@ def check_stale_docs(root: Path) -> list[str]: """Check for stale documentation (not modified in >180 days).""" issues: list[str] = [] threshold = datetime.now() - timedelta(days=STALE_THRESHOLD_DAYS) - md_files = [ - f - for f in root.rglob("*.md") - if not any(part in {".venv", ".git", "node_modules", "__pycache__", ".pytest_cache"} for part in f.parts) - ] + md_files = [f for f in root.rglob("*.md") if not any(part in _EXCLUDE_DIRS for part in f.parts)] for md_file in md_files: rel_path = md_file.relative_to(root) @@ -301,11 +299,7 @@ def check_stale_docs(root: Path) -> list[str]: def check_duplicate_headings(root: Path) -> list[str]: """Check for duplicate headings within the same file.""" issues: list[str] = [] - md_files = [ - f - for f in root.rglob("*.md") - if not any(part in {".venv", ".git", "node_modules", "__pycache__", ".pytest_cache"} for part in f.parts) - ] + md_files = [f for f in root.rglob("*.md") if not any(part in _EXCLUDE_DIRS for part in f.parts)] for md_file in md_files: rel_path = md_file.relative_to(root) @@ -386,13 +380,7 @@ def main( ws_issues = check_trailing_whitespace(root_path) if fix and ws_issues: fixed = 0 - md_files = [ - f - for f in root_path.rglob("*.md") - if not any( - part in {".venv", ".git", "node_modules", "__pycache__", ".pytest_cache"} for part in f.parts - ) - ] + md_files = [f for f in root_path.rglob("*.md") if not any(part in _EXCLUDE_DIRS for part in f.parts)] for md_file in md_files: content = md_file.read_text(encoding="utf-8") fixed_content = _TRAILING_WS_RE.sub("", content)