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,336 @@
#!/usr/bin/env python3
"""changelog_build.py — 从 fragment 源生成三层 changelog 视图。
设计目标(解决多人协作 + git merge 冲突):
- 唯一数据源是 ``.cursor/changelog/entries/<author>/<id>.md`` 一条一文件的 fragment。
- 三层视图full / recent / headlines全部由本脚本**确定性生成**,不再手写。
- 因为排序只依赖 fragment 内的 CL 时间戳,与 git merge 顺序无关,所有人重建后逐字节收敛。
只用标准库,兼容 Python 3.10+,可在任意平台运行。
"""
from __future__ import annotations
import argparse
import re
import sys
from dataclasses import dataclass, field
from pathlib import Path
# ---- 路径约定 --------------------------------------------------------------
CHANGELOG_DIR_NAME = ".cursor/changelog"
ENTRIES_SUBDIR = "entries"
FULL_NAME = "changelog-full.md"
RECENT_NAME = "changelog-recent.md"
HEADLINES_NAME = "changelog-headlines.md"
BY_AUTHOR_NAME = "changelog-by-author.md"
RECENT_WINDOW = 20
HEADLINES_WINDOW = 50
# ---- fragment 解析 ---------------------------------------------------------
ID_RE = re.compile(r"^CL-(\d{8})-(\d{3,4})([a-z]*)(?:-(.+))?$")
FM_FENCE = "---"
SECTION_RE = re.compile(r"^<!--\s*(L1|L2|L3)\s*-->\s*$", re.M)
@dataclass
class Fragment:
fid: str
date: str = ""
author: str = "legacy"
author_name: str = ""
type: str = ""
merge_source: str = ""
status: str = "active"
superseded_by: str = ""
title: str = ""
source_chat: str = ""
tags: list[str] = field(default_factory=list)
affected_files: list[str] = field(default_factory=list)
l3: str = ""
l2: str = ""
l1: str = ""
path: Path | None = None
def sort_key(self) -> tuple[str, str, str, str]:
m = ID_RE.match(self.fid)
if not m:
return (self.fid, "", "", "")
date8, time4, suffix, _author = m.groups()
return (date8, time4.zfill(4), suffix, self.fid)
def date_only(self) -> str:
return self.date.split(" ")[0] if self.date else ""
def _parse_frontmatter(text: str) -> dict[str, object]:
data: dict[str, object] = {}
lines = text.splitlines()
i = 0
while i < len(lines):
line = lines[i]
if not line.strip():
i += 1
continue
m = re.match(r"^([A-Za-z_]+):\s*(.*)$", line)
if not m:
i += 1
continue
key, val = m.group(1), m.group(2).strip()
if val == "":
items: list[str] = []
j = i + 1
while j < len(lines) and re.match(r"^\s+-\s+", lines[j]):
items.append(lines[j].strip()[1:].strip())
j += 1
if items:
data[key] = items
i = j
else:
data[key] = ""
i += 1
elif val.startswith("[") and val.endswith("]"):
inner = val[1:-1].strip()
data[key] = [x.strip() for x in inner.split(",") if x.strip()] if inner else []
i += 1
else:
data[key] = val
i += 1
return data
def parse_fragment(path: Path) -> Fragment:
raw = path.read_text(encoding="utf-8")
if not raw.startswith(FM_FENCE):
raise ValueError(f"fragment 缺少 frontmatter: {path}")
rest = raw[len(FM_FENCE):]
end = rest.find("\n" + FM_FENCE)
if end == -1:
raise ValueError(f"fragment frontmatter 未闭合: {path}")
fm_text = rest[:end]
body = rest[end + len("\n" + FM_FENCE):].lstrip("\n")
fm = _parse_frontmatter(fm_text)
sections: dict[str, str] = {}
parts = SECTION_RE.split(body)
it = iter(parts[1:])
for marker, content in zip(it, it):
sections[marker] = content.strip("\n")
def _s(key: str) -> str:
v = fm.get(key, "")
return v if isinstance(v, str) else ""
def _l(key: str) -> list[str]:
v = fm.get(key, [])
return [str(x) for x in v] if isinstance(v, list) else []
frag = Fragment(
fid=_s("id"),
date=_s("date"),
author=_s("author") or "legacy",
author_name=_s("author_name"),
type=_s("type"),
merge_source=_s("merge_source"),
status=_s("status") or "active",
superseded_by=_s("superseded_by"),
title=_s("title"),
source_chat=_s("source_chat"),
tags=_l("tags"),
affected_files=_l("affected_files"),
l3=sections.get("L3", "").strip(),
l2=sections.get("L2", "").strip(),
l1=sections.get("L1", "").strip(),
path=path,
)
if not frag.fid:
raise ValueError(f"fragment 缺少 id: {path}")
return frag
def load_fragments(entries_dir: Path) -> list[Fragment]:
frags: list[Fragment] = []
seen: dict[str, Path] = {}
for path in sorted(entries_dir.rglob("*.md")):
frag = parse_fragment(path)
if frag.fid in seen:
raise ValueError(
f"重复的 CL id {frag.fid}: {path}{seen[frag.fid]}"
)
seen[frag.fid] = path
frags.append(frag)
frags.sort(key=lambda f: f.sort_key(), reverse=True)
return frags
# ---- 渲染 ------------------------------------------------------------------
def _author_tag(frag: Fragment) -> str:
return "" if frag.author in ("", "legacy") else f"@{frag.author} "
def render_headlines(frags: list[Fragment]) -> str:
head = (
"# Dev Changelog — Headlines\n\n"
"最近 ~50 次改动的一句话概要,按时间倒序排列。每次会话自动注入上下文。\n"
"> 本文件由 `tools/changelog_build.py` 从 `entries/` 自动生成,请勿手改。"
)
lines = []
for frag in frags[:HEADLINES_WINDOW]:
merge = f" [merge:{frag.merge_source}]" if frag.merge_source else ""
text = frag.l3 or frag.title
lines.append(f"- [{frag.fid}] {_author_tag(frag)}{text}{merge}")
body = "\n\n".join(lines)
return head + (f"\n\n{body}" if body else "") + "\n"
def render_recent(frags: list[Fragment]) -> str:
head = (
"# Dev Changelog — Recent\n\n"
"最近 ~20 次改动的摘要记录,按时间倒序排列。\n"
"当 Agent 检测到当前任务与近期改动相关时自动读取。\n"
"> 本文件由 `tools/changelog_build.py` 从 `entries/` 自动生成,请勿手改。"
)
blocks = []
for frag in frags[:RECENT_WINDOW]:
b = [f"### [{frag.fid}] {frag.date_only()}{frag.title}"]
if frag.merge_source:
b.append(f"- **merge_source**: {frag.merge_source}")
if frag.author and frag.author != "legacy":
b.append(f"- **author**: {frag.author}")
if frag.tags:
b.append(f"- **tags**: {', '.join(frag.tags)}")
if frag.affected_files:
b.append(f"- **affected_files**: {''.join(frag.affected_files)}")
if frag.l2:
b.append(f"- **summary**: {frag.l2}")
if frag.source_chat:
b.append(f"- **source_chat**: {frag.source_chat}")
blocks.append("\n".join(b))
body = "\n\n".join(blocks)
return head + (f"\n\n{body}" if body else "") + "\n"
def render_full(frags: list[Fragment]) -> str:
head = (
"# Dev Changelog — Full\n\n"
"完整的开发改动记录,按时间倒序排列。作为主动 RAG 的数据源,用户手动唤醒时读取。\n"
"> 本文件由 `tools/changelog_build.py` 从 `entries/` 自动生成,请勿手改;"
"新增/修改请编辑 `entries/<author>/<id>.md` 后重跑生成器。\n\n"
"## 记录"
)
blocks = []
for frag in frags:
b = [f"### [{frag.fid}] {frag.date}{frag.title}"]
if frag.merge_source:
b.append(f"- **merge_source**: {frag.merge_source}")
author_line = frag.author
if frag.author_name:
author_line = f"{frag.author} ({frag.author_name})"
if frag.author and frag.author != "legacy":
b.append(f"- **author**: {author_line}")
if frag.type:
b.append(f"- **type**: {frag.type}")
if frag.status and frag.status != "active":
sup = f"{frag.superseded_by}" if frag.superseded_by else ""
b.append(f"- **status**: {frag.status}{sup}")
if frag.tags:
b.append(f"- **tags**: {', '.join(frag.tags)}")
if frag.affected_files:
af = "\n".join(f" - {x}" for x in frag.affected_files)
b.append(f"- **affected_files**:\n{af}")
if frag.l1:
b.append(frag.l1)
if frag.source_chat:
b.append(f"- **source_chat**: {frag.source_chat}")
blocks.append("\n".join(b))
body = "\n\n".join(blocks)
return head + (f"\n\n{body}" if body else "") + "\n"
def render_by_author(frags: list[Fragment]) -> str:
head = (
"# Dev Changelog — By Author\n\n"
"按作者聚合的贡献概览,便于多人协作时区分提交来源。\n"
"> 本文件由 `tools/changelog_build.py` 自动生成,请勿手改。"
)
by: dict[str, list[Fragment]] = {}
for frag in frags:
by.setdefault(frag.author or "legacy", []).append(frag)
blocks = []
for author in sorted(by):
items = by[author]
name = next((f.author_name for f in items if f.author_name), "")
title = f"## {author}" + (f" ({name})" if name else "") + f"{len(items)}"
rows = [
f"- [{f.fid}] {f.date_only()}{f.title}" for f in items[:HEADLINES_WINDOW]
]
blocks.append(title + "\n" + "\n".join(rows))
body = "\n\n".join(blocks)
return head + (f"\n\n{body}" if body else "") + "\n"
# ---- 入口 ------------------------------------------------------------------
def find_changelog_dir(start: Path) -> Path:
cur = start.resolve()
for cand in [cur, *cur.parents]:
d = cand / CHANGELOG_DIR_NAME
if d.is_dir():
return d
# 脚本自身位于 .cursor/changelog/tools/ 下时回退
here = Path(__file__).resolve().parent.parent
if here.name == "changelog":
return here
raise SystemExit("找不到 .cursor/changelog 目录")
def build(changelog_dir: Path, check: bool = False) -> int:
entries_dir = changelog_dir / ENTRIES_SUBDIR
if not entries_dir.is_dir():
raise SystemExit(f"entries 目录不存在: {entries_dir}")
frags = load_fragments(entries_dir)
targets = {
HEADLINES_NAME: render_headlines(frags),
RECENT_NAME: render_recent(frags),
FULL_NAME: render_full(frags),
BY_AUTHOR_NAME: render_by_author(frags),
}
drift = False
for name, content in targets.items():
path = changelog_dir / name
old = path.read_text(encoding="utf-8") if path.exists() else None
if old != content:
drift = True
if check:
print(f"[drift] {name} 与 fragment 源不一致")
else:
path.write_text(content, encoding="utf-8", newline="\n")
print(f"[write] {name}")
if check:
if drift:
print("视图与 fragment 源不一致,请运行 changelog_build.py 重建。")
return 1
print("视图已与 fragment 源同步。")
return 0
print(f"完成:{len(frags)} 条 fragment → 4 个视图。")
return 0
def main(argv: list[str] | None = None) -> int:
ap = argparse.ArgumentParser(description="从 fragment 源生成三层 changelog 视图")
ap.add_argument("changelog_dir", nargs="?", default=None, help=".cursor/changelog 路径")
ap.add_argument("--check", action="store_true", help="只校验是否漂移不写文件CI 用)")
args = ap.parse_args(argv)
changelog_dir = (
Path(args.changelog_dir) if args.changelog_dir else find_changelog_dir(Path.cwd())
)
return build(changelog_dir, check=args.check)
if __name__ == "__main__":
sys.exit(main())