Files
llm-wiki/harness/runtime/fix_bare_refs.py
T

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())