Files
workbuddy_skills/product-planning/scripts/check_figures.py
T
admin acfb252bee feat(skills): 两包资源索引机制与双向对账脚本
- product-planning:新建 references/manifest.md 作资源唯一登记处(资源/落点/引用点/口径);
  顶层 SKILL 加两条硬规矩,design-capture、stage-delivery、execution-runbook、stage-requirements
  四处把散落路径改为「落点见 references/manifest.md」
- session-mechanism:01-文档索引 新增 §四「包内资源索引」,并说明不放 manifest.md 的理由
  (那份由 install.py 本机生成、被 .gitignore 挡着)
- 新增两包共用的 scripts/check_resource_index.py(双向对账)与 product-planning/scripts/check_figures.py(图片机检),
  selftest 补对应用例,06-技能优化规程 写入写法规矩
- .gitignore:新增 _留痕/ 不入库(改造前整包快照,180K/688K,库内从无先例)
2026-10-18 18:07:40 +08:00

122 lines
3.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python
"""产品规划文档的「开头必须有图」机检(单一真源:SKILL.md §产出文档开头必须有图)。
用法:
python scripts/check_figures.py --ws <工作区或产品根>
判定三件事(任一不过即 FAIL):
① 每份落盘 md(`产品规划/` 下,排除 `归档-过期版/`)开头 8 行内有 `![…](…)`
② 引用的图文件在位(相对该 md 所在目录解析)
③ 同一张图没有被两份文档共用(一份文档一张自己的图)
输出:逐份一行;末尾给 PASS/FAIL 与计数。退出码 0=全过,1=有 FAIL。
"""
import argparse
import re
import sys
from pathlib import Path
SKIP_DIRS = {"归档-过期版", "node_modules", ".git"}
FIG_RE = re.compile(r"!\[[^\]]*\]\(([^)\s]+)")
def fmt(p: Path) -> str:
try:
return str(p.relative_to(p.anchor and Path(p.anchor) or Path(".")))
except Exception:
return str(p)
def resolve_product_root(ws: Path) -> Path | None:
"""认产品根:自身含 产品规划/ 即产品根;否则在 项目/<名>/ 或下一层里找唯一一个。"""
if (ws / "产品规划").is_dir():
return ws
cands = [d for d in (ws / "项目").glob("*") if (d / "产品规划").is_dir()] if (ws / "项目").is_dir() else []
if not cands:
cands = [d for d in ws.glob("*/产品规划") if d.is_dir()]
cands = [d.parent for d in cands]
if len(cands) == 1:
return cands[0]
if not cands:
return None
print("⚠️ 找到多个候选产品根,请直接传产品根路径:")
for c in cands:
print(" ", c)
return None
def head_lines(p: Path, n: int = 8) -> list[str]:
out = []
try:
with p.open(encoding="utf-8", errors="replace") as f:
for line in f:
if line.strip():
out.append(line.rstrip("\n"))
if len(out) >= n:
break
except Exception as e:
print(" 读取失败:%s" % e)
return out
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--ws", required=True, help="工作区根或产品根")
a = ap.parse_args()
root = resolve_product_root(Path(a.ws).resolve())
if root is None:
print("FAIL 认不出产品根:%s" % a.ws)
return 1
base = root / "产品规划"
print("产品根 = %s" % root)
docs = [p for p in base.rglob("*.md")
if not (set(p.parts) & SKIP_DIRS)
and not p.name.startswith(("索引", "本轮回报"))]
docs.sort()
if not docs:
print("FAIL 产品规划下没有 md 文档")
return 1
used: dict[str, list[Path]] = {}
fails = 0
for d in docs:
rel = d.relative_to(root)
hit = None
for line in head_lines(d):
m = FIG_RE.search(line)
if m:
hit = m.group(1)
break
if not hit:
print(" ❌ 无图 %s(开头 8 行没有 ![..](..))" % rel)
fails += 1
continue
target = (d.parent / hit).resolve()
if not target.exists():
print(" ❌ 图不在位 %s → %s" % (rel, hit))
fails += 1
continue
key = str(target).lower()
used.setdefault(key, []).append(rel)
print(" ✅ %s → %s" % (rel, hit))
shared = {k: v for k, v in used.items() if len(v) > 1}
for k, v in shared.items():
names = "、".join(str(x) for x in v)
print(" ❌ 图被共用 %s 被 %d 份文档共用(%s)" % (Path(k).name, len(v), names))
fails += len(v) - 1
print("—" * 40)
print("文档 %d 份,图 %d 张,问题 %d 处" % (len(docs), len(used), fails))
if fails:
print("FAIL")
return 1
print("PASS")
return 0
if __name__ == "__main__":
sys.exit(main())