cursor Init 完善多人协同changelog,以及godot相关基础skill和代码规范

This commit is contained in:
Nostars Developer
2026-07-15 14:38:08 +08:00
parent fc8557f2a9
commit 6446261c69
56 changed files with 3523 additions and 1584 deletions

View File

@@ -0,0 +1,252 @@
#!/usr/bin/env python3
"""migrate_changelog.py — 一次性迁移:把旧的三层 changelog 文件拆成 fragment。
把现有 ``changelog-full.md`` 按 ``### [CL-...]`` 头切成一条一文件的 fragment
写到 ``entries/legacy/<id>.md``L3/L2 内容尽量从旧的 headlines/recent 中回收,
回收不到时用标题兜底。迁移后由 ``changelog_build.py`` 重新生成视图。
幂等:重复运行会覆盖 entries/legacy/ 下的同名文件。只用标准库。
"""
from __future__ import annotations
import re
import sys
from pathlib import Path
ENTRY_HEADER_RE = re.compile(r"^### \[(CL-[^\]]+)\]\s*(.*)$")
DATE_TITLE_RE = re.compile(r"^(\d{4}-\d{2}-\d{2}[^—]*?)\s*—\s*(.*)$")
ID_DIGITS_RE = re.compile(r"^CL-(\d{4})(\d{2})(\d{2})-(\d{2})(\d{2})")
HEADLINE_LINE_RE = re.compile(r"^- \[(CL-[^\]]+)\]\s*(.*)$")
MERGE_TAG_RE = re.compile(r"\s*\[merge:[^\]]+\]\s*$")
TYPE_BY_TAG = {
"bugfix": "fix",
"fix": "fix",
"feature": "feat",
"feat": "feat",
"docs": "docs",
"refactor": "refactor",
"chore": "chore",
}
def split_entries(full_text: str) -> list[tuple[str, list[str]]]:
"""返回 [(id, [lines...]), ...]lines 含 header 行。"""
lines = full_text.splitlines()
entries: list[tuple[str, list[str]]] = []
cur_id: str | None = None
cur: list[str] = []
for line in lines:
m = ENTRY_HEADER_RE.match(line)
if m:
if cur_id is not None:
entries.append((cur_id, cur))
cur_id = m.group(1)
cur = [line]
elif cur_id is not None:
cur.append(line)
if cur_id is not None:
entries.append((cur_id, cur))
return entries
def parse_headlines(text: str) -> dict[str, str]:
out: dict[str, str] = {}
for line in text.splitlines():
m = HEADLINE_LINE_RE.match(line)
if m:
body = MERGE_TAG_RE.sub("", m.group(2)).strip()
out[m.group(1)] = body
return out
def parse_recent_summaries(text: str) -> dict[str, str]:
out: dict[str, str] = {}
cur_id: str | None = None
for line in text.splitlines():
m = ENTRY_HEADER_RE.match(line)
if m:
cur_id = m.group(1)
continue
if cur_id and line.startswith("- **summary**:"):
out[cur_id] = line[len("- **summary**:"):].strip()
return out
def strip_wrap_backticks(s: str) -> str:
return re.sub(r"^`([^`]+)`$", r"\1", s.strip())
def derive_date(entry_id: str, header_rest: str) -> tuple[str, str]:
"""返回 (date, title)。"""
m = DATE_TITLE_RE.match(header_rest)
if m:
return m.group(1).strip(), m.group(2).strip()
# 无 date —— 标题就是整段date 从 id 推
dm = ID_DIGITS_RE.match(entry_id)
if dm:
y, mo, d, hh, mm = dm.groups()
return f"{y}-{mo}-{d} {hh}:{mm}", header_rest.strip()
return "", header_rest.strip()
def parse_body(lines: list[str]) -> dict[str, object]:
"""解析一个 L1 条目的 body不含 header 行)。"""
tags: list[str] = []
affected: list[str] = []
merge_source = ""
source_chat = ""
l1_lines: list[str] = []
i = 0
n = len(lines)
while i < n:
line = lines[i]
stripped = line.strip()
if stripped.startswith("- **merge_source**:"):
merge_source = stripped[len("- **merge_source**:"):].strip()
i += 1
continue
if stripped.startswith("- **tags**:"):
raw = stripped[len("- **tags**:"):].strip()
tags = [strip_wrap_backticks(t) for t in raw.split(",") if t.strip()]
i += 1
continue
if stripped.startswith("- **affected_files**:"):
inline = stripped[len("- **affected_files**:"):].strip()
if inline:
affected = [strip_wrap_backticks(x) for x in re.split(r"[、,]", inline) if x.strip()]
i += 1
else:
i += 1
while i < n and re.match(r"^\s+-\s+", lines[i]):
item = lines[i].strip()[1:].strip()
affected.append(strip_wrap_backticks(item))
i += 1
continue
if stripped.startswith("- **source_chat**:"):
source_chat = stripped[len("- **source_chat**:"):].strip()
i += 1
continue
l1_lines.append(line)
i += 1
# 去掉首尾空行
while l1_lines and not l1_lines[0].strip():
l1_lines.pop(0)
while l1_lines and not l1_lines[-1].strip():
l1_lines.pop()
return {
"tags": tags,
"affected_files": affected,
"merge_source": merge_source,
"source_chat": source_chat,
"l1": "\n".join(l1_lines),
}
def infer_type(tags: list[str]) -> str:
for t in tags:
if t.lower() in TYPE_BY_TAG:
return TYPE_BY_TAG[t.lower()]
return ""
def fm_scalar(key: str, value: str) -> str:
return f"{key}: {value}".rstrip()
def fm_list(key: str, items: list[str]) -> str:
if not items:
return f"{key}:"
body = "\n".join(f" - {x}" for x in items)
return f"{key}:\n{body}"
def build_fragment_text(
*, fid: str, date: str, author: str, type_: str, merge_source: str,
title: str, source_chat: str, tags: list[str], affected: list[str],
l3: str, l2: str, l1: str,
) -> str:
fm = [
"---",
fm_scalar("id", fid),
fm_scalar("date", date),
fm_scalar("author", author),
fm_scalar("author_name", ""),
fm_scalar("type", type_),
fm_scalar("merge_source", merge_source),
fm_scalar("status", "active"),
fm_scalar("superseded_by", ""),
fm_scalar("title", title),
fm_scalar("source_chat", source_chat),
fm_list("tags", tags),
fm_list("affected_files", affected),
"---",
]
parts = [
"\n".join(fm),
"",
"<!-- L3 -->",
l3,
"",
"<!-- L2 -->",
l2,
"",
"<!-- L1 -->",
l1,
"",
]
return "\n".join(parts)
def main(argv: list[str] | None = None) -> int:
argv = argv if argv is not None else sys.argv[1:]
changelog_dir = Path(argv[0]) if argv else Path(__file__).resolve().parent.parent
author = argv[1] if len(argv) > 1 else "legacy"
full_path = changelog_dir / "changelog-full.md"
recent_path = changelog_dir / "changelog-recent.md"
headlines_path = changelog_dir / "changelog-headlines.md"
if not full_path.exists():
raise SystemExit(f"找不到 {full_path}")
full_text = full_path.read_text(encoding="utf-8")
headlines = parse_headlines(headlines_path.read_text(encoding="utf-8")) if headlines_path.exists() else {}
recent = parse_recent_summaries(recent_path.read_text(encoding="utf-8")) if recent_path.exists() else {}
entries = split_entries(full_text)
out_dir = changelog_dir / "entries" / author
out_dir.mkdir(parents=True, exist_ok=True)
count = 0
for fid, lines in entries:
header_rest = ENTRY_HEADER_RE.match(lines[0]).group(2)
date, title = derive_date(fid, header_rest)
parsed = parse_body(lines[1:])
l3 = headlines.get(fid, "").strip() or title
l2 = recent.get(fid, "").strip() or l3
text = build_fragment_text(
fid=fid,
date=date,
author=author,
type_=infer_type(parsed["tags"]),
merge_source=str(parsed["merge_source"]),
title=title,
source_chat=str(parsed["source_chat"]),
tags=list(parsed["tags"]),
affected=list(parsed["affected_files"]),
l3=l3,
l2=l2,
l1=str(parsed["l1"]),
)
(out_dir / f"{fid}.md").write_text(text, encoding="utf-8", newline="\n")
count += 1
print(f"迁移完成:{count} 条 → {out_dir}")
return 0
if __name__ == "__main__":
sys.exit(main())