#!/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"(? 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())