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:
admin committed 2026-10-18 18:07:40 +08:00
1 parent a9d2adaf72
commit acfb252bee
14 files changed
+626 -39

No files matched your search

+121
View File
@@ -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())