- hooks/git/{pre-commit,pre-push} + scripts/install-git-hooks.sh(Gitea Actions 无 runner,本地兜底)
- install-skills.sh: --scope project --project-dir(装到项目 .agents/skills)
- scripts/scan-installed.sh: 扫第三方已装技能(33 个,0 高危)
- generate-catalog: has_evals/has_docs + coverage;validate --coverage + 路由一致性检查
- SCOPE 覆盖率路线图(3-4 周补齐 40 个技能的 evals 与四段式文档)
- MAINTENANCE/README/CHANGELOG 同步
This commit is contained in:
@@ -136,8 +136,58 @@ def check_skill(rel: Path, skill_md: Path, meta: dict):
|
||||
warnings.append(f"{where}: 引用的相对路径不存在: {target}")
|
||||
|
||||
|
||||
IGNORED_ROUTING_TOKENS = {
|
||||
"completed", "released", "delivery_stage", "status", "true", "false", "user-invoked",
|
||||
"model-invoked", "promoted", "beta", "deprecated", "scoop", "scoop.md",
|
||||
}
|
||||
EXTERNAL_SKILLS = {
|
||||
# 本仓库之外、由 DSH 用户目录提供的技能(docs/skill-routing.md 允许引用)
|
||||
"project-management", "topic-distill", "diary", "dsh-worktable-page",
|
||||
"code-review", "release-notes", "disk-cleanup", "workflow-one",
|
||||
}
|
||||
|
||||
|
||||
def check_routing(catalog_names: set[str]):
|
||||
routing = ROOT / "docs" / "skill-routing.md"
|
||||
if not routing.is_file():
|
||||
return
|
||||
text = routing.read_text(encoding="utf-8")
|
||||
for token in sorted(set(re.findall(r"`([a-z][a-z0-9-]{2,40})`", text))):
|
||||
if token in IGNORED_ROUTING_TOKENS or token in EXTERNAL_SKILLS:
|
||||
continue
|
||||
if token not in catalog_names:
|
||||
warnings.append(f"docs/skill-routing.md: 路由到不存在的技能 {token!r}")
|
||||
|
||||
|
||||
def coverage_report() -> int:
|
||||
missing_evals, missing_docs = [], []
|
||||
for plugin_json in sorted(ROOT.glob("skills-*/*-plugin/.claude-plugin/plugin.json")):
|
||||
meta = json.loads(plugin_json.read_text())
|
||||
if meta.get("status", "promoted") != "promoted":
|
||||
continue
|
||||
skill_md = next((plugin_json.parent.parent / "skills").rglob("SKILL.md"), None)
|
||||
if skill_md is None:
|
||||
continue
|
||||
fm, _ = parse_frontmatter(skill_md.read_text(encoding="utf-8", errors="replace"))
|
||||
if (fm.get("metadata") or {}).get("invocation", "model-invoked") != "model-invoked":
|
||||
continue
|
||||
name = meta.get("install_name") or fm.get("name")
|
||||
if not any((plugin_json.parent.parent / "skills").rglob("evals/evals.json")):
|
||||
missing_evals.append(name)
|
||||
if not (ROOT / "docs" / "skills" / f"{name}.md").is_file():
|
||||
missing_docs.append(name)
|
||||
print(f"覆盖率(promoted + model-invoked):缺 evals {len(missing_evals)} 个,缺四段式文档 {len(missing_docs)} 个")
|
||||
if missing_evals:
|
||||
print("缺 evals:", ", ".join(missing_evals[:20]) + ("..." if len(missing_evals) > 20 else ""))
|
||||
if missing_docs:
|
||||
print("缺文档:", ", ".join(missing_docs[:20]) + ("..." if len(missing_docs) > 20 else ""))
|
||||
return 0
|
||||
|
||||
|
||||
def main() -> int:
|
||||
strict = "--strict" in sys.argv
|
||||
if "--coverage" in sys.argv:
|
||||
return coverage_report()
|
||||
plugins = sorted(ROOT.glob("skills-*/*-plugin/.claude-plugin/plugin.json"))
|
||||
if not plugins:
|
||||
print("未找到任何 plugin.json", file=sys.stderr)
|
||||
@@ -145,6 +195,11 @@ def main() -> int:
|
||||
for plugin_json in plugins:
|
||||
check_plugin(plugin_json)
|
||||
|
||||
catalog = ROOT / "catalog.json"
|
||||
if catalog.is_file():
|
||||
names = {x["name"] for x in json.loads(catalog.read_text()).get("skills", [])}
|
||||
check_routing(names)
|
||||
|
||||
for warning in warnings:
|
||||
print(f"[warn] {warning}")
|
||||
for error in errors:
|
||||
|
||||
Reference in New Issue
Block a user