105 lines
3.9 KiB
Python
105 lines
3.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Atomically convert checker-confirmed bare branch decision/owner refs to wikilinks."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import importlib.util
|
|
import json
|
|
from pathlib import Path
|
|
import re
|
|
import sys
|
|
from typing import Any
|
|
|
|
from fs_transaction import replace_many
|
|
|
|
|
|
DEFAULT_ROOT = Path(__file__).resolve().parents[2]
|
|
|
|
|
|
def _load_checker(root: Path):
|
|
path = root / ".claude/hooks/wiki_consistency_check.py"
|
|
hooks = str(path.parent)
|
|
if hooks not in sys.path:
|
|
sys.path.insert(0, hooks)
|
|
spec = importlib.util.spec_from_file_location("wiki_consistency_fix_source", path)
|
|
if spec is None or spec.loader is None:
|
|
raise OSError(f"cannot load checker: {path}")
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
def _replace_slug(line: str, slug: str, branch_dir: str) -> tuple[str, bool]:
|
|
pattern = re.compile(
|
|
rf"(?<![\/\w-])`?{re.escape(slug)}`?(?![\w-])"
|
|
)
|
|
rendered, count = pattern.subn(f"[[{branch_dir}/{slug}]]", line, count=1)
|
|
return rendered, bool(count)
|
|
|
|
|
|
def build_updates(root: Path) -> tuple[dict[Path, str], dict[str, Any]]:
|
|
checker = _load_checker(root)
|
|
updates: dict[Path, str] = {}
|
|
decision_fixes = 0
|
|
owner_fixes = 0
|
|
for branch_path in sorted((root / checker.BRANCH_DIR).glob("*.md")):
|
|
text = branch_path.read_text(encoding="utf-8")
|
|
lines = text.splitlines(keepends=True)
|
|
for line_number, slug, _ref_id, kind in checker.extract_refs(text, branch_path.stem):
|
|
if kind != "bare":
|
|
continue
|
|
original = lines[line_number - 1]
|
|
lines[line_number - 1], changed = _replace_slug(original, slug, checker.BRANCH_DIR)
|
|
if changed:
|
|
decision_fixes += 1
|
|
current = "".join(lines)
|
|
coverage = checker.coverage_rows(current)
|
|
for line_number, _concern, status, owner in coverage:
|
|
if "delegated" not in status or not owner:
|
|
continue
|
|
slugs = [match.group(1) for match in checker.BARE_SLUG_RE.finditer(owner)]
|
|
for slug in slugs:
|
|
original = lines[line_number - 1]
|
|
lines[line_number - 1], changed = _replace_slug(original, slug, checker.BRANCH_DIR)
|
|
if changed:
|
|
owner_fixes += 1
|
|
rendered = "".join(lines)
|
|
if rendered != text:
|
|
updates[branch_path] = rendered
|
|
return updates, {
|
|
"decision_ref_fixes": decision_fixes,
|
|
"owner_ref_fixes": owner_fixes,
|
|
"changed_files": [path.relative_to(root).as_posix() for path in sorted(updates)],
|
|
}
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
mode = parser.add_mutually_exclusive_group(required=True)
|
|
mode.add_argument("--check", action="store_true")
|
|
mode.add_argument("--write", action="store_true")
|
|
parser.add_argument("--root", type=Path, default=DEFAULT_ROOT)
|
|
args = parser.parse_args(argv)
|
|
try:
|
|
root = args.root.resolve(strict=True)
|
|
updates, counts = build_updates(root)
|
|
if args.write and updates:
|
|
replace_many({path: text.encode("utf-8") for path, text in updates.items()})
|
|
result = {
|
|
"schema_version": "bare-ref-migration-result/v1",
|
|
"status": "DRIFT" if args.check and updates else "UPDATED" if updates else "CURRENT",
|
|
**counts,
|
|
}
|
|
json.dump(result, sys.stdout, ensure_ascii=False, indent=2, sort_keys=True)
|
|
sys.stdout.write("\n")
|
|
return 1 if args.check and updates else 0
|
|
except (OSError, UnicodeError, ValueError) as exc:
|
|
json.dump({"schema_version": "bare-ref-migration-result/v1", "status": "FAIL", "error": str(exc)}, sys.stdout, ensure_ascii=False, indent=2)
|
|
sys.stdout.write("\n")
|
|
return 2
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|