- 删除 01~05 旧中文教程目录 - 将 03 核心功能教程拆分为各 framework 模块的子文档 - 06 工具脚本移入 modules/frameworks/编辑器工具/UI工具/ - 非框架内容合并进 rules/20-开发规则.md 与 rules/40-工作流清单.md - 更新所有交叉链接与文档关联表 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
219 lines
7.4 KiB
Python
219 lines
7.4 KiB
Python
#!/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())
|