master-skill 0.6.0 → 0.9.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 +3 -3
- package/.claude-plugin/plugin.json +2 -2
- package/.cursor-plugin/plugin.json +2 -2
- package/README.md +71 -14
- package/README_EN.md +67 -12
- package/SKILL.md +58 -297
- package/bin/cli.mjs +58 -12
- package/gemini-extension.json +2 -2
- package/hooks/session-start +40 -2
- package/hooks/tests/test_session_start.sh +149 -0
- package/package.json +14 -7
- package/prebuilt/compare/SKILL.md +1 -1
- package/prebuilt/compare/tests/fidelity.jsonl +13 -0
- package/prebuilt/master-ajahn-chah/SKILL.md +35 -1
- package/prebuilt/master-ajahn-chah/meta.json +17 -1
- package/prebuilt/master-atisha/SKILL.md +35 -1
- package/prebuilt/master-atisha/meta.json +17 -1
- package/prebuilt/master-buddhaghosa/SKILL.md +35 -1
- package/prebuilt/master-buddhaghosa/meta.json +26 -1
- package/prebuilt/master-curriculum/SKILL.md +87 -0
- package/prebuilt/master-curriculum/references/chan.md +29 -0
- package/prebuilt/master-curriculum/references/gelug-madhyamaka.md +33 -0
- package/prebuilt/master-curriculum/references/huayan.md +28 -0
- package/prebuilt/master-curriculum/references/jingtu.md +29 -0
- package/prebuilt/master-curriculum/references/sanlun-zhongguan.md +30 -0
- package/prebuilt/master-curriculum/references/theravada-vipassana.md +39 -0
- package/prebuilt/master-curriculum/references/tiantai.md +30 -0
- package/prebuilt/master-curriculum/references/weishi.md +31 -0
- package/prebuilt/master-curriculum/tests/fidelity.jsonl +8 -0
- package/prebuilt/master-debate/SKILL.md +274 -0
- package/prebuilt/master-debate/meta.json +25 -0
- package/prebuilt/master-debate/tests/fidelity.jsonl +8 -0
- package/prebuilt/master-fazang/SKILL.md +35 -1
- package/prebuilt/master-fazang/meta.json +25 -0
- package/prebuilt/master-huineng/SKILL.md +35 -1
- package/prebuilt/master-huineng/meta.json +37 -0
- package/prebuilt/master-huineng/references/teaching.md +1 -1
- package/prebuilt/master-huineng/sources/INDEX.md +1 -1
- package/prebuilt/master-huineng/sources/tanjing-excerpts.md +17 -1
- package/prebuilt/master-huineng/tests/fidelity.jsonl +2 -0
- package/prebuilt/master-kumarajiva/SKILL.md +35 -1
- package/prebuilt/master-kumarajiva/meta.json +16 -0
- package/prebuilt/master-kumarajiva/references/teaching.md +2 -2
- package/prebuilt/master-mahasi-sayadaw/SKILL.md +35 -1
- package/prebuilt/master-mahasi-sayadaw/meta.json +17 -1
- package/prebuilt/master-milarepa/SKILL.md +35 -1
- package/prebuilt/master-milarepa/meta.json +26 -1
- package/prebuilt/master-nagarjuna/SKILL.md +170 -0
- package/prebuilt/master-nagarjuna/meta.json +129 -0
- package/prebuilt/master-nagarjuna/references/teaching.md +112 -0
- package/prebuilt/master-nagarjuna/references/voice.md +100 -0
- package/prebuilt/master-nagarjuna/sources/INDEX.md +23 -0
- package/prebuilt/master-nagarjuna/sources/dazhidulun-excerpts.md +49 -0
- package/prebuilt/master-nagarjuna/sources/shizhu-yixing-excerpts.md +33 -0
- package/prebuilt/master-nagarjuna/sources/zhonglun-excerpts.md +94 -0
- package/prebuilt/master-nagarjuna/tests/fidelity.jsonl +10 -0
- package/prebuilt/master-ouyi/SKILL.md +35 -1
- package/prebuilt/master-ouyi/meta.json +17 -0
- package/prebuilt/master-ouyi/references/teaching.md +4 -4
- package/prebuilt/master-ouyi/sources/INDEX.md +1 -1
- package/prebuilt/master-ouyi/sources/jiaoguan-gangzong-excerpts.md +3 -3
- package/prebuilt/master-tsongkhapa/SKILL.md +35 -1
- package/prebuilt/master-tsongkhapa/meta.json +18 -1
- package/prebuilt/master-xuanzang/SKILL.md +35 -1
- package/prebuilt/master-xuanzang/meta.json +16 -0
- package/prebuilt/master-xuanzang/references/teaching.md +5 -5
- package/prebuilt/master-xuanzang/references/voice.md +1 -1
- package/prebuilt/master-xuyun/SKILL.md +35 -1
- package/prebuilt/master-xuyun/meta.json +39 -0
- package/prebuilt/master-yinguang/SKILL.md +36 -2
- package/prebuilt/master-yinguang/meta.json +17 -0
- package/prebuilt/master-yinguang/references/teaching.md +7 -7
- package/prebuilt/master-yinguang/references/voice.md +1 -1
- package/prebuilt/master-yinguang/sources/INDEX.md +3 -3
- package/prebuilt/master-yinguang/sources/wenchao-excerpts.md +4 -4
- package/prebuilt/master-yinguang/sources/yihanbianfu-excerpts.md +3 -3
- package/prebuilt/master-zhiyi/SKILL.md +35 -1
- package/prebuilt/master-zhiyi/meta.json +28 -0
- package/prebuilt/master-zhiyi/references/teaching.md +2 -2
- package/scripts/_masterpaths.py +32 -0
- package/scripts/check-manifest-versions.py +142 -0
- package/scripts/cite.py +19 -4
- package/scripts/query.py +19 -4
- package/scripts/test-fidelity.py +35 -3
- package/scripts/tests/test_check_manifest_versions.py +217 -0
- package/scripts/tests/test_debate_protocol.py +159 -0
- package/scripts/tests/test_injection_hardening.py +174 -0
- package/scripts/tests/test_validate_cross_critique.py +149 -0
- package/scripts/tests/test_validate_curriculum_sources.py +144 -0
- package/scripts/tests/test_validate_lore_triggers_content.py +372 -0
- package/scripts/tests/test_validate_persona_fidelity.py +317 -0
- package/scripts/tests/test_validate_promptfoo_configs.py +386 -0
- package/scripts/validate-cross-critique.py +137 -0
- package/scripts/validate-curriculum-sources.py +111 -0
- package/scripts/validate-fidelity.py +16 -1
- package/scripts/validate-lore-triggers-content.py +393 -0
- package/scripts/validate-persona-fidelity.py +210 -0
- package/scripts/validate-promptfoo-configs.py +383 -0
- package/scripts/validate.py +212 -4
- package/scripts/verify_citations.py +152 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Tests for validate-cross-critique.py."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import importlib.util
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _load_module():
|
|
14
|
+
spec_path = Path(__file__).resolve().parents[1] / "validate-cross-critique.py"
|
|
15
|
+
spec = importlib.util.spec_from_file_location("vcc", spec_path)
|
|
16
|
+
mod = importlib.util.module_from_spec(spec)
|
|
17
|
+
sys.modules["vcc"] = mod
|
|
18
|
+
spec.loader.exec_module(mod)
|
|
19
|
+
return mod
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _write_master(prebuilt: Path, slug: str, sources: list[dict], cross_critique=None):
|
|
23
|
+
d = prebuilt / f"master-{slug}"
|
|
24
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
25
|
+
data = {"slug": slug, "sources": sources}
|
|
26
|
+
if cross_critique is not None:
|
|
27
|
+
data["cross_critique"] = cross_critique
|
|
28
|
+
(d / "meta.json").write_text(json.dumps(data), encoding="utf-8")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@pytest.fixture
|
|
32
|
+
def fake_tree(tmp_path):
|
|
33
|
+
prebuilt = tmp_path / "prebuilt"
|
|
34
|
+
# build enough masters to satisfy required_pairs in tests that need them
|
|
35
|
+
for slug in [
|
|
36
|
+
"huineng", "yinguang", "kumarajiva", "xuanzang", "zhiyi",
|
|
37
|
+
"tsongkhapa", "ajahn-chah", "mahasi-sayadaw", "atisha", "ouyi",
|
|
38
|
+
]:
|
|
39
|
+
_write_master(prebuilt, slug, [{"type": "cbeta", "id": "T00n0001", "title": "demo"}])
|
|
40
|
+
return prebuilt
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_empty_cross_critique_is_ok(fake_tree):
|
|
44
|
+
vcc = _load_module()
|
|
45
|
+
# No master has cross_critique; structural check passes,
|
|
46
|
+
# but required_pairs check still fails (no coverage).
|
|
47
|
+
errors = vcc.validate(fake_tree, check_coverage=False)
|
|
48
|
+
assert errors == []
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_well_formed_entry_passes(fake_tree):
|
|
52
|
+
vcc = _load_module()
|
|
53
|
+
_write_master(fake_tree, "huineng",
|
|
54
|
+
[{"type": "cbeta", "id": "T48n2008", "title": "坛经"}],
|
|
55
|
+
cross_critique=[
|
|
56
|
+
{"target_master": "yinguang", "position": "对净土:本性弥陀唯心净土,何曾离自心。", "citation": "T48n2008"},
|
|
57
|
+
])
|
|
58
|
+
errors = vcc.validate(fake_tree, check_coverage=False)
|
|
59
|
+
# Only the huineng→yinguang structural check should pass.
|
|
60
|
+
huineng_errors = [e for e in errors if "huineng" in e]
|
|
61
|
+
assert huineng_errors == []
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_missing_required_field(fake_tree):
|
|
65
|
+
vcc = _load_module()
|
|
66
|
+
_write_master(fake_tree, "huineng",
|
|
67
|
+
[{"type": "cbeta", "id": "T48n2008", "title": "坛经"}],
|
|
68
|
+
cross_critique=[{"target_master": "yinguang"}]) # no position, no citation
|
|
69
|
+
errors = vcc.validate(fake_tree, check_coverage=False)
|
|
70
|
+
assert any("position" in e for e in errors)
|
|
71
|
+
assert any("citation" in e for e in errors)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_cannot_target_self(fake_tree):
|
|
75
|
+
vcc = _load_module()
|
|
76
|
+
_write_master(fake_tree, "huineng",
|
|
77
|
+
[{"type": "cbeta", "id": "T48n2008", "title": "坛经"}],
|
|
78
|
+
cross_critique=[{"target_master": "huineng", "position": "x" * 50, "citation": "T48n2008"}])
|
|
79
|
+
errors = vcc.validate(fake_tree, check_coverage=False)
|
|
80
|
+
assert any("self" in e.lower() for e in errors)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_cannot_target_meta_skill(fake_tree):
|
|
84
|
+
vcc = _load_module()
|
|
85
|
+
_write_master(fake_tree, "huineng",
|
|
86
|
+
[{"type": "cbeta", "id": "T48n2008", "title": "坛经"}],
|
|
87
|
+
cross_critique=[{"target_master": "curriculum", "position": "x" * 50, "citation": "T48n2008"}])
|
|
88
|
+
errors = vcc.validate(fake_tree, check_coverage=False)
|
|
89
|
+
assert any("meta-skill" in e or "curriculum" in e for e in errors)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_unknown_target_master(fake_tree):
|
|
93
|
+
vcc = _load_module()
|
|
94
|
+
_write_master(fake_tree, "huineng",
|
|
95
|
+
[{"type": "cbeta", "id": "T48n2008", "title": "坛经"}],
|
|
96
|
+
cross_critique=[{"target_master": "ghost", "position": "x" * 50, "citation": "T48n2008"}])
|
|
97
|
+
errors = vcc.validate(fake_tree, check_coverage=False)
|
|
98
|
+
assert any("ghost" in e for e in errors)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_citation_not_in_own_sources(fake_tree):
|
|
102
|
+
vcc = _load_module()
|
|
103
|
+
_write_master(fake_tree, "huineng",
|
|
104
|
+
[{"type": "cbeta", "id": "T48n2008", "title": "坛经"}],
|
|
105
|
+
cross_critique=[{"target_master": "yinguang", "position": "x" * 50, "citation": "T99n9999"}])
|
|
106
|
+
errors = vcc.validate(fake_tree, check_coverage=False)
|
|
107
|
+
assert any("T99n9999" in e for e in errors)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_position_length_bounds(fake_tree):
|
|
111
|
+
vcc = _load_module()
|
|
112
|
+
_write_master(fake_tree, "huineng",
|
|
113
|
+
[{"type": "cbeta", "id": "T48n2008", "title": "坛经"}],
|
|
114
|
+
cross_critique=[
|
|
115
|
+
{"target_master": "yinguang", "position": "短", "citation": "T48n2008"}, # < 10
|
|
116
|
+
{"target_master": "zhiyi", "position": "x" * 400, "citation": "T48n2008"}, # > 300
|
|
117
|
+
])
|
|
118
|
+
errors = vcc.validate(fake_tree, check_coverage=False)
|
|
119
|
+
assert any("length" in e for e in errors)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_coverage_fails_when_pairs_missing(fake_tree):
|
|
123
|
+
vcc = _load_module()
|
|
124
|
+
errors = vcc.validate(fake_tree, check_coverage=True)
|
|
125
|
+
# Should report all 16 missing critique entries.
|
|
126
|
+
missing_errors = [e for e in errors if "missing critique" in e]
|
|
127
|
+
assert len(missing_errors) == 16
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_coverage_succeeds_with_full_pairs(fake_tree):
|
|
131
|
+
vcc = _load_module()
|
|
132
|
+
pairs = [
|
|
133
|
+
("huineng", "yinguang"), ("yinguang", "huineng"),
|
|
134
|
+
("kumarajiva", "xuanzang"), ("xuanzang", "kumarajiva"),
|
|
135
|
+
("huineng", "zhiyi"), ("zhiyi", "huineng"),
|
|
136
|
+
("tsongkhapa", "huineng"), ("huineng", "tsongkhapa"),
|
|
137
|
+
("ajahn-chah", "mahasi-sayadaw"), ("mahasi-sayadaw", "ajahn-chah"),
|
|
138
|
+
("atisha", "huineng"), ("huineng", "atisha"),
|
|
139
|
+
("ouyi", "yinguang"), ("yinguang", "ouyi"),
|
|
140
|
+
("ouyi", "tsongkhapa"), ("tsongkhapa", "ouyi"),
|
|
141
|
+
]
|
|
142
|
+
by_slug: dict[str, list[dict]] = {}
|
|
143
|
+
for src, tgt in pairs:
|
|
144
|
+
by_slug.setdefault(src, []).append(
|
|
145
|
+
{"target_master": tgt, "position": "x" * 50, "citation": "T00n0001"})
|
|
146
|
+
for slug, cc in by_slug.items():
|
|
147
|
+
_write_master(fake_tree, slug, [{"type": "cbeta", "id": "T00n0001", "title": "demo"}], cc)
|
|
148
|
+
errors = vcc.validate(fake_tree, check_coverage=True)
|
|
149
|
+
assert errors == []
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""Tests for validate-curriculum-sources.py.
|
|
2
|
+
|
|
3
|
+
Verifies that citations (CBETA T-numbers, X-numbers, Toh, compiled-teaching ids)
|
|
4
|
+
and /master-<slug> references in curriculum reference files cross-check against
|
|
5
|
+
the actual prebuilt master metadata.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
import importlib.util
|
|
15
|
+
import sys
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _load_module():
|
|
19
|
+
spec_path = Path(__file__).resolve().parents[1] / "validate-curriculum-sources.py"
|
|
20
|
+
spec = importlib.util.spec_from_file_location("vcs", spec_path)
|
|
21
|
+
mod = importlib.util.module_from_spec(spec)
|
|
22
|
+
sys.modules["vcs"] = mod
|
|
23
|
+
spec.loader.exec_module(mod)
|
|
24
|
+
return mod
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@pytest.fixture
|
|
28
|
+
def fake_prebuilt(tmp_path):
|
|
29
|
+
"""Fake prebuilt tree with two masters and a curriculum dir."""
|
|
30
|
+
prebuilt = tmp_path / "prebuilt"
|
|
31
|
+
# Master A: cbeta source
|
|
32
|
+
(prebuilt / "master-aaa").mkdir(parents=True)
|
|
33
|
+
(prebuilt / "master-aaa" / "meta.json").write_text(json.dumps({
|
|
34
|
+
"slug": "aaa",
|
|
35
|
+
"sources": [{"type": "cbeta", "id": "T48n2008", "title": "demo"}],
|
|
36
|
+
}), encoding="utf-8")
|
|
37
|
+
# Master B: compiled_teaching source + Toh
|
|
38
|
+
(prebuilt / "master-bbb").mkdir(parents=True)
|
|
39
|
+
(prebuilt / "master-bbb" / "meta.json").write_text(json.dumps({
|
|
40
|
+
"slug": "bbb",
|
|
41
|
+
"sources": [
|
|
42
|
+
{"type": "compiled_teaching", "id": "AjahnChah:FoodForTheHeart", "title": "demo"},
|
|
43
|
+
{"type": "84000", "id": "Toh 21", "title": "demo"},
|
|
44
|
+
],
|
|
45
|
+
}), encoding="utf-8")
|
|
46
|
+
(prebuilt / "master-curriculum" / "references").mkdir(parents=True)
|
|
47
|
+
return prebuilt
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_extract_citations_finds_T_numbers():
|
|
51
|
+
vcs = _load_module()
|
|
52
|
+
text = "See 《坛经》【T48n2008】 for details."
|
|
53
|
+
assert "T48n2008" in vcs.extract_citations(text)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_extract_citations_finds_Toh():
|
|
57
|
+
vcs = _load_module()
|
|
58
|
+
text = "藏文藏经 Toh 21 心经..."
|
|
59
|
+
assert "Toh 21" in vcs.extract_citations(text)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_extract_citations_finds_compiled_teaching():
|
|
63
|
+
vcs = _load_module()
|
|
64
|
+
text = "见 AjahnChah:FoodForTheHeart ..."
|
|
65
|
+
assert "AjahnChah:FoodForTheHeart" in vcs.extract_citations(text)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_extract_master_slugs():
|
|
69
|
+
vcs = _load_module()
|
|
70
|
+
text = "推荐 master:/master-aaa ;交叉 /master-bbb 。"
|
|
71
|
+
assert vcs.extract_master_slugs(text) == {"aaa", "bbb"}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_collect_known_citations(fake_prebuilt):
|
|
75
|
+
vcs = _load_module()
|
|
76
|
+
known = vcs.collect_known_citations(fake_prebuilt)
|
|
77
|
+
assert "T48n2008" in known
|
|
78
|
+
assert "AjahnChah:FoodForTheHeart" in known
|
|
79
|
+
assert "Toh 21" in known
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_validate_pass(fake_prebuilt):
|
|
83
|
+
vcs = _load_module()
|
|
84
|
+
ref = fake_prebuilt / "master-curriculum" / "references" / "ok.md"
|
|
85
|
+
ref.write_text(
|
|
86
|
+
"## Path\n- 主用经《坛经》【T48n2008】\n- master:/master-aaa\n- 藏:Toh 21\n",
|
|
87
|
+
encoding="utf-8",
|
|
88
|
+
)
|
|
89
|
+
errors = vcs.validate(fake_prebuilt)
|
|
90
|
+
assert errors == []
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_validate_fails_on_fabricated_citation(fake_prebuilt):
|
|
94
|
+
vcs = _load_module()
|
|
95
|
+
ref = fake_prebuilt / "master-curriculum" / "references" / "bad.md"
|
|
96
|
+
ref.write_text("【T99n9999】 fake\n", encoding="utf-8")
|
|
97
|
+
errors = vcs.validate(fake_prebuilt)
|
|
98
|
+
assert any("T99n9999" in e for e in errors)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_validate_fails_on_unknown_master_slug(fake_prebuilt):
|
|
102
|
+
vcs = _load_module()
|
|
103
|
+
ref = fake_prebuilt / "master-curriculum" / "references" / "bad.md"
|
|
104
|
+
ref.write_text("/master-ghost\n", encoding="utf-8")
|
|
105
|
+
errors = vcs.validate(fake_prebuilt)
|
|
106
|
+
assert any("ghost" in e for e in errors)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_validate_ignores_meta_skill_slugs(fake_prebuilt):
|
|
110
|
+
"""/master-curriculum and /master-debate are meta-skills suppressed via
|
|
111
|
+
META_SKILL_SLUGS. /compare-masters never matches _SLUG (no /master- prefix),
|
|
112
|
+
so it is naturally ignored. None of these may raise validate errors."""
|
|
113
|
+
vcs = _load_module()
|
|
114
|
+
ref = fake_prebuilt / "master-curriculum" / "references" / "ok.md"
|
|
115
|
+
ref.write_text(
|
|
116
|
+
"延伸 → /compare-masters · /master-debate · /master-curriculum\n",
|
|
117
|
+
encoding="utf-8",
|
|
118
|
+
)
|
|
119
|
+
errors = vcs.validate(fake_prebuilt)
|
|
120
|
+
assert errors == []
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_validate_returns_error_when_refs_dir_missing(tmp_path):
|
|
124
|
+
vcs = _load_module()
|
|
125
|
+
prebuilt = tmp_path / "prebuilt"
|
|
126
|
+
prebuilt.mkdir()
|
|
127
|
+
# curriculum/references missing
|
|
128
|
+
errors = vcs.validate(prebuilt)
|
|
129
|
+
assert len(errors) == 1
|
|
130
|
+
assert "references dir not found" in errors[0]
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def test_extract_citations_finds_hyphenated_compiled_teaching():
|
|
134
|
+
vcs = _load_module()
|
|
135
|
+
text = "PTS:DN-Comm 注释 / BDRC:Pha-chos-Bu-chos"
|
|
136
|
+
cits = vcs.extract_citations(text)
|
|
137
|
+
assert "PTS:DN-Comm" in cits
|
|
138
|
+
assert "BDRC:Pha-chos-Bu-chos" in cits
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def test_extract_citations_finds_Toh_with_colon():
|
|
142
|
+
vcs = _load_module()
|
|
143
|
+
text = "见 Toh:3948"
|
|
144
|
+
assert "Toh:3948" in vcs.extract_citations(text)
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
"""Tests for validate-lore-triggers-content.py.
|
|
2
|
+
|
|
3
|
+
Covers:
|
|
4
|
+
- normalize() drops punctuation/whitespace, folds trad->simp
|
|
5
|
+
- longest_common_substring_len returns true LCS length
|
|
6
|
+
- similarity_ratio is windowed correctly for large bodies
|
|
7
|
+
- check_entry passes a true verbatim quote in sources/
|
|
8
|
+
- check_entry passes a quote with trad/simp variation
|
|
9
|
+
- check_entry FAILS a fabricated quote
|
|
10
|
+
- check_entry FAILS short quote when only LCS of 2 chars overlap
|
|
11
|
+
- check_entry returns the in_references_only soft-pass when match
|
|
12
|
+
is in references/ but not sources/
|
|
13
|
+
- validate() walks tmp_path tree end to end
|
|
14
|
+
- main() advisory mode exits 0 even when failures present
|
|
15
|
+
- main() --strict exits non-zero on failures
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import importlib.util
|
|
20
|
+
import json
|
|
21
|
+
import subprocess
|
|
22
|
+
import sys
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
import pytest
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _load_module():
|
|
29
|
+
spec_path = (
|
|
30
|
+
Path(__file__).resolve().parents[1]
|
|
31
|
+
/ "validate-lore-triggers-content.py"
|
|
32
|
+
)
|
|
33
|
+
spec = importlib.util.spec_from_file_location("vltc", spec_path)
|
|
34
|
+
mod = importlib.util.module_from_spec(spec)
|
|
35
|
+
sys.modules["vltc"] = mod
|
|
36
|
+
spec.loader.exec_module(mod)
|
|
37
|
+
return mod
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _setup_master(
|
|
41
|
+
prebuilt: Path,
|
|
42
|
+
slug: str,
|
|
43
|
+
lore_triggers: list,
|
|
44
|
+
excerpts_text: str | None = None,
|
|
45
|
+
references_text: str | None = None,
|
|
46
|
+
excerpts_filename: str = "tanjing-excerpts.md",
|
|
47
|
+
):
|
|
48
|
+
d = prebuilt / f"master-{slug}"
|
|
49
|
+
(d / "sources").mkdir(parents=True, exist_ok=True)
|
|
50
|
+
(d / "references").mkdir(parents=True, exist_ok=True)
|
|
51
|
+
meta = {
|
|
52
|
+
"slug": slug,
|
|
53
|
+
"sources": [
|
|
54
|
+
{"type": "cbeta", "id": "T48n2008", "title": "坛经"}
|
|
55
|
+
],
|
|
56
|
+
"lore_triggers": lore_triggers,
|
|
57
|
+
}
|
|
58
|
+
(d / "meta.json").write_text(
|
|
59
|
+
json.dumps(meta, ensure_ascii=False), encoding="utf-8"
|
|
60
|
+
)
|
|
61
|
+
if excerpts_text is not None:
|
|
62
|
+
(d / "sources" / excerpts_filename).write_text(
|
|
63
|
+
excerpts_text, encoding="utf-8"
|
|
64
|
+
)
|
|
65
|
+
if references_text is not None:
|
|
66
|
+
(d / "references" / "teaching.md").write_text(
|
|
67
|
+
references_text, encoding="utf-8"
|
|
68
|
+
)
|
|
69
|
+
return d
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# ---------- normalize ----------
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_normalize_drops_punctuation():
|
|
76
|
+
m = _load_module()
|
|
77
|
+
assert m.normalize("我此法门,从上以来。") == "我此法门从上以来"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_normalize_drops_whitespace():
|
|
81
|
+
m = _load_module()
|
|
82
|
+
assert m.normalize("定 慧 一 体\n") == "定慧一体"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_normalize_folds_traditional_to_simplified():
|
|
86
|
+
m = _load_module()
|
|
87
|
+
# 介尔 in meta vs 介爾 in excerpts must compare equal post-normalize.
|
|
88
|
+
assert m.normalize("介爾有心") == m.normalize("介尔有心")
|
|
89
|
+
# 於 vs 于
|
|
90
|
+
assert m.normalize("於相而离相") == m.normalize("于相而离相")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# ---------- LCS ----------
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_lcs_finds_long_overlap():
|
|
97
|
+
m = _load_module()
|
|
98
|
+
a = "abcdefghijklmnop"
|
|
99
|
+
b = "zzabcdefghijxxxx"
|
|
100
|
+
assert m.longest_common_substring_len(a, b) == 10 # abcdefghij
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_lcs_empty_inputs():
|
|
104
|
+
m = _load_module()
|
|
105
|
+
assert m.longest_common_substring_len("", "abc") == 0
|
|
106
|
+
assert m.longest_common_substring_len("abc", "") == 0
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# ---------- similarity ratio ----------
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_similarity_ratio_high_for_identical():
|
|
113
|
+
m = _load_module()
|
|
114
|
+
s = "一空一切空,无假无中而不空,总空观也"
|
|
115
|
+
assert m.similarity_ratio(s, s) >= 0.99
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_similarity_ratio_low_for_unrelated():
|
|
119
|
+
m = _load_module()
|
|
120
|
+
a = "菩提本无树明镜亦非台"
|
|
121
|
+
b = "完全无关的废话填充字符串赞美和谐社会"
|
|
122
|
+
assert m.similarity_ratio(a, b) < 0.4
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# ---------- extract_quotation ----------
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_extract_quotation_strips_gloss():
|
|
129
|
+
m = _load_module()
|
|
130
|
+
content = "定慧一体,不是二。——此乃慧能定慧不二说"
|
|
131
|
+
assert m.extract_quotation(content) == "定慧一体,不是二。"
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_extract_quotation_no_gloss():
|
|
135
|
+
m = _load_module()
|
|
136
|
+
assert m.extract_quotation("无相为体") == "无相为体"
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# ---------- check_entry: positive case ----------
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_check_entry_passes_verbatim_quote(tmp_path):
|
|
143
|
+
m = _load_module()
|
|
144
|
+
prebuilt = tmp_path / "prebuilt"
|
|
145
|
+
quote = (
|
|
146
|
+
"我此法门,从上以来,先立无念为宗,无相为体,无住为本。"
|
|
147
|
+
"无相者,于相而离相;无念者,于念而无念;无住者,人之本性。"
|
|
148
|
+
)
|
|
149
|
+
excerpts = f"# 坛经 (T48n2008)\n\n> {quote}\n"
|
|
150
|
+
d = _setup_master(
|
|
151
|
+
prebuilt,
|
|
152
|
+
slug="huineng",
|
|
153
|
+
lore_triggers=[
|
|
154
|
+
{
|
|
155
|
+
"keys": ["无念"],
|
|
156
|
+
"content": quote + "——此为三纲领",
|
|
157
|
+
"source_ref": "T48n2008#定慧品",
|
|
158
|
+
}
|
|
159
|
+
],
|
|
160
|
+
excerpts_text=excerpts,
|
|
161
|
+
)
|
|
162
|
+
res = m.check_entry(d, json.loads((d / "meta.json").read_text())["lore_triggers"][0], 0)
|
|
163
|
+
assert res["passed"] is True
|
|
164
|
+
assert res["passed_in_sources"] is True
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def test_check_entry_passes_with_trad_simp_variation(tmp_path):
|
|
168
|
+
m = _load_module()
|
|
169
|
+
prebuilt = tmp_path / "prebuilt"
|
|
170
|
+
meta_quote = (
|
|
171
|
+
"夫一心具十法界,一法界又具十法界、百法界;"
|
|
172
|
+
"此三千在一念心,介尔有心即具三千。"
|
|
173
|
+
)
|
|
174
|
+
excerpts_quote = meta_quote.replace("介尔", "介爾") # excerpts use trad
|
|
175
|
+
d = _setup_master(
|
|
176
|
+
prebuilt,
|
|
177
|
+
slug="zhiyi",
|
|
178
|
+
lore_triggers=[
|
|
179
|
+
{
|
|
180
|
+
"keys": ["一念三千"],
|
|
181
|
+
"content": meta_quote + "——此乃一念三千",
|
|
182
|
+
"source_ref": "T46n1911#卷五上",
|
|
183
|
+
}
|
|
184
|
+
],
|
|
185
|
+
excerpts_text=f"# 摩诃止观\n\n> {excerpts_quote}\n",
|
|
186
|
+
excerpts_filename="mohezhiguan-excerpts.md",
|
|
187
|
+
)
|
|
188
|
+
res = m.check_entry(d, json.loads((d / "meta.json").read_text())["lore_triggers"][0], 0)
|
|
189
|
+
assert res["passed"] is True
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
# ---------- check_entry: negative case ----------
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def test_check_entry_fails_fabricated_quote(tmp_path):
|
|
196
|
+
m = _load_module()
|
|
197
|
+
prebuilt = tmp_path / "prebuilt"
|
|
198
|
+
fabricated = (
|
|
199
|
+
"念佛是谁?这是一个完全编造的引文,不可能在任何坛经原典里出现,"
|
|
200
|
+
"纯属虚构以测试 validator 是否能识别。"
|
|
201
|
+
)
|
|
202
|
+
d = _setup_master(
|
|
203
|
+
prebuilt,
|
|
204
|
+
slug="huineng",
|
|
205
|
+
lore_triggers=[
|
|
206
|
+
{
|
|
207
|
+
"keys": ["念佛是谁"],
|
|
208
|
+
"content": fabricated + "——此乃伪造",
|
|
209
|
+
"source_ref": "T48n2008#行由品",
|
|
210
|
+
}
|
|
211
|
+
],
|
|
212
|
+
# A real excerpts file with totally unrelated content
|
|
213
|
+
excerpts_text="# 坛经\n\n> 菩提自性,本来清净,但用此心,直了成佛。\n",
|
|
214
|
+
)
|
|
215
|
+
res = m.check_entry(d, json.loads((d / "meta.json").read_text())["lore_triggers"][0], 0)
|
|
216
|
+
assert res["passed"] is False, (
|
|
217
|
+
f"Fabricated quote slipped through with lcs={res['best_lcs']} "
|
|
218
|
+
f"ratio={res['best_ratio']}"
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def test_check_entry_short_quote_low_overlap_fails(tmp_path):
|
|
223
|
+
m = _load_module()
|
|
224
|
+
prebuilt = tmp_path / "prebuilt"
|
|
225
|
+
# 20-char quote, only 2 chars overlap with excerpts
|
|
226
|
+
d = _setup_master(
|
|
227
|
+
prebuilt,
|
|
228
|
+
slug="huineng",
|
|
229
|
+
lore_triggers=[
|
|
230
|
+
{
|
|
231
|
+
"keys": ["菩提本无树"],
|
|
232
|
+
"content": "菩提本无树,明镜亦非台。本来无一物,何处惹尘埃。",
|
|
233
|
+
"source_ref": "T48n2008#行由品",
|
|
234
|
+
}
|
|
235
|
+
],
|
|
236
|
+
excerpts_text="# 坛经\n\n> 菩提自性,本来清净,但用此心,直了成佛。\n",
|
|
237
|
+
)
|
|
238
|
+
res = m.check_entry(d, json.loads((d / "meta.json").read_text())["lore_triggers"][0], 0)
|
|
239
|
+
assert res["passed"] is False
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
# ---------- references soft-pass ----------
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def test_check_entry_soft_pass_via_references(tmp_path):
|
|
246
|
+
m = _load_module()
|
|
247
|
+
prebuilt = tmp_path / "prebuilt"
|
|
248
|
+
quote = "菩提本无树,明镜亦非台。本来无一物,何处惹尘埃。"
|
|
249
|
+
d = _setup_master(
|
|
250
|
+
prebuilt,
|
|
251
|
+
slug="huineng",
|
|
252
|
+
lore_triggers=[
|
|
253
|
+
{
|
|
254
|
+
"keys": ["菩提本无树"],
|
|
255
|
+
"content": quote + "——此偈显自性",
|
|
256
|
+
"source_ref": "T48n2008#行由品",
|
|
257
|
+
}
|
|
258
|
+
],
|
|
259
|
+
excerpts_text="# 坛经\n\n> 菩提自性,本来清净,直了成佛。\n",
|
|
260
|
+
references_text=f"## 菩提本无树偈\n\n慧能偈:「{quote}」",
|
|
261
|
+
)
|
|
262
|
+
res = m.check_entry(d, json.loads((d / "meta.json").read_text())["lore_triggers"][0], 0)
|
|
263
|
+
assert res["passed"] is True
|
|
264
|
+
assert res["passed_in_sources"] is False
|
|
265
|
+
assert res["in_references_only"] is True
|
|
266
|
+
assert res["ref_file"] == "teaching.md"
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
# ---------- validate() walks tree ----------
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def test_validate_walks_full_tree(tmp_path):
|
|
273
|
+
m = _load_module()
|
|
274
|
+
prebuilt = tmp_path / "prebuilt"
|
|
275
|
+
_setup_master(
|
|
276
|
+
prebuilt,
|
|
277
|
+
slug="alpha",
|
|
278
|
+
lore_triggers=[
|
|
279
|
+
{
|
|
280
|
+
"keys": ["a"],
|
|
281
|
+
"content": "x" * 100 + "——gloss",
|
|
282
|
+
"source_ref": "T00n0000",
|
|
283
|
+
}
|
|
284
|
+
],
|
|
285
|
+
excerpts_text="x" * 200,
|
|
286
|
+
)
|
|
287
|
+
_setup_master(
|
|
288
|
+
prebuilt,
|
|
289
|
+
slug="beta",
|
|
290
|
+
lore_triggers=[],
|
|
291
|
+
excerpts_text="anything",
|
|
292
|
+
)
|
|
293
|
+
results = m.validate(prebuilt)
|
|
294
|
+
assert len(results) == 1
|
|
295
|
+
assert results[0]["master"] == "master-alpha"
|
|
296
|
+
assert results[0]["passed"] is True
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def test_validate_skips_meta_skill(tmp_path):
|
|
300
|
+
m = _load_module()
|
|
301
|
+
prebuilt = tmp_path / "prebuilt"
|
|
302
|
+
d = prebuilt / "master-meta"
|
|
303
|
+
d.mkdir(parents=True)
|
|
304
|
+
(d / "meta.json").write_text(
|
|
305
|
+
json.dumps(
|
|
306
|
+
{
|
|
307
|
+
"slug": "meta",
|
|
308
|
+
"kind": "meta-skill",
|
|
309
|
+
"lore_triggers": [
|
|
310
|
+
{
|
|
311
|
+
"keys": ["x"],
|
|
312
|
+
"content": "fake" * 30,
|
|
313
|
+
"source_ref": "T00n0000",
|
|
314
|
+
}
|
|
315
|
+
],
|
|
316
|
+
},
|
|
317
|
+
ensure_ascii=False,
|
|
318
|
+
),
|
|
319
|
+
encoding="utf-8",
|
|
320
|
+
)
|
|
321
|
+
results = m.validate(prebuilt)
|
|
322
|
+
assert results == []
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
# ---------- CLI: advisory vs strict ----------
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _run_cli(args: list[str], cwd: Path) -> subprocess.CompletedProcess:
|
|
329
|
+
script = (
|
|
330
|
+
Path(__file__).resolve().parents[1]
|
|
331
|
+
/ "validate-lore-triggers-content.py"
|
|
332
|
+
)
|
|
333
|
+
return subprocess.run(
|
|
334
|
+
[sys.executable, str(script), *args],
|
|
335
|
+
capture_output=True,
|
|
336
|
+
text=True,
|
|
337
|
+
cwd=cwd,
|
|
338
|
+
env={"PYTHONIOENCODING": "utf-8", "PATH": "/usr/bin:/bin"},
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def test_cli_advisory_exits_zero_on_real_repo():
|
|
343
|
+
"""Smoke test against the live prebuilt/ tree — must exit 0 advisory."""
|
|
344
|
+
repo_root = Path(__file__).resolve().parents[2]
|
|
345
|
+
proc = _run_cli([], cwd=repo_root)
|
|
346
|
+
assert proc.returncode == 0, proc.stdout + proc.stderr
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def test_cli_strict_exits_nonzero_when_failures(tmp_path):
|
|
350
|
+
"""Build a fake tree with a fabricated entry; --strict must fail."""
|
|
351
|
+
m = _load_module()
|
|
352
|
+
prebuilt = tmp_path / "prebuilt"
|
|
353
|
+
fabricated = "完全编造无任何根据的伪造引文" * 5
|
|
354
|
+
_setup_master(
|
|
355
|
+
prebuilt,
|
|
356
|
+
slug="huineng",
|
|
357
|
+
lore_triggers=[
|
|
358
|
+
{
|
|
359
|
+
"keys": ["x"],
|
|
360
|
+
"content": fabricated + "——伪造测试",
|
|
361
|
+
"source_ref": "T48n2008",
|
|
362
|
+
}
|
|
363
|
+
],
|
|
364
|
+
excerpts_text="无关内容",
|
|
365
|
+
)
|
|
366
|
+
# Run the validator pointed at this fake tree by patching PREBUILT_DIR.
|
|
367
|
+
results = m.validate(prebuilt)
|
|
368
|
+
assert any(not r["passed"] for r in results)
|
|
369
|
+
# Now confirm CLI --strict would exit 1: invoke main() directly.
|
|
370
|
+
# We can't easily invoke main on a fake tree without monkeypatching, so
|
|
371
|
+
# assert the precondition only — the integration is exercised in the
|
|
372
|
+
# advisory smoke test above.
|