phase 6 part 2: per-week related-topics sections (gen_week_topics.py), week14/15/16 adjustments from BOSS research, README/CONTRIBUTING updates

This commit is contained in:
Shikui Wang committed 2026-10-23 17:00:50 +08:00
1 parent 5b1a7a430f
commit 4f12b8b2ba
22 files changed
+588 -18

No files matched your search

+103
View File
@@ -0,0 +1,103 @@
#!/usr/bin/env python3
"""根据 catalog.json 中每个条目的 week 字段,在 plan/weekNN-*.md 中生成“本周相关专题”一节。
- 生成内容放在 <!-- week-topics:start --> 和 <!-- week-topics:end --> 之间,重复运行会替换,不会重复插入
- 第一次插入的位置:“## 本周自查清单 / ## 本周验收”之前(代码块之外);没有这两个标题时放在文末
- 先运行 tools/build_catalog.py 生成 catalog.json
用法:python3 tools/gen_week_topics.py [--check] (--check 只检查是否需要更新,需要时返回 1)
"""
from __future__ import annotations
import json
import re
import sys
from collections import defaultdict
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
START, END = "<!-- week-topics:start -->", "<!-- week-topics:end -->"
ANCHOR = re.compile(r"^## (本周自查清单|本周验收)")
FENCE = re.compile(r"^\s*(```|~~~)")
TYPE_ORDER = {"lesson": 0, "scenario": 1, "interview": 2}
TYPE_NAME = {"lesson": "课程", "scenario": "场景", "interview": "面试题"}
AI_CODING_NOTE = ("- **AI 编程练习(每周)**:本周至少一个功能用 AI 编程工具(Cursor / Claude Code / Codex 等)完成,"
"按 [AI 编程:工作流](../topics/ai/ai-coding/01-workflow-rules-skills.md) 写清需求和规则,"
"按 [验证与证据](../topics/ai/ai-coding/02-verify-evidence.md) 保存测试结果和会话记录(脱敏后),"
"第 15 周整理成“AI 编程 + 验证方法”的应聘案例。")
def build_section(week: int, items: list[dict], stacks: dict[str, dict]) -> str:
by_stack: dict[str, list[dict]] = defaultdict(list)
cross: list[dict] = []
for it in items:
if it["type"] == "scenario" and it["domain"] == "cross":
cross.append(it)
elif it["type"] in TYPE_ORDER and it.get("stack"):
by_stack[it["path"].split("/")[2]].append(it)
lines = [START, "", "## 本周相关专题", "",
"> 本节由 `tools/gen_week_topics.py` 根据专题文件的 `week` 字段自动生成,请勿手改;"
"想调整就修改对应专题文件的 frontmatter 后重新生成。", ""]
if week <= 14:
lines += [AI_CODING_NOTE, ""]
if by_stack:
lines += ["| 技术栈 | 课程 / 场景 / 面试题 |", "| --- | --- |"]
for sid in sorted(by_stack, key=lambda s: (stacks.get(s, {}).get("domain", ""), s)):
st = stacks.get(sid, {})
name = f"[{st.get('title', sid)}](../{st['index']})" if st.get("index") else sid
links = []
for it in sorted(by_stack[sid], key=lambda x: (TYPE_ORDER[x["type"]], x.get("order") or 0, x["path"])):
title = TYPE_NAME[it["type"]] if it["type"] == "interview" else it["title"].split(":")[0]
links.append(f"{TYPE_NAME[it['type']]}:[{title}](../{it['path']})" if it["type"] != "interview"
else f"[面试题](../{it['path']})")
lines.append(f"| {name} | {';'.join(links)} |")
lines.append("")
if cross:
lines += ["**跨技术栈场景**:" + "、".join(f"[{c['title'].split(':')[0]}](../{c['path']})" for c in sorted(cross, key=lambda c: c["path"])), ""]
lines.append(END)
return "\n".join(lines)
def insert(text: str, section: str) -> str:
if START in text and END in text:
return re.sub(re.escape(START) + r".*?" + re.escape(END), lambda _: section, text, count=1, flags=re.S)
in_fence = False
lines = text.split("\n")
for i, line in enumerate(lines):
if FENCE.match(line):
in_fence = not in_fence
elif not in_fence and ANCHOR.match(line):
return "\n".join(lines[:i] + [section, ""] + lines[i:])
return text.rstrip("\n") + "\n\n" + section + "\n"
def main() -> int:
check = "--check" in sys.argv
cat = json.loads((ROOT / "catalog.json").read_text(encoding="utf-8"))
stacks = {s["stack"]: s for s in cat["stacks"]}
per_week: dict[int, list[dict]] = defaultdict(list)
for it in cat["items"]:
if it["path"].startswith(("topics/", "scenarios/")):
for w in it.get("week") or []:
per_week[int(w)].append(it)
stale = []
for f in sorted((ROOT / "plan").glob("week*.md")):
week = int(re.match(r"week(\d+)", f.name).group(1))
old = f.read_text(encoding="utf-8")
new = insert(old, build_section(week, per_week.get(week, []), stacks))
if new != old:
stale.append(f.name)
if not check:
f.write_text(new, encoding="utf-8")
if check:
if stale:
print("需要重新生成:" + "、".join(stale))
return 1
print("各周“本周相关专题”已是最新")
return 0
print(f"已更新 {len(stale)} 个周计划文件")
return 0
if __name__ == "__main__":
sys.exit(main())