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,库内从无先例)
This commit is contained in:
1 parent
a9d2adaf72
commit
acfb252bee
14 files changed
+626
-39
No files matched your search
@@ -0,0 +1,121 @@
|
||||
#!/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())
|
||||
Reference in new issue
Block a user