#!/usr/bin/env python3 """Efficiently lint active project and branch documents with one shared index.""" from __future__ import annotations import argparse import importlib.util import json from pathlib import Path import sys from typing import Any DEFAULT_ROOT = Path(__file__).resolve().parents[2] RESULT_SCHEMA = "active-structure-result/v1" class ActiveStructureError(RuntimeError): pass def _module(path: Path) -> Any: spec = importlib.util.spec_from_file_location("wiki_structure_lint_active", path) if spec is None or spec.loader is None: raise ActiveStructureError(f"cannot load structure lint: {path}") module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module def check(root: Path) -> dict[str, Any]: root = root.resolve(strict=True) lint = _module(root / ".claude/hooks/wiki_structure_lint.py") authority = lint.authority_mapping(root) by_st, by_file = lint.build_template_index(root) vault_paths, vault_bases = lint.build_vault_index(root) cache: dict[Path, str] = {} if authority["mode"] == "canonical": paths = sorted( root / canonical for legacy, canonical in authority["legacy_to_canonical"].items() if ( legacy.startswith("raw/branch-notes/") or legacy.startswith("raw/project-notes/") ) and legacy.endswith(".md") ) else: paths = sorted((root / "raw/branch-notes").glob("*.md")) + sorted( (root / "raw/project-notes").glob("*.md") ) paths = [path for path in paths if path.is_file()] if not paths: raise ActiveStructureError("no active project/branch documents found") findings: list[dict[str, Any]] = [] for path in paths: relative = path.relative_to(root).as_posix() mode = lint.classify(relative, root) lint_findings, _source_type = lint.lint_file( path, root, by_st, by_file, vault_paths, vault_bases, cache, mode=mode, ) findings.extend( {"code": code, "path": relative, "line": line, "message": message} for code, line, message in lint_findings ) findings.sort(key=lambda item: (item["path"], item["line"], item["code"])) return { "schema_version": RESULT_SCHEMA, "status": "PASS" if not findings else "FAIL", "mode": authority["mode"], "namespace": "canonical" if authority["mode"] == "canonical" else "legacy", "checked": len(paths), "findings": findings, } def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--root", type=Path, default=DEFAULT_ROOT) args = parser.parse_args(argv) try: result = check(args.root) exit_code = 0 if result["status"] == "PASS" else 1 except (ActiveStructureError, OSError, UnicodeError, ValueError, ImportError) as exc: result = { "schema_version": RESULT_SCHEMA, "status": "ERROR", "errors": [{"code": "ACTIVE_STRUCTURE_ERROR", "message": str(exc)}], } exit_code = 2 json.dump(result, sys.stdout, ensure_ascii=False, indent=2, sort_keys=True) sys.stdout.write("\n") return exit_code if __name__ == "__main__": raise SystemExit(main())