This repository has been archived on 2026-07-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files

446 lines
18 KiB
Python
Raw Permalink 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 python3
"""
SKILL 体系静态一致性检查 (Layer 1)
检测 canonical-manifest.json、opencode-init SKILL.md、registry.md、各 SKILL.md 之间的
交叉引用一致性,不需要 LLM 调用。
"""
import json
import os
import re
import sys
from pathlib import Path
# --- 路径解析 ---
SCRIPT_DIR = Path(__file__).resolve().parent
SKILL_TESTER_DIR = SCRIPT_DIR.parent
SKILLS_DIR = SKILL_TESTER_DIR.parent
OPENDODE_DIR = SKILLS_DIR.parent
PROJECT_ROOT = OPENDODE_DIR.parent
MANIFEST_PATH = OPENDODE_DIR / "canonical-manifest.json"
INIT_SKILL_PATH = SKILLS_DIR / "opencode-init" / "SKILL.md"
REGISTRY_PATH = SKILLS_DIR / "epee-orchestrator" / "registry.md"
class CheckResult:
def __init__(self, name, passed, detail=""):
self.name = name
self.passed = passed
self.detail = detail
def load_manifest():
with open(MANIFEST_PATH, encoding="utf-8") as f:
return json.load(f)
def load_init_md():
with open(INIT_SKILL_PATH, encoding="utf-8") as f:
return f.read()
def load_registry_md():
with open(REGISTRY_PATH, encoding="utf-8") as f:
return f.read()
# ≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡
# 检查 1manifest canonical_skills 与 init Skills 白名单一致
# ≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡
def check_manifest_vs_init_skills():
manifest = load_manifest()
init_md = load_init_md()
manifest_skills = set()
for s in manifest.get("canonical_skills", []):
name = s.rstrip("/").split("/")[-1]
manifest_skills.add(name)
# 从 init 的 Skills 白名单提取
init_skills = set()
in_whitelist = False
for line in init_md.split("\n"):
if "Skills规范 Skill 目录白名单)" in line or "Skills 白名单" in line:
in_whitelist = True
continue
if in_whitelist:
if line.strip().startswith(">") or line.strip() == "":
continue
if line.strip().startswith("-"):
m = re.search(r"`\.opencode/skills/([^/]+)/`", line)
if m:
init_skills.add(m.group(1))
elif not line.strip().startswith("-") and not line.strip().startswith("`"):
in_whitelist = False
only_manifest = manifest_skills - init_skills
only_init = init_skills - manifest_skills
if not only_manifest and not only_init:
return CheckResult("manifest vs init Skills 白名单一致", True)
details = []
if only_manifest:
details.append(f"manifest 有但 init 缺: {only_manifest}")
if only_init:
details.append(f"init 有但 manifest 缺: {only_init}")
return CheckResult("manifest vs init Skills 白名单一致", False, "; ".join(details))
# ≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡
# 检查 2manifest template_phase_files 与 init 阶段数据占位目录一致
# ≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡
def check_manifest_vs_init_phase_files():
manifest = load_manifest()
init_md = load_init_md()
manifest_files = set()
for f in manifest.get("template_phase_files", []):
manifest_files.add(f.replace(".opencode/", ""))
# 从 init 的 "阶段数据占位目录" 提取
init_files = set()
in_section = False
for line in init_md.split("\n"):
if "阶段数据占位目录" in line:
in_section = True
continue
if in_section:
if line.strip().startswith("---"):
break
if line.strip().startswith("-"):
m = re.search(r"`\.opencode/(phase/[^`]+)`", line)
if m:
init_files.add(m.group(1))
if "{" in line:
base = re.search(r"`\.opencode/(phase/[^{`]+)\{", line)
sub = re.findall(r"([\w-]+\.[\w]+)", line)
if base and sub:
for s in sub:
init_files.add(f"{base.group(1)}{s}")
only_manifest = {f for f in manifest_files if not any(f.endswith(m.split('/')[-1]) and f.count('/') == m.count('/') for m in init_files)}
# 简化比较:提取文件名对比
manifest_names = {f.split("/")[-1] for f in manifest_files}
init_names = set()
for f in init_files:
init_names.add(f.split("/")[-1])
only_m = manifest_names - init_names
only_i = init_names - manifest_names
if not only_m and not only_i:
return CheckResult("manifest vs init 阶段数据文件一致", True)
details = []
if only_m:
details.append(f"manifest 有但 init 缺: {only_m}")
if only_i:
details.append(f"init 有但 manifest 缺: {only_i}")
return CheckResult("manifest vs init 阶段数据文件一致", False, "; ".join(details))
# ≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡
# 检查 3registry 中非 ["all"] 的 Skill 类型应为"基础设施"(防误删)
# ≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡
def check_registry_types():
registry_md = load_registry_md()
# 解析 registry 条目
entries = {}
current_skill = None
current_type = None
for line in registry_md.split("\n"):
if line.startswith("### "):
current_skill = line.replace("### ", "").strip()
current_type = None
if line.startswith("- **类型**:"):
current_type = line.split("**类型**:")[1].strip()
if current_skill and current_type:
entries[current_skill] = current_type
issues = []
for skill_name in entries:
skill_path = SKILLS_DIR / skill_name / "SKILL.md"
if not skill_path.exists():
continue
content = skill_path.read_text(encoding="utf-8")
# 检查 phases
m = re.search(r'phases:\s*\[(.*?)\]', content)
if not m:
continue
phases_str = m.group(1)
phases = [p.strip().strip('"') for p in phases_str.split(",")]
if "all" in phases:
if entries[skill_name] != "基础设施" and entries[skill_name] != "个人级":
issues.append(f"{skill_name}: phases=[\"all\"] 但类型={entries[skill_name]}(应为基础设施或个人级)")
else:
if entries[skill_name] == "项目级":
issues.append(f"{skill_name}: phases≠[\"all\"] 但类型=项目级init 会删除。应为基础设施)")
if not issues:
return CheckResult("registry 类型与 phases 一致", True)
return CheckResult("registry 类型与 phases 一致", False, "; ".join(issues))
# ≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡
# 检查 4所有 SKILL.md Phase Guard 包含 "all" 处理
# ≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡
def check_phase_guard_all():
issues = []
for skill_dir in SKILLS_DIR.iterdir():
if not skill_dir.is_dir():
continue
skill_md = skill_dir / "SKILL.md"
if not skill_md.exists():
continue
content = skill_md.read_text(encoding="utf-8")
# 只检查有 Phase Guard 块的 Skill
if "相位守卫" not in content and "Phase Guard" not in content:
# 无 Phase Guard 块的 Skill 依赖 AGENTS.md 的外层检查,无需本地守卫
continue
# 检查守卫逻辑是否包含 "all" 处理
guard_section = re.search(
r'相位守卫.*?(?=\n#|\n---|\Z)',
content, re.DOTALL
)
if not guard_section:
guard_section = re.search(
r'Phase Guard.*?(?=\n#|\n---|\Z)',
content, re.DOTALL
)
if guard_section:
guard_text = guard_section.group(0)
if '"all"' not in guard_text:
issues.append(
f"{skill_dir.name}: Phase Guard 未包含 '\"all\"' 特殊值处理 "
f"(仅检查 phase 值是否在 phases 列表中)"
)
if not issues:
return CheckResult("Phase Guard 包含 all 处理", True)
return CheckResult("Phase Guard 包含 all 处理", False, "; ".join(issues))
# ≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡
# 检查 5manifest 每个文件在 init 模板内容章节有对应模板
# ≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡
def check_init_templates_exist():
manifest = load_manifest()
init_md = load_init_md()
all_template_files = (
manifest.get("template_data_files", []) +
manifest.get("template_phase_files", [])
)
missing = []
for fpath in all_template_files:
fname = fpath.split("/")[-1]
# 检查 init 的 "模板内容" 章节是否包含该文件名
templates_section = re.search(
r'## 模板内容.*?(?=## 模板内容 ·|## 冲突|$)',
init_md, re.DOTALL
)
if templates_section and f"### {fname}" not in templates_section.group(0):
# 尝试更宽松的匹配
base_name = fname.replace(".md", "").replace(".json", "")
if base_name not in templates_section.group(0):
missing.append(fname)
if not missing:
return CheckResult("init 模板内容覆盖所有 manifest 文件", True)
return CheckResult("init 模板内容覆盖所有 manifest 文件", False,
f"缺少模板: {missing}")
# ≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡
# 检查 6所有 SKILL.md frontmatter 完整性
# ≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡
def check_skill_frontmatter():
issues = []
for skill_dir in SKILLS_DIR.iterdir():
if not skill_dir.is_dir():
continue
skill_md = skill_dir / "SKILL.md"
if not skill_md.exists():
continue
content = skill_md.read_text(encoding="utf-8")
lines = content.split("\n")
# 检查第一行是否为 ---
if not lines or lines[0].strip() != "---":
issues.append(f"{skill_dir.name}: frontmatter 未以 --- 开头")
continue
# 检查是否有 name 字段
if "name:" not in content[:500]:
issues.append(f"{skill_dir.name}: frontmatter 缺少 name 字段")
continue
# 检查是否有 phases 字段
if "phases:" not in content[:500]:
issues.append(f"{skill_dir.name}: frontmatter 缺少 phases 字段")
continue
# 检查是否正确闭合(第二个 ---
fm_end = content.find("---", 3)
if fm_end == -1:
issues.append(f"{skill_dir.name}: frontmatter 未正确闭合(缺少第二个 ---")
if not issues:
return CheckResult("所有 SKILL.md frontmatter 完整", True)
return CheckResult("所有 SKILL.md frontmatter 完整", False, "; ".join(issues))
# ≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡
# 入口
# ≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡
# ── manifest 分组 → init 章节 的映射表 ──
# 键manifest JSON 中的数组名init SKILL.md 中对应的章节标题关键字
MANIFEST_TO_INIT_SECTION = {
"canonical_skills": "Skills 白名单",
"template_data_files": "数据占位目录",
"template_phase_files": "阶段数据占位目录",
}
# ≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡
# 检查 0自举manifest 分组是否被 init 和 CHECKS 覆盖
# ≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡
def check_meta_coverage():
"""确保 manifest 中的每个文件分组在 init 和 CHECKS 中都有对应覆盖"""
manifest = load_manifest()
init_md = load_init_md()
# manifest 中所有文件数组分组(跳过非文件分组的字段)
manifest_groups = {}
for key in ["canonical_skills", "template_data_files", "template_phase_files"]:
if key in manifest:
manifest_groups[key] = manifest[key]
issues = []
for group_name in manifest_groups:
# 检查 MANIFEST_TO_INIT_SECTION 是否有映射
if group_name not in MANIFEST_TO_INIT_SECTION:
issues.append(
f"manifest 分组 '{group_name}' 在 MANIFEST_TO_INIT_SECTION 中无映射 → "
f"需在 run.py 中新增映射条目"
)
continue
# 检查 init SKILL.md 是否有对应章节
section_keyword = MANIFEST_TO_INIT_SECTION[group_name]
if section_keyword not in init_md:
issues.append(
f"manifest 分组 '{group_name}' 对应的 init 章节 '{section_keyword}' 未找到"
)
# 检查 CHECKS 列表中是否有对应检查
check_ids = [c[0] for c in CHECKS]
has_check = any(
gid in check_ids or group_name.replace("_", "-") in cid
for cid in check_ids
for gid in [group_name]
)
# 更可靠的方式:检查是否有检查函数引用了这个 manifest key
covered = False
for cid, cname, cfn in CHECKS:
src = (cfn.__code__.co_consts if hasattr(cfn, '__code__') else ())
# 简单启发式:函数名或检查 ID 中包含分组关键字
if group_name.replace("_", "-") in cid:
covered = True
break
if not covered and group_name != "canonical_skills":
# canonical_skills 的覆盖在 manifest-skills-sync 里,但它的 ID 不直接包含 canonical_skills
# 不做字符串匹配警告,而是在下面单独处理
pass
if not issues:
return CheckResult("manifest 所有分组均被覆盖", True)
return CheckResult("manifest 所有分组均被覆盖", False, "; ".join(issues))
# ≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡
# 入口 main
# ≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡
CHECKS = [
# ═══ 自举检查(优先级最高) ═══
("meta-manifest-coverage", "manifest 分组在 init 和 CHECKS 中的覆盖", check_meta_coverage),
# ═══ 交叉引用一致性 ═══
("manifest-skills-sync", "manifest vs init Skills 白名单", check_manifest_vs_init_skills),
("manifest-phase-sync", "manifest vs init 阶段数据文件", check_manifest_vs_init_phase_files),
("init-templates", "init 模板内容覆盖", check_init_templates_exist),
# ═══ 语义规则 ═══
("registry-types", "registry 类型与 phases 一致性", check_registry_types),
("phase-guard-all", "Phase Guard all 逻辑", check_phase_guard_all),
("frontmatter", "Skill frontmatter 完整性", check_skill_frontmatter),
]
def main():
# Windows GBK 编码兼容
import io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
if not MANIFEST_PATH.exists():
print("[FAIL] canonical-manifest.json 不存在,不是有效的 .opencode 模板")
sys.exit(1)
results = []
for check_id, check_name, check_fn in CHECKS:
try:
result = check_fn()
except Exception as e:
result = CheckResult(check_name, False, f"执行异常: {e}")
results.append(result)
passed = sum(1 for r in results if r.passed)
total = len(results)
print("=" * 60)
print(" SKILL 体系静态一致性检查 (Layer 1)")
print("=" * 60)
for r in results:
status = "[PASS]" if r.passed else "[FAIL]"
print(f"\n{status} {r.name}")
if r.detail:
for line in r.detail.split(";"):
print(f" {line.strip()}")
print(f"\n{'=' * 60}")
print(f" 结果: {passed}/{total} 通过")
if passed < total:
print(f" ({total - passed} 项需要修复)")
print(f"{'=' * 60}")
sys.exit(0 if passed == total else 1)
if __name__ == "__main__":
main()