"""함수 → 로직 묶음 자동 배정(PLAN 478-2). 규칙 = docs/tools/groups.yml · 위에서부터 첫 맞음: registry — 계산 등록부 id 무늬 → 그 줄의 ts · pair 함수 files — 파일 이름(확장자 뺀) 무늬 어디에도 안 맞는 함수 = 「묶음 없음」(None). """ from __future__ import annotations import hashlib from fnmatch import fnmatchcase from pathlib import Path, PurePosixPath import yaml TOOLS = Path(__file__).resolve().parent NONE = "none" def load_rules(path: Path = TOOLS / "groups.yml") -> dict: return yaml.safe_load(path.read_text(encoding="utf-8")) def stem(file: str) -> str: return PurePosixPath(file.replace("\\", "/")).stem def registry_ref_id(ref: str | None) -> str | None: """등록부 칸 `<경로>#<함수>` → 함수 id.""" if not ref or "#" not in ref: return None lang = "py" if ref.split("#", 1)[0].endswith(".py") else "ts" return f"{lang}:{ref}" def registry_owners(registry: list[dict], groups: list[dict]) -> dict[str, str]: """등록부로 정해지는 함수 id → 묶음 id.""" owners: dict[str, str] = {} for row in registry: rid = row.get("id", "") group = next( (g["id"] for g in groups if any(fnmatchcase(rid, p) for p in g.get("registry", []))), None, ) if group is None: continue for key in ("ts", "pair"): fid = registry_ref_id(row.get(key)) if fid: owners.setdefault(fid, group) return owners def file_group(file: str, groups: list[dict]) -> str | None: name = stem(file) for g in groups: if any(fnmatchcase(name, p) for p in g.get("files", [])): return g["id"] return None def assign( functions: list[dict], registry: list[dict], groups: list[dict] ) -> dict[str, list[dict]]: """묶음 id → 함수 목록(파일 · 줄 차례) · 「묶음 없음」 = NONE.""" owners = registry_owners(registry, groups) out: dict[str, list[dict]] = {g["id"]: [] for g in groups} out[NONE] = [] for fn in functions: gid = owners.get(fn["id"]) or file_group(fn["file"], groups) or NONE out[gid].append(fn) for fns in out.values(): fns.sort(key=lambda f: (f["file"], f.get("line", 0))) return out def fingerprint(fns: list[dict]) -> str: """함수 목록 지문 — 손글에 적힌 지문과 다르면 「설명 확인 필요」.""" text = "\n".join(sorted(f["id"] for f in fns)) return hashlib.sha1(text.encode("utf-8")).hexdigest()[:10]