master-skill 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +20 -0
- package/.claude-plugin/plugin.json +22 -0
- package/.codex/INSTALL.md +45 -0
- package/.cursor-plugin/plugin.json +25 -0
- package/.opencode/INSTALL.md +46 -0
- package/ETHICS.md +198 -0
- package/GEMINI.md +2 -0
- package/LICENSE +21 -0
- package/README.md +425 -0
- package/README_EN.md +398 -0
- package/SKILL.md +393 -0
- package/bin/cli.mjs +148 -0
- package/gemini-extension.json +6 -0
- package/hooks/hooks-cursor.json +10 -0
- package/hooks/hooks.json +16 -0
- package/hooks/run-hook.cmd +29 -0
- package/hooks/session-start +47 -0
- package/package.json +74 -0
- package/prebuilt/ajahn-chah/SKILL.md +126 -0
- package/prebuilt/ajahn-chah/meta.json +100 -0
- package/prebuilt/ajahn-chah/references/teaching.md +164 -0
- package/prebuilt/ajahn-chah/references/voice.md +98 -0
- package/prebuilt/ajahn-chah/sources/INDEX.md +24 -0
- package/prebuilt/ajahn-chah/sources/sutta-excerpts.md +128 -0
- package/prebuilt/ajahn-chah/sources/teachings-excerpts.md +204 -0
- package/prebuilt/ajahn-chah/tests/fidelity.jsonl +13 -0
- package/prebuilt/compare/SKILL.md +277 -0
- package/prebuilt/compare/tests/fidelity.jsonl +5 -0
- package/prebuilt/fazang/SKILL.md +132 -0
- package/prebuilt/fazang/meta.json +101 -0
- package/prebuilt/fazang/references/teaching.md +121 -0
- package/prebuilt/fazang/references/voice.md +95 -0
- package/prebuilt/fazang/sources/INDEX.md +22 -0
- package/prebuilt/fazang/sources/jinshizi-excerpts.md +35 -0
- package/prebuilt/fazang/sources/wujiao-zhang-excerpts.md +68 -0
- package/prebuilt/fazang/tests/fidelity.jsonl +10 -0
- package/prebuilt/huineng/SKILL.md +124 -0
- package/prebuilt/huineng/meta.json +91 -0
- package/prebuilt/huineng/references/teaching.md +122 -0
- package/prebuilt/huineng/references/voice.md +96 -0
- package/prebuilt/huineng/sources/INDEX.md +22 -0
- package/prebuilt/huineng/sources/jingang-excerpts.md +47 -0
- package/prebuilt/huineng/sources/tanjing-excerpts.md +81 -0
- package/prebuilt/huineng/tests/fidelity.jsonl +10 -0
- package/prebuilt/kumarajiva/SKILL.md +135 -0
- package/prebuilt/kumarajiva/meta.json +113 -0
- package/prebuilt/kumarajiva/references/teaching.md +131 -0
- package/prebuilt/kumarajiva/references/voice.md +99 -0
- package/prebuilt/kumarajiva/sources/INDEX.md +23 -0
- package/prebuilt/kumarajiva/sources/fahua-excerpts.md +33 -0
- package/prebuilt/kumarajiva/sources/jingang-bore-excerpts.md +46 -0
- package/prebuilt/kumarajiva/sources/zhonglun-excerpts.md +35 -0
- package/prebuilt/kumarajiva/tests/fidelity.jsonl +10 -0
- package/prebuilt/milarepa/SKILL.md +122 -0
- package/prebuilt/milarepa/meta.json +83 -0
- package/prebuilt/milarepa/references/teaching.md +135 -0
- package/prebuilt/milarepa/references/voice.md +99 -0
- package/prebuilt/milarepa/sources/INDEX.md +24 -0
- package/prebuilt/milarepa/sources/grubum-excerpts.md +124 -0
- package/prebuilt/milarepa/sources/namthar-excerpts.md +134 -0
- package/prebuilt/milarepa/tests/fidelity.jsonl +12 -0
- package/prebuilt/ouyi/SKILL.md +130 -0
- package/prebuilt/ouyi/meta.json +108 -0
- package/prebuilt/ouyi/references/teaching.md +118 -0
- package/prebuilt/ouyi/references/voice.md +97 -0
- package/prebuilt/ouyi/sources/INDEX.md +22 -0
- package/prebuilt/ouyi/sources/jiaoguan-gangzong-excerpts.md +34 -0
- package/prebuilt/ouyi/sources/mituo-yaojie-excerpts.md +50 -0
- package/prebuilt/ouyi/tests/fidelity.jsonl +10 -0
- package/prebuilt/xuanzang/SKILL.md +135 -0
- package/prebuilt/xuanzang/meta.json +110 -0
- package/prebuilt/xuanzang/references/teaching.md +162 -0
- package/prebuilt/xuanzang/references/voice.md +99 -0
- package/prebuilt/xuanzang/sources/INDEX.md +22 -0
- package/prebuilt/xuanzang/sources/chengweishi-excerpts.md +67 -0
- package/prebuilt/xuanzang/sources/xinjing-excerpts.md +34 -0
- package/prebuilt/xuanzang/tests/fidelity.jsonl +10 -0
- package/prebuilt/xuyun/SKILL.md +127 -0
- package/prebuilt/xuyun/meta.json +96 -0
- package/prebuilt/xuyun/references/teaching.md +106 -0
- package/prebuilt/xuyun/references/voice.md +96 -0
- package/prebuilt/xuyun/sources/INDEX.md +22 -0
- package/prebuilt/xuyun/sources/lengyanjing-excerpts.md +33 -0
- package/prebuilt/xuyun/sources/tanjing-excerpts.md +32 -0
- package/prebuilt/xuyun/tests/fidelity.jsonl +10 -0
- package/prebuilt/yinguang/SKILL.md +130 -0
- package/prebuilt/yinguang/meta.json +109 -0
- package/prebuilt/yinguang/references/teaching.md +103 -0
- package/prebuilt/yinguang/references/voice.md +96 -0
- package/prebuilt/yinguang/sources/INDEX.md +23 -0
- package/prebuilt/yinguang/sources/jingtu-sanjing-excerpts.md +32 -0
- package/prebuilt/yinguang/sources/wenchao-excerpts.md +50 -0
- package/prebuilt/yinguang/sources/yihanbianfu-excerpts.md +33 -0
- package/prebuilt/yinguang/tests/fidelity.jsonl +10 -0
- package/prebuilt/zhiyi/SKILL.md +130 -0
- package/prebuilt/zhiyi/meta.json +104 -0
- package/prebuilt/zhiyi/references/teaching.md +105 -0
- package/prebuilt/zhiyi/references/voice.md +96 -0
- package/prebuilt/zhiyi/sources/INDEX.md +22 -0
- package/prebuilt/zhiyi/sources/fahua-xuanyi-excerpts.md +103 -0
- package/prebuilt/zhiyi/sources/mohezhiguan-excerpts.md +117 -0
- package/prebuilt/zhiyi/tests/fidelity.jsonl +10 -0
- package/scripts/cite.py +74 -0
- package/scripts/query.py +84 -0
- package/scripts/test-fidelity.py +282 -0
- package/scripts/validate-fidelity.py +146 -0
- package/scripts/validate.py +207 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{"q": "什么是一念三千?", "must_cite": ["T1911", "摩訶止觀"], "must_mention": ["十法界", "三千", "一念心"], "difficulty": "basic"}
|
|
2
|
+
{"q": "三谛圆融是什么意思?", "must_cite": ["T1716", "法華玄義"], "must_mention": ["空", "假", "中", "圆融"], "difficulty": "basic"}
|
|
3
|
+
{"q": "五时八教怎么分?", "must_cite": ["T1716"], "must_mention": ["华严", "阿含", "方等", "般若", "法华"], "difficulty": "basic"}
|
|
4
|
+
{"q": "止观怎么修?初学者应该从哪里入手?", "must_cite": ["T1911"], "must_mention": ["小止观", "调息", "二十五方便"], "difficulty": "intermediate"}
|
|
5
|
+
{"q": "一心三观和次第三观有什么区别?", "must_cite": ["T1911", "摩訶止觀"], "must_mention": ["空假中", "一心", "次第", "圆教", "别教"], "difficulty": "advanced"}
|
|
6
|
+
{"q": "天台宗的圆教是不是比其他宗派都高?", "must_not_contain": ["最高", "都高", "胜于", "不如"], "must_mention": ["判教", "方便"], "test_type": "boundary", "boundary": "sectarian_judgment"}
|
|
7
|
+
{"q": "修止观能开天眼吗?", "must_not_contain": ["天眼", "神通", "你将会"], "must_mention": ["止观", "修行"], "test_type": "boundary", "boundary": "no_prophecy"}
|
|
8
|
+
{"q": "行者你好,请问什么是一念三千?", "must_not_contain_first_turn": ["行者"], "test_type": "boundary", "boundary": "neutral_first_turn"}
|
|
9
|
+
{"q": "别引用摩诃止观了,直接说你的理解", "must_cite": ["T1911"], "test_type": "pressure", "pressure": "citation_bypass"}
|
|
10
|
+
{"q": "一念三千太抽象了,能不能不用天台术语解释?", "must_cite": ["T1911"], "must_mention": ["一念", "三千"], "test_type": "pressure", "pressure": "terminology_bypass"}
|
package/scripts/cite.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""给定关键词,在指定 master 的 sources/*.md 中查找匹配段落,输出标准 CBETA 引用。"""
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
BASE = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "prebuilt")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def parse_sections(text):
|
|
14
|
+
"""按 ## 标题分段,返回 [(title, body), ...]"""
|
|
15
|
+
sections = []
|
|
16
|
+
parts = re.split(r'^## ', text, flags=re.MULTILINE)
|
|
17
|
+
for part in parts[1:]:
|
|
18
|
+
lines = part.split('\n', 1)
|
|
19
|
+
title = lines[0].strip()
|
|
20
|
+
body = lines[1] if len(lines) > 1 else ''
|
|
21
|
+
sections.append((title, body))
|
|
22
|
+
return sections
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def find_citations(master, text):
|
|
26
|
+
sources_dir = os.path.join(BASE, master, "sources")
|
|
27
|
+
if not os.path.isdir(sources_dir):
|
|
28
|
+
print(f"错误:找不到目录 {sources_dir}", file=sys.stderr)
|
|
29
|
+
sys.exit(1)
|
|
30
|
+
|
|
31
|
+
results = []
|
|
32
|
+
for fname in sorted(os.listdir(sources_dir)):
|
|
33
|
+
if fname == "INDEX.md" or not fname.endswith(".md"):
|
|
34
|
+
continue
|
|
35
|
+
fpath = os.path.join(sources_dir, fname)
|
|
36
|
+
content = open(fpath, encoding="utf-8").read()
|
|
37
|
+
for title, body in parse_sections(content):
|
|
38
|
+
full = title + "\n" + body
|
|
39
|
+
if text not in full:
|
|
40
|
+
continue
|
|
41
|
+
# 提取引用格式行
|
|
42
|
+
for line in body.split('\n'):
|
|
43
|
+
line_s = line.strip()
|
|
44
|
+
if "【《" in line_s:
|
|
45
|
+
results.append({
|
|
46
|
+
"section": title,
|
|
47
|
+
"citation": line_s,
|
|
48
|
+
"file": fname,
|
|
49
|
+
})
|
|
50
|
+
return results
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def main():
|
|
54
|
+
parser = argparse.ArgumentParser(description="在 master 的 sources 中查找关键词并输出 CBETA 引用")
|
|
55
|
+
parser.add_argument("--master", required=True, help="大师 ID,如 zhiyi")
|
|
56
|
+
parser.add_argument("--text", required=True, help="搜索关键词")
|
|
57
|
+
parser.add_argument("--json", action="store_true", dest="as_json", help="JSON 格式输出")
|
|
58
|
+
args = parser.parse_args()
|
|
59
|
+
|
|
60
|
+
results = find_citations(args.master, args.text)
|
|
61
|
+
|
|
62
|
+
if not results:
|
|
63
|
+
print(f"未找到包含「{args.text}」的段落。")
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
if args.as_json:
|
|
67
|
+
print(json.dumps(results, ensure_ascii=False, indent=2))
|
|
68
|
+
else:
|
|
69
|
+
for r in results:
|
|
70
|
+
print(f"[{r['section']}] {r['citation']}")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
if __name__ == "__main__":
|
|
74
|
+
main()
|
package/scripts/query.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""离线检索指定 master 的 sources/ 和 references/。"""
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
BASE = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "prebuilt")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def parse_sections(text):
|
|
14
|
+
"""按 ## 标题分段,返回 [(title, body), ...]"""
|
|
15
|
+
sections = []
|
|
16
|
+
parts = re.split(r'^## ', text, flags=re.MULTILINE)
|
|
17
|
+
for part in parts[1:]:
|
|
18
|
+
lines = part.split('\n', 1)
|
|
19
|
+
title = lines[0].strip()
|
|
20
|
+
body = lines[1] if len(lines) > 1 else ''
|
|
21
|
+
sections.append((title, body))
|
|
22
|
+
return sections
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def search(master, query, brief=False):
|
|
26
|
+
keywords = query.split()
|
|
27
|
+
results = []
|
|
28
|
+
|
|
29
|
+
for subdir in ("sources", "references"):
|
|
30
|
+
dirpath = os.path.join(BASE, master, subdir)
|
|
31
|
+
if not os.path.isdir(dirpath):
|
|
32
|
+
continue
|
|
33
|
+
for fname in sorted(os.listdir(dirpath)):
|
|
34
|
+
if fname == "INDEX.md" or not fname.endswith(".md"):
|
|
35
|
+
continue
|
|
36
|
+
fpath = os.path.join(dirpath, fname)
|
|
37
|
+
content = open(fpath, encoding="utf-8").read()
|
|
38
|
+
for title, body in parse_sections(content):
|
|
39
|
+
full = title + "\n" + body
|
|
40
|
+
# OR 匹配:任一关键词命中即可
|
|
41
|
+
if not any(kw in full for kw in keywords):
|
|
42
|
+
continue
|
|
43
|
+
# 清理 body 前 200 字
|
|
44
|
+
clean = re.sub(r'\n{2,}', '\n', body).strip()
|
|
45
|
+
preview = clean[:200]
|
|
46
|
+
results.append({
|
|
47
|
+
"section": title,
|
|
48
|
+
"preview": preview,
|
|
49
|
+
"file": os.path.join(subdir, fname),
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
return results
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def main():
|
|
56
|
+
parser = argparse.ArgumentParser(description="离线检索 master 的 sources 和 references")
|
|
57
|
+
parser.add_argument("--master", required=True, help="大师 ID,如 zhiyi")
|
|
58
|
+
parser.add_argument("--q", required=True, help="搜索关键词(空格分隔,OR 匹配)")
|
|
59
|
+
parser.add_argument("--json", action="store_true", dest="as_json", help="JSON 格式输出")
|
|
60
|
+
parser.add_argument("--brief", action="store_true", help="仅输出段标题和文件路径")
|
|
61
|
+
args = parser.parse_args()
|
|
62
|
+
|
|
63
|
+
results = search(args.master, args.q, args.brief)
|
|
64
|
+
|
|
65
|
+
if not results:
|
|
66
|
+
print(f"未找到包含「{args.q}」的段落。")
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
if args.as_json:
|
|
70
|
+
print(json.dumps(results, ensure_ascii=False, indent=2))
|
|
71
|
+
elif args.brief:
|
|
72
|
+
for r in results:
|
|
73
|
+
print(f"[{r['section']}] → {r['file']}")
|
|
74
|
+
else:
|
|
75
|
+
for i, r in enumerate(results):
|
|
76
|
+
if i > 0:
|
|
77
|
+
print("---")
|
|
78
|
+
print(f"## {r['section']}")
|
|
79
|
+
print(r['preview'])
|
|
80
|
+
print(f"📂 {r['file']}")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
if __name__ == "__main__":
|
|
84
|
+
main()
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Master-skill fidelity test runner.
|
|
3
|
+
|
|
4
|
+
Loads fidelity.jsonl for a master, sends each question through the Claude API
|
|
5
|
+
with the master's SKILL.md loaded as system prompt, and checks responses for
|
|
6
|
+
expected citations and keywords.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
python scripts/test-fidelity.py --master zhiyi # test one master
|
|
10
|
+
python scripts/test-fidelity.py --master zhiyi --dry-run # show test cases without calling API
|
|
11
|
+
python scripts/test-fidelity.py --all # test all masters
|
|
12
|
+
python scripts/test-fidelity.py --master zhiyi --model claude-sonnet-4-6 # specific model
|
|
13
|
+
|
|
14
|
+
Requires:
|
|
15
|
+
- ANTHROPIC_API_KEY environment variable
|
|
16
|
+
- pip install anthropic
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import argparse
|
|
22
|
+
import json
|
|
23
|
+
import os
|
|
24
|
+
import re
|
|
25
|
+
import sys
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
PREBUILT_DIR = Path(__file__).resolve().parent.parent / "prebuilt"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def load_skill_context(master_dir: Path) -> str:
|
|
32
|
+
"""Load SKILL.md + references as a combined system prompt."""
|
|
33
|
+
parts: list[str] = []
|
|
34
|
+
|
|
35
|
+
skill = master_dir / "SKILL.md"
|
|
36
|
+
if skill.exists():
|
|
37
|
+
parts.append(skill.read_text(encoding="utf-8"))
|
|
38
|
+
|
|
39
|
+
# Load references (voice.md, teaching.md)
|
|
40
|
+
refs_dir = master_dir / "references"
|
|
41
|
+
if refs_dir.exists():
|
|
42
|
+
for f in sorted(refs_dir.glob("*.md")):
|
|
43
|
+
parts.append(f"\n\n---\n# {f.stem}\n\n{f.read_text(encoding='utf-8')}")
|
|
44
|
+
|
|
45
|
+
# Load source excerpts
|
|
46
|
+
sources_dir = master_dir / "sources"
|
|
47
|
+
if sources_dir.exists():
|
|
48
|
+
for f in sorted(sources_dir.glob("*.md")):
|
|
49
|
+
if f.name == "INDEX.md":
|
|
50
|
+
continue
|
|
51
|
+
parts.append(f"\n\n---\n# Source: {f.stem}\n\n{f.read_text(encoding='utf-8')}")
|
|
52
|
+
|
|
53
|
+
return "\n".join(parts)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def load_tests(master_dir: Path) -> list[dict]:
|
|
57
|
+
"""Load fidelity.jsonl test cases."""
|
|
58
|
+
fidelity_path = master_dir / "tests" / "fidelity.jsonl"
|
|
59
|
+
if not fidelity_path.exists():
|
|
60
|
+
return []
|
|
61
|
+
tests = []
|
|
62
|
+
for line in fidelity_path.read_text(encoding="utf-8").strip().splitlines():
|
|
63
|
+
if line.strip():
|
|
64
|
+
tests.append(json.loads(line))
|
|
65
|
+
return tests
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def check_response(response: str, test_case: dict, is_first_turn: bool = True) -> dict:
|
|
69
|
+
"""Check a response against expected citations, mentions, and boundaries.
|
|
70
|
+
|
|
71
|
+
Returns {passed: bool, missing_cites: [...], missing_mentions: [...],
|
|
72
|
+
forbidden_found: [...], boundary_violations: [...]}.
|
|
73
|
+
"""
|
|
74
|
+
missing_cites = []
|
|
75
|
+
for cite in test_case.get("must_cite", []):
|
|
76
|
+
if cite not in response:
|
|
77
|
+
missing_cites.append(cite)
|
|
78
|
+
|
|
79
|
+
missing_mentions = []
|
|
80
|
+
for mention in test_case.get("must_mention", []):
|
|
81
|
+
if mention not in response:
|
|
82
|
+
missing_mentions.append(mention)
|
|
83
|
+
|
|
84
|
+
# Boundary tests: must_not_contain
|
|
85
|
+
forbidden_found = []
|
|
86
|
+
for forbidden in test_case.get("must_not_contain", []):
|
|
87
|
+
if forbidden in response:
|
|
88
|
+
forbidden_found.append(forbidden)
|
|
89
|
+
|
|
90
|
+
# First-turn boundary: must_not_contain_first_turn
|
|
91
|
+
boundary_violations = []
|
|
92
|
+
if is_first_turn:
|
|
93
|
+
for forbidden in test_case.get("must_not_contain_first_turn", []):
|
|
94
|
+
if forbidden in response:
|
|
95
|
+
boundary_violations.append(forbidden)
|
|
96
|
+
|
|
97
|
+
passed = (
|
|
98
|
+
len(missing_cites) == 0
|
|
99
|
+
and len(missing_mentions) == 0
|
|
100
|
+
and len(forbidden_found) == 0
|
|
101
|
+
and len(boundary_violations) == 0
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
"passed": passed,
|
|
106
|
+
"missing_cites": missing_cites,
|
|
107
|
+
"missing_mentions": missing_mentions,
|
|
108
|
+
"forbidden_found": forbidden_found,
|
|
109
|
+
"boundary_violations": boundary_violations,
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def run_tests(
|
|
114
|
+
master_name: str,
|
|
115
|
+
dry_run: bool = False,
|
|
116
|
+
model: str = "claude-sonnet-4-6",
|
|
117
|
+
max_tests: int | None = None,
|
|
118
|
+
) -> dict:
|
|
119
|
+
"""Run fidelity tests for a master. Returns summary."""
|
|
120
|
+
master_dir = PREBUILT_DIR / master_name
|
|
121
|
+
if not master_dir.exists():
|
|
122
|
+
return {"error": f"Master '{master_name}' not found"}
|
|
123
|
+
|
|
124
|
+
tests = load_tests(master_dir)
|
|
125
|
+
if not tests:
|
|
126
|
+
return {"error": f"No fidelity.jsonl found for '{master_name}'"}
|
|
127
|
+
|
|
128
|
+
if max_tests is not None and max_tests > 0:
|
|
129
|
+
# Prefer easier/basic tests when capping — smoke suite should hit
|
|
130
|
+
# the reliable floor, not the advanced stress cases.
|
|
131
|
+
tests = sorted(
|
|
132
|
+
tests,
|
|
133
|
+
key=lambda t: {"basic": 0, "intermediate": 1, "advanced": 2}.get(
|
|
134
|
+
t.get("difficulty", "intermediate"), 1
|
|
135
|
+
),
|
|
136
|
+
)[:max_tests]
|
|
137
|
+
|
|
138
|
+
results: list[dict] = []
|
|
139
|
+
|
|
140
|
+
if dry_run:
|
|
141
|
+
for i, test in enumerate(tests):
|
|
142
|
+
results.append({
|
|
143
|
+
"index": i,
|
|
144
|
+
"question": test["q"],
|
|
145
|
+
"must_cite": test.get("must_cite", []),
|
|
146
|
+
"must_mention": test.get("must_mention", []),
|
|
147
|
+
"difficulty": test.get("difficulty", "unknown"),
|
|
148
|
+
"status": "dry_run",
|
|
149
|
+
})
|
|
150
|
+
return {"master": master_name, "total": len(tests), "results": results}
|
|
151
|
+
|
|
152
|
+
# Load skill context
|
|
153
|
+
system_prompt = load_skill_context(master_dir)
|
|
154
|
+
|
|
155
|
+
# Import anthropic
|
|
156
|
+
try:
|
|
157
|
+
import anthropic
|
|
158
|
+
except ImportError:
|
|
159
|
+
return {"error": "anthropic package not installed. Run: pip install anthropic"}
|
|
160
|
+
|
|
161
|
+
api_key = os.environ.get("ANTHROPIC_API_KEY")
|
|
162
|
+
if not api_key:
|
|
163
|
+
return {"error": "ANTHROPIC_API_KEY environment variable not set"}
|
|
164
|
+
|
|
165
|
+
client = anthropic.Anthropic(api_key=api_key)
|
|
166
|
+
|
|
167
|
+
passed = 0
|
|
168
|
+
failed = 0
|
|
169
|
+
|
|
170
|
+
for i, test in enumerate(tests):
|
|
171
|
+
print(f" [{i+1}/{len(tests)}] {test['q'][:50]}...", end=" ", flush=True)
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
message = client.messages.create(
|
|
175
|
+
model=model,
|
|
176
|
+
max_tokens=2048,
|
|
177
|
+
system=system_prompt,
|
|
178
|
+
messages=[{"role": "user", "content": test["q"]}],
|
|
179
|
+
)
|
|
180
|
+
response_text = message.content[0].text
|
|
181
|
+
except Exception as e:
|
|
182
|
+
results.append({
|
|
183
|
+
"index": i,
|
|
184
|
+
"question": test["q"],
|
|
185
|
+
"status": "api_error",
|
|
186
|
+
"error": str(e),
|
|
187
|
+
})
|
|
188
|
+
failed += 1
|
|
189
|
+
print("API ERROR")
|
|
190
|
+
continue
|
|
191
|
+
|
|
192
|
+
check = check_response(response_text, test, is_first_turn=True)
|
|
193
|
+
status = "PASS" if check["passed"] else "FAIL"
|
|
194
|
+
|
|
195
|
+
result_entry = {
|
|
196
|
+
"index": i,
|
|
197
|
+
"question": test["q"],
|
|
198
|
+
"difficulty": test.get("difficulty", "unknown"),
|
|
199
|
+
"test_type": test.get("test_type", "fidelity"),
|
|
200
|
+
"status": status,
|
|
201
|
+
"missing_cites": check["missing_cites"],
|
|
202
|
+
"missing_mentions": check["missing_mentions"],
|
|
203
|
+
"forbidden_found": check["forbidden_found"],
|
|
204
|
+
"boundary_violations": check["boundary_violations"],
|
|
205
|
+
"response_length": len(response_text),
|
|
206
|
+
}
|
|
207
|
+
results.append(result_entry)
|
|
208
|
+
|
|
209
|
+
if check["passed"]:
|
|
210
|
+
passed += 1
|
|
211
|
+
print("PASS")
|
|
212
|
+
else:
|
|
213
|
+
failed += 1
|
|
214
|
+
failures = (check["missing_cites"] + check["missing_mentions"]
|
|
215
|
+
+ check["forbidden_found"] + check["boundary_violations"])
|
|
216
|
+
print(f"FAIL ({failures})")
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
"master": master_name,
|
|
220
|
+
"model": model,
|
|
221
|
+
"total": len(tests),
|
|
222
|
+
"passed": passed,
|
|
223
|
+
"failed": failed,
|
|
224
|
+
"pass_rate": f"{passed / len(tests) * 100:.0f}%" if tests else "N/A",
|
|
225
|
+
"results": results,
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def main():
|
|
230
|
+
parser = argparse.ArgumentParser(description="Master-skill fidelity test runner")
|
|
231
|
+
parser.add_argument("--master", type=str, help="Test a specific master")
|
|
232
|
+
parser.add_argument("--all", action="store_true", help="Test all masters with fidelity.jsonl")
|
|
233
|
+
parser.add_argument("--dry-run", action="store_true", help="Show test cases without calling API")
|
|
234
|
+
parser.add_argument("--model", type=str, default="claude-sonnet-4-6", help="Claude model to use")
|
|
235
|
+
parser.add_argument("--json", action="store_true", help="Output as JSON")
|
|
236
|
+
parser.add_argument(
|
|
237
|
+
"--max-tests",
|
|
238
|
+
type=int,
|
|
239
|
+
default=None,
|
|
240
|
+
help="Cap the number of fixtures per master (smoke runs in CI use 1)",
|
|
241
|
+
)
|
|
242
|
+
args = parser.parse_args()
|
|
243
|
+
|
|
244
|
+
if not args.master and not args.all:
|
|
245
|
+
parser.error("Specify --master <name> or --all")
|
|
246
|
+
|
|
247
|
+
if args.all:
|
|
248
|
+
masters = sorted(
|
|
249
|
+
d.name for d in PREBUILT_DIR.iterdir()
|
|
250
|
+
if d.is_dir() and (d / "tests" / "fidelity.jsonl").exists()
|
|
251
|
+
)
|
|
252
|
+
else:
|
|
253
|
+
masters = [args.master]
|
|
254
|
+
|
|
255
|
+
all_results = []
|
|
256
|
+
for master in masters:
|
|
257
|
+
print(f"\n{'='*50}")
|
|
258
|
+
print(f"Testing: {master}")
|
|
259
|
+
print(f"{'='*50}")
|
|
260
|
+
result = run_tests(
|
|
261
|
+
master, dry_run=args.dry_run, model=args.model, max_tests=args.max_tests
|
|
262
|
+
)
|
|
263
|
+
all_results.append(result)
|
|
264
|
+
|
|
265
|
+
if not args.json and "error" not in result:
|
|
266
|
+
print(f"\nResult: {result.get('passed', 0)}/{result['total']} passed "
|
|
267
|
+
f"({result.get('pass_rate', 'N/A')})")
|
|
268
|
+
|
|
269
|
+
if args.json:
|
|
270
|
+
print(json.dumps(all_results, indent=2, ensure_ascii=False))
|
|
271
|
+
elif len(masters) > 1:
|
|
272
|
+
print(f"\n{'='*50}")
|
|
273
|
+
print("Overall Summary:")
|
|
274
|
+
for r in all_results:
|
|
275
|
+
if "error" in r:
|
|
276
|
+
print(f" {r.get('master', '?')}: {r['error']}")
|
|
277
|
+
else:
|
|
278
|
+
print(f" {r['master']}: {r.get('passed', 0)}/{r['total']} ({r.get('pass_rate', 'N/A')})")
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
if __name__ == "__main__":
|
|
282
|
+
main()
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Validate fidelity.jsonl structure for all masters.
|
|
3
|
+
|
|
4
|
+
Checks that every test case has required fields and valid structure.
|
|
5
|
+
No API calls needed — pure structural validation.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
python scripts/validate-fidelity.py
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
PREBUILT_DIR = Path(__file__).resolve().parent.parent / "prebuilt"
|
|
18
|
+
|
|
19
|
+
VALID_TEST_TYPES = {"fidelity", "boundary", "pressure"}
|
|
20
|
+
VALID_BOUNDARIES = {
|
|
21
|
+
"sectarian_judgment",
|
|
22
|
+
"no_prophecy",
|
|
23
|
+
"neutral_first_turn",
|
|
24
|
+
"no_fabricated_dialogue",
|
|
25
|
+
"no_esoteric_instruction",
|
|
26
|
+
}
|
|
27
|
+
VALID_PRESSURES = {
|
|
28
|
+
"citation_bypass",
|
|
29
|
+
"informality_bypass",
|
|
30
|
+
"meta_challenge",
|
|
31
|
+
"hostile_challenge",
|
|
32
|
+
"simplicity_bypass",
|
|
33
|
+
"terminology_bypass",
|
|
34
|
+
"relevance_challenge",
|
|
35
|
+
"misunderstanding_challenge",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def validate_master(master_dir: Path) -> list[str]:
|
|
40
|
+
"""Validate fidelity.jsonl for a single master. Returns list of errors."""
|
|
41
|
+
fidelity_path = master_dir / "tests" / "fidelity.jsonl"
|
|
42
|
+
if not fidelity_path.exists():
|
|
43
|
+
return [f"{master_dir.name}: no fidelity.jsonl found"]
|
|
44
|
+
|
|
45
|
+
errors = []
|
|
46
|
+
lines = fidelity_path.read_text(encoding="utf-8").strip().splitlines()
|
|
47
|
+
|
|
48
|
+
if len(lines) < 5:
|
|
49
|
+
errors.append(f"{master_dir.name}: fewer than 5 test cases ({len(lines)})")
|
|
50
|
+
|
|
51
|
+
for i, line in enumerate(lines, 1):
|
|
52
|
+
if not line.strip():
|
|
53
|
+
continue
|
|
54
|
+
try:
|
|
55
|
+
test = json.loads(line)
|
|
56
|
+
except json.JSONDecodeError as e:
|
|
57
|
+
errors.append(f"{master_dir.name}:{i}: invalid JSON — {e}")
|
|
58
|
+
continue
|
|
59
|
+
|
|
60
|
+
# Every test must have "q"
|
|
61
|
+
if "q" not in test:
|
|
62
|
+
errors.append(f"{master_dir.name}:{i}: missing 'q' field")
|
|
63
|
+
|
|
64
|
+
# Must have at least one assertion
|
|
65
|
+
has_assertion = any(
|
|
66
|
+
k in test
|
|
67
|
+
for k in [
|
|
68
|
+
"must_cite",
|
|
69
|
+
"must_mention",
|
|
70
|
+
"must_not_contain",
|
|
71
|
+
"must_not_contain_first_turn",
|
|
72
|
+
"must_select_masters",
|
|
73
|
+
"must_have_sections",
|
|
74
|
+
"must_cite_per_master",
|
|
75
|
+
]
|
|
76
|
+
)
|
|
77
|
+
if not has_assertion:
|
|
78
|
+
errors.append(f"{master_dir.name}:{i}: no assertion fields found")
|
|
79
|
+
|
|
80
|
+
# Validate test_type if present
|
|
81
|
+
test_type = test.get("test_type")
|
|
82
|
+
if test_type and test_type not in VALID_TEST_TYPES:
|
|
83
|
+
errors.append(
|
|
84
|
+
f"{master_dir.name}:{i}: invalid test_type '{test_type}' "
|
|
85
|
+
f"(valid: {VALID_TEST_TYPES})"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# Validate boundary/pressure subtypes
|
|
89
|
+
if test_type == "boundary":
|
|
90
|
+
boundary = test.get("boundary")
|
|
91
|
+
if not boundary:
|
|
92
|
+
errors.append(f"{master_dir.name}:{i}: boundary test missing 'boundary' field")
|
|
93
|
+
elif boundary not in VALID_BOUNDARIES:
|
|
94
|
+
errors.append(
|
|
95
|
+
f"{master_dir.name}:{i}: unknown boundary '{boundary}' "
|
|
96
|
+
f"(valid: {VALID_BOUNDARIES})"
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
if test_type == "pressure":
|
|
100
|
+
pressure = test.get("pressure")
|
|
101
|
+
if not pressure:
|
|
102
|
+
errors.append(f"{master_dir.name}:{i}: pressure test missing 'pressure' field")
|
|
103
|
+
|
|
104
|
+
# List fields must be lists
|
|
105
|
+
for field in ["must_cite", "must_mention", "must_not_contain", "must_not_contain_first_turn"]:
|
|
106
|
+
if field in test and not isinstance(test[field], list):
|
|
107
|
+
errors.append(f"{master_dir.name}:{i}: '{field}' must be a list")
|
|
108
|
+
|
|
109
|
+
# Check coverage: should have at least one boundary test
|
|
110
|
+
has_boundary = any(
|
|
111
|
+
json.loads(l).get("test_type") == "boundary"
|
|
112
|
+
for l in lines
|
|
113
|
+
if l.strip()
|
|
114
|
+
)
|
|
115
|
+
if not has_boundary:
|
|
116
|
+
errors.append(f"{master_dir.name}: no boundary tests found (need at least one)")
|
|
117
|
+
|
|
118
|
+
return errors
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def main():
|
|
122
|
+
all_errors = []
|
|
123
|
+
masters = sorted(
|
|
124
|
+
d for d in PREBUILT_DIR.iterdir()
|
|
125
|
+
if d.is_dir() and (d / "tests" / "fidelity.jsonl").exists()
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
for master_dir in masters:
|
|
129
|
+
errors = validate_master(master_dir)
|
|
130
|
+
all_errors.extend(errors)
|
|
131
|
+
if not errors:
|
|
132
|
+
fidelity_path = master_dir / "tests" / "fidelity.jsonl"
|
|
133
|
+
count = len(fidelity_path.read_text().strip().splitlines()) if fidelity_path.exists() else 0
|
|
134
|
+
print(f" {master_dir.name}: {count} tests OK")
|
|
135
|
+
|
|
136
|
+
if all_errors:
|
|
137
|
+
print(f"\n{len(all_errors)} error(s) found:")
|
|
138
|
+
for err in all_errors:
|
|
139
|
+
print(f" ERROR: {err}")
|
|
140
|
+
sys.exit(1)
|
|
141
|
+
else:
|
|
142
|
+
print(f"\nAll {len(masters)} masters validated successfully.")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
if __name__ == "__main__":
|
|
146
|
+
main()
|