#!/usr/bin/env python3 # hooks/sync_doc_associations.py # # 当 md 文件变更时,自动在关联文档的标准位置追加变更提醒: # - 有 ## 变更记录 表格的模块文档:追加一行变更记录 # - 有 ## 下一步 清单的 progress/tasks 文档:追加一个待检查项 # - 其他文档:追加 ## 最近关联变更 章节 # # 本脚本只追加、不覆盖正文;执行后请通过 git diff 人工确认。 import os import re import subprocess import sys from datetime import datetime from pathlib import Path PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent EXCLUDED_PREFIXES = ( '.claude/', '.codex/', '.git/', 'Library/', 'Temp/', 'Logs/', 'UserSettings/', 'obj/', 'bin/', 'HybridCLRData/', 'Assets/ThirdPlugins/', 'Assets/.WX-WASM-SDK-V2/', 'Packages/', ) def run_git(args): result = subprocess.run( ["git"] + args, cwd=PROJECT_ROOT, capture_output=True, text=True, encoding="utf-8", ) return result.stdout.strip(), result.returncode def get_changed_md_files(): """获取 tracked 改动 + untracked 新 md 文件。""" changed, code = run_git(["diff", "--name-only"]) if code != 0 or not changed: changed = "" untracked, code = run_git(["ls-files", "--others", "--exclude-standard"]) if code == 0 and untracked: changed = (changed + "\n" + untracked).strip() if not changed: return [] files = [line.strip() for line in changed.splitlines() if line.strip().endswith(".md")] files = [f for f in files if not f.startswith(EXCLUDED_PREFIXES)] return sorted(set(files)) def resolve_link(source_file: Path, link: str): """解析相对链接为项目相对路径。""" if re.match(r"^[a-zA-Z][a-zA-Z0-9+.-]*://", link): return None if link.startswith("#"): return None link = link.split("#")[0] if not link: return None target = (source_file.parent / link).resolve() try: target.relative_to(PROJECT_ROOT) except ValueError: return None rel = target.relative_to(PROJECT_ROOT) return str(rel).replace("\\", "/") def extract_associations(md_path: Path): """读取 md 的 ## 文档关联 章节,返回关联文件相对路径列表。""" content = md_path.read_text(encoding="utf-8") in_section = False targets = [] for line in content.splitlines(): if re.match(r"^##\s+文档关联\s*$", line): in_section = True continue if in_section and re.match(r"^##\s+", line): break if in_section: for _, link in re.findall(r"\[([^\]]*)\]\(([^\)]+)\)", line): resolved = resolve_link(md_path, link) if resolved: targets.append(resolved) return targets def find_section_range(lines, title_re): """返回 [start, end) 行号,end 为下一个 ## 标题或文件末尾。""" start = None for i, line in enumerate(lines): if re.match(title_re, line): start = i continue if start is not None and re.match(r"^##\s+", line): return start, i if start is not None: return start, len(lines) return None, None def append_changelog(target_path: Path, source_rel: str, date_str: str) -> bool: """在 ## 变更记录 表格追加一行;成功返回 True。""" content = target_path.read_text(encoding="utf-8") lines = content.splitlines() start, end = find_section_range(lines, r"^##\s+变更记录\s*$") if start is None: return False # 找到表格最后一行(跳过标题和分隔线) insert_pos = end for i in range(start + 1, end): if lines[i].strip().startswith("|") and not re.match(r"^\|[-:\s|]+\|$", lines[i].strip()): insert_pos = i + 1 new_line = f"| {date_str} | [{source_rel}](../../{source_rel}) 发生变更 | 待补充影响范围 | 待确认 |" lines.insert(insert_pos, new_line) target_path.write_text("\n".join(lines) + ("\n" if content.endswith("\n") else ""), encoding="utf-8") return True def append_next_step(target_path: Path, source_rel: str) -> bool: """在 ## 下一步 清单追加一个待检查项;成功返回 True。""" content = target_path.read_text(encoding="utf-8") lines = content.splitlines() start, end = find_section_range(lines, r"^##\s+下一步\s*$") if start is None: return False new_line = f"- [ ] 检查 [{source_rel}](../../{source_rel}) 的变更是否影响本文档" # 避免重复 for line in lines[start:end]: if source_rel in line and "变更是否影响本文档" in line: return True # 已存在,视为成功但不写入 lines.insert(end, new_line) target_path.write_text("\n".join(lines) + ("\n" if content.endswith("\n") else ""), encoding="utf-8") return True def append_recent_changes_section(target_path: Path, source_rel: str, date_str: str) -> bool: """在没有 变更记录/下一步 的文档末尾追加 ## 最近关联变更。""" content = target_path.read_text(encoding="utf-8") # 生成从 target 到 source 的相对链接 try: link = os.path.relpath(PROJECT_ROOT / source_rel, target_path.parent).replace("\\", "/") except ValueError: link = source_rel if "## 最近关联变更" in content: lines = content.splitlines() start, end = find_section_range(lines, r"^##\s+最近关联变更\s*$") if start is not None: new_line = f"- {date_str}:关联文档 [{source_rel}]({link}) 发生变更,需同步检查。" lines.insert(end, new_line) target_path.write_text("\n".join(lines) + ("\n" if content.endswith("\n") else ""), encoding="utf-8") return True section = ( "\n---\n\n" "## 最近关联变更\n\n" f"- {date_str}:关联文档 [{source_rel}]({link}) 发生变更,需同步检查。\n" ) target_path.write_text(content.rstrip() + section, encoding="utf-8") return True def sync_associations(): changed_files = get_changed_md_files() if not changed_files: print("[sync-doc-assoc] No .md changes detected.") return 0 date_str = datetime.now().strftime("%Y-%m-%d") modified = [] for source_rel in changed_files: source_path = PROJECT_ROOT / source_rel if not source_path.exists(): continue targets = extract_associations(source_path) for target_rel in targets: target_path = PROJECT_ROOT / target_rel if not target_path.is_file(): # Skip directories, globs, or missing targets continue if target_rel == source_rel: continue done = ( append_changelog(target_path, source_rel, date_str) or append_next_step(target_path, source_rel) or append_recent_changes_section(target_path, source_rel, date_str) ) if done: modified.append((source_rel, target_rel)) if not modified: print("[sync-doc-assoc] No associated docs need updating.") return 0 print("[sync-doc-assoc] Updated associated docs:") for source, target in modified: print(f" - {source} -> {target}") print("\n[sync-doc-assoc] Please review the changes with git diff before committing.") return 0 if __name__ == "__main__": sys.exit(sync_associations())