105 lines
3.4 KiB
Python
105 lines
3.4 KiB
Python
#!/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())
|