LeMetaSkill v1.1.0:跨 runtime(ZCode/Claude Code/Codex)的 Agent Skill 创建与迭代工坊
- SKILL.md 双工作流(创建/迭代)+ 两道门(验料/通用化)+ 三档位 - 7 篇方法论 references + 3 个纯标准库脚本(scaffold/validate/trigger_eval) - 自身触发用例 16 条 + 先例研究/创建/迭代证据报告 - 方法论吸收自 qiaomu-meta-skill 与 luban-skill,取舍见 reports/prior-art.md
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
#!/usr/bin/env python3
|
||||
"""LeMetaSkill 起草脚手架:按档位生成"出生即合规"的 skill 骨架。
|
||||
|
||||
档位与产出:
|
||||
- draft 仅 SKILL.md(来自 assets/SKILL.template.md)
|
||||
- shared + evals/trigger_cases.json 用例模板 + README.md(三节起步)
|
||||
- rigorous + reports/ 证据目录约定
|
||||
|
||||
用法: python3 scaffold.py <name> "<一句话职责>" [--tier draft|shared|rigorous] [--target 目录]
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
TIERS = ("draft", "shared", "rigorous")
|
||||
|
||||
README_TEMPLATE = """# {name}
|
||||
|
||||
> {tagline}
|
||||
|
||||
**状态**:{tier} 档,个人/项目内使用。
|
||||
|
||||
## 它解决什么问题
|
||||
|
||||
【TODO:2-3 句,第二人称说清痛点和交付物】
|
||||
|
||||
## 触发方式
|
||||
|
||||
- 【TODO:用户真实会说的原话 3-8 条】
|
||||
|
||||
## 安全边界
|
||||
|
||||
【TODO:不会做什么、什么时候停下来问用户】
|
||||
"""
|
||||
|
||||
REPORTS_README = """# reports/ —— 运行证据
|
||||
|
||||
存放本 skill 的可复查证据,约定:
|
||||
|
||||
- `trigger-eval.json` trigger_eval.py 的输出(含手工补记的 live_turns 实测段)
|
||||
- `creation-handoff.md` 创建交付报告(rigorous 档)
|
||||
- `iteration-<日期>.md` 每轮迭代的改动报告与回炉清单
|
||||
|
||||
诚实条款:验证不了的写「证据缺失」,不编造;计划中的工作不是证据。
|
||||
"""
|
||||
|
||||
CASES_TEMPLATE = """{
|
||||
"_说明": "填入用户真实会说的话:应触发/不应触发/边界各若干条。family 标注场景族,便于回归分析。",
|
||||
"recommended_threshold": 0.3,
|
||||
"negative_patterns": ["不要做成 skill", "只要总结", "翻译一下"],
|
||||
"should_trigger": [
|
||||
{"text": "【TODO:用户原话】", "family": "cn_case_1"}
|
||||
],
|
||||
"should_not_trigger": [
|
||||
{"text": "【TODO:相邻但不该触发的原话】", "family": "near_task"}
|
||||
],
|
||||
"near_neighbor": []
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description="生成 skill 骨架")
|
||||
ap.add_argument("name")
|
||||
ap.add_argument("tagline")
|
||||
ap.add_argument("--tier", choices=TIERS, default="draft")
|
||||
ap.add_argument("--target", type=Path, default=None)
|
||||
args = ap.parse_args()
|
||||
|
||||
name = args.name.strip().lower()
|
||||
if not re.fullmatch(r"[a-z0-9]+(-[a-z0-9]+)*", name):
|
||||
print(f"✘ name 必须是 kebab-case(小写字母数字+连字符): {name!r}")
|
||||
return 1
|
||||
|
||||
target = (args.target or Path(".agents/skills") / name).resolve()
|
||||
if target.exists() and any(target.iterdir()):
|
||||
print(f"✘ 目标目录已存在且非空: {target}(不覆盖,确认后手动清理)")
|
||||
return 1
|
||||
target.mkdir(parents=True)
|
||||
|
||||
# 模板:优先读 assets/SKILL.template.md,缺失时用内置兜底
|
||||
template_path = Path(__file__).resolve().parent.parent / "assets" / "SKILL.template.md"
|
||||
if template_path.is_file():
|
||||
template = template_path.read_text(encoding="utf-8")
|
||||
else:
|
||||
template = (
|
||||
"---\nname: {name}\ndescription: |\n {tagline}\n 【TODO:做什么/何时触发,2-3 句 + 触发原话】\n"
|
||||
" 不要用于:【TODO:负触发,至少 2 条】。\n---\n\n# {name}\n\n【TODO:工作流正文】\n"
|
||||
)
|
||||
(target / "SKILL.md").write_text(
|
||||
template.replace("{{name}}", name).replace("{{tagline}}", args.tagline),
|
||||
encoding="utf-8",
|
||||
)
|
||||
print(f"✔ {target / 'SKILL.md'}")
|
||||
|
||||
if args.tier in ("shared", "rigorous"):
|
||||
cases_dir = target / "evals"
|
||||
cases_dir.mkdir(exist_ok=True)
|
||||
(cases_dir / "trigger_cases.json").write_text(CASES_TEMPLATE, encoding="utf-8")
|
||||
print(f"✔ {cases_dir / 'trigger_cases.json'}(待填真实原话)")
|
||||
(target / "README.md").write_text(
|
||||
README_TEMPLATE.format(name=name, tagline=args.tagline, tier=args.tier), encoding="utf-8"
|
||||
)
|
||||
print(f"✔ {target / 'README.md'}")
|
||||
|
||||
if args.tier == "rigorous":
|
||||
reports = target / "reports"
|
||||
reports.mkdir(exist_ok=True)
|
||||
(reports / "README.md").write_text(REPORTS_README, encoding="utf-8")
|
||||
print(f"✔ {reports / 'README.md'}")
|
||||
|
||||
print(
|
||||
f"\n下一步:\n"
|
||||
f"1. 填 TODO(grep -rn 'TODO' {target})——先写好 description 再写正文;\n"
|
||||
f"2. python3 scripts/validate_skill.py {target};\n"
|
||||
f"3. 填好用例后 python3 scripts/trigger_eval.py {target},再做真实对话实测。"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,237 @@
|
||||
#!/usr/bin/env python3
|
||||
"""LeMetaSkill 触发冒烟测试:词面检查 description 是否覆盖用户会说的原话。
|
||||
|
||||
这是**词面信号检查**,不是行为判断——它快速暴露"description 里没写用户
|
||||
会用的词"这类硬伤;真正的触发行为要靠真实对话实测(见 references/trigger-eval.md)。
|
||||
|
||||
做法:
|
||||
1. 解析 SKILL.md 的 description,与用例文件 positive_concepts 的概念族词表合并;
|
||||
2. 对每条测试原话做**概念族匹配**:负触发词命中直接判不触发;否则需命中全部
|
||||
required_families 且总命中族数 >= families_required(无概念族配置时退化为覆盖率阈值);
|
||||
3. 与期望比对,报告不一致条目;结果连同词面覆盖率写入 reports/trigger-eval.json。
|
||||
|
||||
用法: python3 trigger_eval.py <skill目录> [--cases 路径] [--output 路径] [--threshold 0.3]
|
||||
退出码: 0=全部一致 1=有不一致或 FAIL 2=缺少用例文件
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
|
||||
CJK = r"\u4e00-\u9fff"
|
||||
LATIN_WORD = re.compile(r"[a-z0-9]+")
|
||||
CJK_RUN = re.compile(rf"[{CJK}]+")
|
||||
|
||||
# 中文虚词/口语助词:不承重的触发信号,剔除后再匹配
|
||||
STOP_BIGRAMS = {
|
||||
"我们", "你们", "帮我", "帮忙", "一下", "这个", "那个", "一个", "一些",
|
||||
"然后", "可能", "可以", "应该", "就是", "还是", "什么", "怎么", "如果",
|
||||
"但是", "所以", "觉得", "感觉", "一下", "每次", "先不", "不用", "不要",
|
||||
"没有", "已经", "正在", "出来", "上去", "过去", "东西", "事情", "麻烦",
|
||||
}
|
||||
STOP_SINGLE = set("的了吧呢啊呀嘛么是有在和与我你他她它就把被这那也给从对上下里中要去会来能多很太都还再又就才只")
|
||||
|
||||
NEG_HINT = re.compile(r"不要|不该|不用于|排除|exclude|not for|do not use|don'?t use", re.I)
|
||||
|
||||
|
||||
def normalize(text: str) -> str:
|
||||
return re.sub(r"\s+", " ", text.lower().strip())
|
||||
|
||||
|
||||
def tokenize(text: str) -> set[str]:
|
||||
"""中文 → 滑动二元组(剔除虚词);英文/数字 → 单词。"""
|
||||
tokens: set[str] = set()
|
||||
for run in CJK_RUN.findall(text):
|
||||
if len(run) == 1:
|
||||
if run not in STOP_SINGLE:
|
||||
tokens.add(run)
|
||||
continue
|
||||
for i in range(len(run) - 1):
|
||||
bg = run[i : i + 2]
|
||||
if bg not in STOP_BIGRAMS:
|
||||
tokens.add(bg)
|
||||
for w in LATIN_WORD.findall(text):
|
||||
if len(w) > 1:
|
||||
tokens.add(w)
|
||||
return tokens
|
||||
|
||||
|
||||
def parse_description(skill_md: Path) -> tuple[str, dict[str, str]]:
|
||||
text = skill_md.read_text(encoding="utf-8")
|
||||
lines = text.splitlines()
|
||||
fields: dict[str, str] = {}
|
||||
if lines and lines[0].strip() == "---":
|
||||
key = ""
|
||||
for line in lines[1:60]:
|
||||
if line.strip() == "---":
|
||||
break
|
||||
m = re.match(r"^([A-Za-z_][\w-]*):\s*(.*)$", line)
|
||||
if m:
|
||||
key = m.group(1)
|
||||
fields[key] = m.group(2).strip().strip("'\"")
|
||||
elif line.startswith((" ", "\t")) and key:
|
||||
fields[key] = (fields[key] + " " + line.strip()).strip()
|
||||
return fields.get("description", ""), fields
|
||||
|
||||
|
||||
def coverage(case_text: str, desc_tokens: set[str]) -> tuple[float, list[str]]:
|
||||
case_tokens = tokenize(normalize(case_text))
|
||||
if not case_tokens:
|
||||
return 0.0, []
|
||||
hits = sorted(t for t in case_tokens if t in desc_tokens)
|
||||
return len(hits) / len(case_tokens), hits
|
||||
|
||||
|
||||
def phrase_in(phrase_norm: str, text_norm: str) -> bool:
|
||||
"""短语命中:含中文按子串,纯拉丁按整词边界。"""
|
||||
if re.search(rf"[{CJK}]", phrase_norm):
|
||||
return phrase_norm in text_norm
|
||||
return re.search(rf"\b{re.escape(phrase_norm)}\b", text_norm) is not None
|
||||
|
||||
|
||||
def family_hits(text_norm: str, concepts: dict[str, list[str]]) -> dict[str, str]:
|
||||
"""返回 {命中的族: 命中的短语}。"""
|
||||
hits: dict[str, str] = {}
|
||||
for family, phrases in concepts.items():
|
||||
for p in phrases:
|
||||
pn = normalize(p)
|
||||
if pn and phrase_in(pn, text_norm):
|
||||
hits[family] = p
|
||||
break
|
||||
return hits
|
||||
|
||||
|
||||
def load_cases(path: Path) -> dict:
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def as_items(raw) -> list[dict]:
|
||||
items = []
|
||||
for entry in raw or []:
|
||||
if isinstance(entry, str):
|
||||
items.append({"text": entry, "family": "default"})
|
||||
elif isinstance(entry, dict):
|
||||
it = dict(entry)
|
||||
it.setdefault("family", "default")
|
||||
items.append(it)
|
||||
return items
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description="触发冒烟测试")
|
||||
ap.add_argument("skill_dir", type=Path)
|
||||
ap.add_argument("--cases", type=Path, default=None)
|
||||
ap.add_argument("--output", type=Path, default=None)
|
||||
ap.add_argument("--threshold", type=float, default=None)
|
||||
args = ap.parse_args()
|
||||
|
||||
root = args.skill_dir.resolve()
|
||||
skill_md = root / "SKILL.md"
|
||||
if not skill_md.is_file():
|
||||
print(f"FAIL SKILL.md 不存在: {skill_md}")
|
||||
return 1
|
||||
|
||||
cases_path = (args.cases or root / "evals" / "trigger_cases.json").resolve()
|
||||
if not cases_path.is_file():
|
||||
cases_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
starter = {
|
||||
"_说明": "填入用户真实会说的话:应触发/不应触发/边界各若干条。family 标注场景族,便于回归分析。",
|
||||
"recommended_threshold": 0.3,
|
||||
"negative_patterns": ["不要做成 skill", "只要总结", "翻译一下"],
|
||||
"should_trigger": [
|
||||
{"text": "【TODO:用户原话】", "family": "cn_workflow"},
|
||||
],
|
||||
"should_not_trigger": [
|
||||
{"text": "【TODO:相邻但不该触发的原话】", "family": "near_task"},
|
||||
],
|
||||
"near_neighbor": [],
|
||||
}
|
||||
cases_path.write_text(json.dumps(starter, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
print(f"未找到用例文件,已生成模板: {cases_path}")
|
||||
print("请填入真实用例后重跑。")
|
||||
return 2
|
||||
|
||||
cases = load_cases(cases_path)
|
||||
desc, _fields = parse_description(skill_md)
|
||||
if not desc:
|
||||
print("FAIL description 为空,触发信号不存在")
|
||||
return 1
|
||||
if not NEG_HINT.search(desc):
|
||||
print("WARN description 缺负触发表述,不应触发的判定仅靠用例负词兜底")
|
||||
|
||||
threshold = args.threshold if args.threshold is not None else float(cases.get("recommended_threshold", 0.3))
|
||||
desc_tokens = tokenize(normalize(desc))
|
||||
# positive_concepts: {族: [词]},视作 description 的延伸词面
|
||||
concepts = cases.get("positive_concepts")
|
||||
if isinstance(concepts, dict):
|
||||
for words in concepts.values():
|
||||
for w in words:
|
||||
desc_tokens |= tokenize(normalize(w))
|
||||
|
||||
neg_patterns = [normalize(p) for p in cases.get("negative_patterns", [])]
|
||||
concepts = cases.get("positive_concepts") if isinstance(cases.get("positive_concepts"), dict) else {}
|
||||
required_families = set(cases.get("required_families", []))
|
||||
families_required = int(cases.get("families_required", 2))
|
||||
use_families = bool(concepts)
|
||||
report: dict = {
|
||||
"skill": root.name,
|
||||
"date": str(date.today()),
|
||||
"threshold": threshold,
|
||||
"prediction_mode": "concept_families" if use_families else "coverage_fallback",
|
||||
"note": "词面冒烟测试;行为结论需真实对话实测(live_turns 字段手工补记)",
|
||||
"cases": [],
|
||||
}
|
||||
|
||||
n_mismatch = n_total = 0
|
||||
for bucket, expected in (("should_trigger", True), ("should_not_trigger", False), ("near_neighbor", None)):
|
||||
for item in as_items(cases.get(bucket)):
|
||||
text = normalize(item["text"])
|
||||
hit_neg = any(p and p in text for p in neg_patterns)
|
||||
cov, cov_hits = coverage(item["text"], desc_tokens)
|
||||
fams = family_hits(text, concepts)
|
||||
if use_families:
|
||||
predicted = (not hit_neg) and required_families <= set(fams) and len(fams) >= families_required
|
||||
basis = f"fams={','.join(sorted(fams)) or '-'}"
|
||||
else:
|
||||
predicted = (not hit_neg) and cov >= threshold
|
||||
basis = f"cov={cov:.2f}"
|
||||
if expected is None:
|
||||
verdict = "boundary"
|
||||
entry_verdict = "边界条目(不计分)"
|
||||
else:
|
||||
ok_ = predicted == expected
|
||||
n_total += 1
|
||||
n_mismatch += 0 if ok_ else 1
|
||||
verdict = "pass" if ok_ else "MISMATCH"
|
||||
entry_verdict = ("一致" if ok_ else "不一致") + f"(预测={'触发' if predicted else '不触发'},期望={'触发' if expected else '不触发'})"
|
||||
flag = "" if verdict in ("pass", "boundary") else " <-- 需修 description"
|
||||
print(f"[{verdict:>9}] {item['text'][:48]} {basis} 负词命中={hit_neg} {entry_verdict}{flag}")
|
||||
report["cases"].append({
|
||||
"text": item["text"], "family": item["family"], "bucket": bucket,
|
||||
"coverage": round(cov, 3), "coverage_hits": cov_hits,
|
||||
"families_hit": sorted(fams),
|
||||
"negative_hit": hit_neg, "predicted_trigger": predicted,
|
||||
"expected_trigger": expected, "verdict": verdict,
|
||||
})
|
||||
|
||||
report["summary"] = {"scored": n_total, "mismatch": n_mismatch,
|
||||
"pass_rate": round((n_total - n_mismatch) / n_total, 3) if n_total else None}
|
||||
out = args.output or (root / "reports" / "trigger-eval.json")
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
out.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
|
||||
print(f"\n计分条目 {n_total},不一致 {n_mismatch};报告已写入 {out}")
|
||||
if n_mismatch:
|
||||
print("结论:description 与用户原话存在词面缺口,先补触发短语/负触发再扩正文。")
|
||||
return 1
|
||||
print("结论:冒烟全绿。仍需真实对话实测后方可声称[已验证]。")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,194 @@
|
||||
#!/usr/bin/env python3
|
||||
"""LeMetaSkill 结构体检:检查一个 skill 目录是否满足基本契约。
|
||||
|
||||
只依赖标准库。检查项(FAIL 阻断 / WARN 提醒):
|
||||
- SKILL.md 存在、frontmatter 可解析 [FAIL]
|
||||
- name 存在、kebab-case、1-64 字符、与目录名一致 [FAIL]
|
||||
- description 存在且够长(>=30 字符) [WARN]
|
||||
- description 含负触发信号("不要用于"类表述) [WARN]
|
||||
- 正文非空、<=500 行 [WARN]
|
||||
- 正文中的本地 .md 链接可达(无悬空引用) [FAIL]
|
||||
- evals/*.json 可解析 [FAIL]
|
||||
- 无密钥/私有绝对路径 [WARN]
|
||||
- 顶层目录都是已知用途 [WARN]
|
||||
|
||||
用法: python3 validate_skill.py <skill目录>
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
KNOWN_DIRS = {"references", "scripts", "evals", "assets", "reports", "examples", "tests"}
|
||||
MAX_BODY_LINES = 500
|
||||
SECRET_PATTERNS = [
|
||||
r"AKIA[0-9A-Z]{16}",
|
||||
r"sk-[A-Za-z0-9]{20,}",
|
||||
r"(?:api[_-]?key|token|secret|password)\s*[:=]\s*['\"]?[A-Za-z0-9_\-]{16,}",
|
||||
r"ghp_[A-Za-z0-9]{36}",
|
||||
]
|
||||
PRIVATE_PATH = "/Users" + "/[A-Za-z0-9_.\\-]+/" # 拼接写法:避免扫描器匹配到自身源码
|
||||
NEGATIVE_HINT = re.compile(r"不要|不该|不用于|排除|exclude|not for|don'?t use|do not use", re.I)
|
||||
MD_LINK = re.compile(r"\[[^\]]*\]\(([^)\s]+)\)")
|
||||
|
||||
|
||||
def parse_frontmatter(text: str) -> tuple[dict[str, str], int]:
|
||||
"""解析简易 YAML frontmatter(key: value 与 key: | 块),返回 (字段, 正文起始行号)。"""
|
||||
lines = text.splitlines()
|
||||
if not lines or lines[0].strip() != "---":
|
||||
return {}, 0
|
||||
fields: dict[str, str] = {}
|
||||
key = ""
|
||||
for i in range(1, min(len(lines), 60)):
|
||||
line = lines[i]
|
||||
if line.strip() == "---":
|
||||
return fields, i + 1
|
||||
m = re.match(r"^([A-Za-z_][\w-]*):\s*(.*)$", line)
|
||||
if m:
|
||||
key = m.group(1)
|
||||
fields[key] = m.group(2).strip().strip("'\"")
|
||||
elif line.startswith((" ", "\t")) and key:
|
||||
fields[key] = (fields[key] + " " + line.strip().strip("'\"")).strip()
|
||||
return {}, 0 # 没有闭合的 ---
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description="skill 结构体检")
|
||||
ap.add_argument("skill_dir", type=Path)
|
||||
args = ap.parse_args()
|
||||
root: Path = args.skill_dir.resolve()
|
||||
if not root.is_dir():
|
||||
print(f"FAIL 目录不存在: {root}")
|
||||
return 1
|
||||
|
||||
n_pass = n_warn = n_fail = 0
|
||||
|
||||
def ok(msg: str) -> None:
|
||||
nonlocal n_pass
|
||||
n_pass += 1
|
||||
print(f"PASS {msg}")
|
||||
|
||||
def warn(msg: str) -> None:
|
||||
nonlocal n_warn
|
||||
n_warn += 1
|
||||
print(f"WARN {msg}")
|
||||
|
||||
def fail(msg: str) -> None:
|
||||
nonlocal n_fail
|
||||
n_fail += 1
|
||||
print(f"FAIL {msg}")
|
||||
|
||||
skill_md = root / "SKILL.md"
|
||||
if not skill_md.is_file():
|
||||
fail("SKILL.md 不存在")
|
||||
print(f"\n--- Summary --- PASS:{n_pass} WARN:{n_warn} FAIL:{n_fail}")
|
||||
return 1
|
||||
ok("SKILL.md 存在")
|
||||
|
||||
text = skill_md.read_text(encoding="utf-8")
|
||||
fields, body_start = parse_frontmatter(text)
|
||||
if not fields:
|
||||
fail("frontmatter 缺失或未闭合(需要 --- 包裹的头)")
|
||||
else:
|
||||
ok("frontmatter 可解析")
|
||||
|
||||
name = fields.get("name", "")
|
||||
if not name:
|
||||
fail("缺少 name 字段")
|
||||
else:
|
||||
if not re.fullmatch(r"[a-z0-9]+(-[a-z0-9]+)*", name):
|
||||
fail(f"name 不是 kebab-case: {name!r}")
|
||||
elif not 1 <= len(name) <= 64:
|
||||
fail(f"name 长度超出 1-64: {len(name)}")
|
||||
else:
|
||||
ok(f"name 合法: {name}")
|
||||
if root.name != name:
|
||||
fail(f"name 与目录名不一致: {name!r} vs {root.name!r}")
|
||||
else:
|
||||
ok("name 与目录名一致")
|
||||
|
||||
desc = fields.get("description", "")
|
||||
if not desc:
|
||||
fail("缺少 description 字段(触发信号,必填)")
|
||||
else:
|
||||
if len(desc) < 30:
|
||||
warn(f"description 太短({len(desc)} 字符),盖不住触发场景")
|
||||
else:
|
||||
ok(f"description 长度充分({len(desc)} 字符)")
|
||||
if NEGATIVE_HINT.search(desc):
|
||||
ok("description 含负触发信号")
|
||||
else:
|
||||
warn("description 缺负触发(\"不要用于…\"),边界请求会犹豫")
|
||||
|
||||
body = "\n".join(text.splitlines()[body_start:]).strip()
|
||||
if not body:
|
||||
fail("正文为空")
|
||||
else:
|
||||
n_lines = len(body.splitlines())
|
||||
if n_lines > MAX_BODY_LINES:
|
||||
warn(f"正文 {n_lines} 行,超过 {MAX_BODY_LINES} 行预算——考虑拆进 references/")
|
||||
else:
|
||||
ok(f"正文 {n_lines} 行,在预算内")
|
||||
|
||||
# 悬空引用
|
||||
dangling = []
|
||||
for target in MD_LINK.findall(body):
|
||||
if target.startswith(("http://", "https://", "mailto:", "#")):
|
||||
continue
|
||||
if not (root / target).exists():
|
||||
dangling.append(target)
|
||||
if dangling:
|
||||
fail(f"正文引用的本地文件不存在: {', '.join(sorted(set(dangling)))}")
|
||||
elif MD_LINK.search(body):
|
||||
ok("正文本地链接全部可达")
|
||||
|
||||
# evals JSON
|
||||
evals_dir = root / "evals"
|
||||
if evals_dir.is_dir():
|
||||
for j in sorted(evals_dir.glob("*.json")):
|
||||
try:
|
||||
json.loads(j.read_text(encoding="utf-8"))
|
||||
ok(f"{j.relative_to(root)} 可解析")
|
||||
except json.JSONDecodeError as e:
|
||||
fail(f"{j.relative_to(root)} JSON 解析失败: {e}")
|
||||
|
||||
# 密钥与私有路径(只读文本类文件)
|
||||
for p in sorted(root.rglob("*")):
|
||||
if not p.is_file() or p.suffix not in {".md", ".json", ".py", ".sh", ".txt", ".yaml", ".yml"}:
|
||||
continue
|
||||
try:
|
||||
content = p.read_text(encoding="utf-8")
|
||||
except (UnicodeDecodeError, PermissionError):
|
||||
continue
|
||||
rel = p.relative_to(root)
|
||||
for pat in SECRET_PATTERNS:
|
||||
if re.search(pat, content):
|
||||
warn(f"{rel} 疑似包含密钥/令牌(模式 {pat})")
|
||||
if rel.name != "SKILL.md" and re.search(PRIVATE_PATH, content):
|
||||
warn(f"{rel} 含私有绝对路径(/Users/…)")
|
||||
|
||||
# 顶层结构
|
||||
unknown = [d.name for d in root.iterdir() if d.is_dir() and d.name not in KNOWN_DIRS]
|
||||
if unknown:
|
||||
warn(f"未知顶层目录(确认其用途,或移除): {', '.join(unknown)}")
|
||||
present = [d.name for d in root.iterdir() if d.is_dir() and d.name in KNOWN_DIRS]
|
||||
if present:
|
||||
ok(f"标准目录: {', '.join(sorted(present))}")
|
||||
|
||||
print(f"\n--- Summary --- PASS:{n_pass} WARN:{n_warn} FAIL:{n_fail}")
|
||||
if n_fail:
|
||||
print("结论:未过体检,先修 FAIL 再交付。")
|
||||
return 1
|
||||
if n_warn:
|
||||
print("结论:可交付,WARN 项酌情处理。")
|
||||
else:
|
||||
print("结论:全绿。")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user