master-skill 0.5.0 → 0.8.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.
Files changed (156) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/.claude-plugin/plugin.json +2 -2
  3. package/.cursor-plugin/plugin.json +2 -2
  4. package/ETHICS.md +4 -4
  5. package/README.md +105 -51
  6. package/README_EN.md +105 -53
  7. package/SKILL.md +65 -308
  8. package/bin/cli.mjs +29 -8
  9. package/gemini-extension.json +2 -2
  10. package/hooks/session-start +40 -2
  11. package/hooks/tests/test_session_start.sh +149 -0
  12. package/package.json +7 -3
  13. package/prebuilt/compare/SKILL.md +44 -44
  14. package/prebuilt/compare/tests/fidelity.jsonl +13 -0
  15. package/prebuilt/{ajahn-chah → master-ajahn-chah}/SKILL.md +1 -1
  16. package/prebuilt/{ajahn-chah → master-ajahn-chah}/meta.json +17 -1
  17. package/prebuilt/{atisha → master-atisha}/SKILL.md +1 -1
  18. package/prebuilt/{atisha → master-atisha}/meta.json +17 -1
  19. package/prebuilt/{buddhaghosa → master-buddhaghosa}/SKILL.md +1 -1
  20. package/prebuilt/{buddhaghosa → master-buddhaghosa}/meta.json +26 -1
  21. package/prebuilt/master-curriculum/SKILL.md +87 -0
  22. package/prebuilt/master-curriculum/references/chan.md +29 -0
  23. package/prebuilt/master-curriculum/references/gelug-madhyamaka.md +33 -0
  24. package/prebuilt/master-curriculum/references/huayan.md +28 -0
  25. package/prebuilt/master-curriculum/references/jingtu.md +29 -0
  26. package/prebuilt/master-curriculum/references/sanlun-zhongguan.md +30 -0
  27. package/prebuilt/master-curriculum/references/theravada-vipassana.md +39 -0
  28. package/prebuilt/master-curriculum/references/tiantai.md +30 -0
  29. package/prebuilt/master-curriculum/references/weishi.md +31 -0
  30. package/prebuilt/master-curriculum/tests/fidelity.jsonl +8 -0
  31. package/prebuilt/master-debate/SKILL.md +274 -0
  32. package/prebuilt/master-debate/meta.json +25 -0
  33. package/prebuilt/master-debate/tests/fidelity.jsonl +8 -0
  34. package/prebuilt/{fazang → master-fazang}/SKILL.md +1 -1
  35. package/prebuilt/{fazang → master-fazang}/meta.json +25 -0
  36. package/prebuilt/{huineng → master-huineng}/SKILL.md +1 -1
  37. package/prebuilt/master-huineng/meta.json +128 -0
  38. package/prebuilt/{huineng → master-huineng}/references/teaching.md +1 -1
  39. package/prebuilt/{huineng → master-huineng}/sources/INDEX.md +1 -1
  40. package/prebuilt/{huineng → master-huineng}/sources/tanjing-excerpts.md +17 -1
  41. package/prebuilt/{kumarajiva → master-kumarajiva}/SKILL.md +1 -1
  42. package/prebuilt/{kumarajiva → master-kumarajiva}/meta.json +16 -0
  43. package/prebuilt/{kumarajiva → master-kumarajiva}/references/teaching.md +2 -2
  44. package/prebuilt/{mahasi-sayadaw → master-mahasi-sayadaw}/SKILL.md +1 -1
  45. package/prebuilt/{mahasi-sayadaw → master-mahasi-sayadaw}/meta.json +17 -1
  46. package/prebuilt/{milarepa → master-milarepa}/SKILL.md +1 -1
  47. package/prebuilt/{milarepa → master-milarepa}/meta.json +26 -1
  48. package/prebuilt/{ouyi → master-ouyi}/SKILL.md +1 -1
  49. package/prebuilt/{ouyi → master-ouyi}/meta.json +17 -0
  50. package/prebuilt/{ouyi → master-ouyi}/references/teaching.md +4 -4
  51. package/prebuilt/{ouyi → master-ouyi}/sources/INDEX.md +1 -1
  52. package/prebuilt/{ouyi → master-ouyi}/sources/jiaoguan-gangzong-excerpts.md +3 -3
  53. package/prebuilt/{tsongkhapa → master-tsongkhapa}/SKILL.md +1 -1
  54. package/prebuilt/{tsongkhapa → master-tsongkhapa}/meta.json +18 -1
  55. package/prebuilt/{xuanzang → master-xuanzang}/SKILL.md +1 -1
  56. package/prebuilt/{xuanzang → master-xuanzang}/meta.json +16 -0
  57. package/prebuilt/{xuanzang → master-xuanzang}/references/teaching.md +5 -5
  58. package/prebuilt/{xuanzang → master-xuanzang}/references/voice.md +1 -1
  59. package/prebuilt/{xuyun → master-xuyun}/SKILL.md +1 -1
  60. package/prebuilt/{xuyun → master-xuyun}/meta.json +39 -0
  61. package/prebuilt/{yinguang → master-yinguang}/SKILL.md +2 -2
  62. package/prebuilt/{yinguang → master-yinguang}/meta.json +17 -0
  63. package/prebuilt/{yinguang → master-yinguang}/references/teaching.md +7 -7
  64. package/prebuilt/{yinguang → master-yinguang}/references/voice.md +1 -1
  65. package/prebuilt/{yinguang → master-yinguang}/sources/INDEX.md +3 -3
  66. package/prebuilt/{yinguang → master-yinguang}/sources/wenchao-excerpts.md +4 -4
  67. package/prebuilt/{yinguang → master-yinguang}/sources/yihanbianfu-excerpts.md +3 -3
  68. package/prebuilt/{zhiyi → master-zhiyi}/SKILL.md +1 -1
  69. package/prebuilt/{zhiyi → master-zhiyi}/meta.json +28 -0
  70. package/prebuilt/{zhiyi → master-zhiyi}/references/teaching.md +2 -2
  71. package/scripts/check-manifest-versions.py +142 -0
  72. package/scripts/cite.py +1 -1
  73. package/scripts/query.py +1 -1
  74. package/scripts/test-fidelity.py +4 -4
  75. package/scripts/tests/test_check_manifest_versions.py +217 -0
  76. package/scripts/tests/test_debate_protocol.py +159 -0
  77. package/scripts/tests/test_validate_cross_critique.py +149 -0
  78. package/scripts/tests/test_validate_curriculum_sources.py +144 -0
  79. package/scripts/tests/test_validate_lore_triggers_content.py +372 -0
  80. package/scripts/tests/test_validate_persona_fidelity.py +317 -0
  81. package/scripts/tests/test_validate_promptfoo_configs.py +386 -0
  82. package/scripts/validate-cross-critique.py +137 -0
  83. package/scripts/validate-curriculum-sources.py +111 -0
  84. package/scripts/validate-fidelity.py +16 -1
  85. package/scripts/validate-lore-triggers-content.py +393 -0
  86. package/scripts/validate-persona-fidelity.py +210 -0
  87. package/scripts/validate-promptfoo-configs.py +383 -0
  88. package/scripts/validate.py +213 -5
  89. package/prebuilt/huineng/meta.json +0 -91
  90. /package/prebuilt/{ajahn-chah → master-ajahn-chah}/references/teaching.md +0 -0
  91. /package/prebuilt/{ajahn-chah → master-ajahn-chah}/references/voice.md +0 -0
  92. /package/prebuilt/{ajahn-chah → master-ajahn-chah}/sources/INDEX.md +0 -0
  93. /package/prebuilt/{ajahn-chah → master-ajahn-chah}/sources/sutta-excerpts.md +0 -0
  94. /package/prebuilt/{ajahn-chah → master-ajahn-chah}/sources/teachings-excerpts.md +0 -0
  95. /package/prebuilt/{ajahn-chah → master-ajahn-chah}/tests/fidelity.jsonl +0 -0
  96. /package/prebuilt/{atisha → master-atisha}/references/teaching.md +0 -0
  97. /package/prebuilt/{atisha → master-atisha}/references/voice.md +0 -0
  98. /package/prebuilt/{atisha → master-atisha}/sources/INDEX.md +0 -0
  99. /package/prebuilt/{atisha → master-atisha}/sources/bodhipathapradipa-excerpts.md +0 -0
  100. /package/prebuilt/{atisha → master-atisha}/tests/fidelity.jsonl +0 -0
  101. /package/prebuilt/{buddhaghosa → master-buddhaghosa}/references/teaching.md +0 -0
  102. /package/prebuilt/{buddhaghosa → master-buddhaghosa}/references/voice.md +0 -0
  103. /package/prebuilt/{buddhaghosa → master-buddhaghosa}/sources/INDEX.md +0 -0
  104. /package/prebuilt/{buddhaghosa → master-buddhaghosa}/sources/visuddhimagga-excerpts.md +0 -0
  105. /package/prebuilt/{buddhaghosa → master-buddhaghosa}/tests/fidelity.jsonl +0 -0
  106. /package/prebuilt/{fazang → master-fazang}/references/teaching.md +0 -0
  107. /package/prebuilt/{fazang → master-fazang}/references/voice.md +0 -0
  108. /package/prebuilt/{fazang → master-fazang}/sources/INDEX.md +0 -0
  109. /package/prebuilt/{fazang → master-fazang}/sources/jinshizi-excerpts.md +0 -0
  110. /package/prebuilt/{fazang → master-fazang}/sources/wujiao-zhang-excerpts.md +0 -0
  111. /package/prebuilt/{fazang → master-fazang}/tests/fidelity.jsonl +0 -0
  112. /package/prebuilt/{huineng → master-huineng}/references/voice.md +0 -0
  113. /package/prebuilt/{huineng → master-huineng}/sources/jingang-excerpts.md +0 -0
  114. /package/prebuilt/{huineng → master-huineng}/tests/fidelity.jsonl +0 -0
  115. /package/prebuilt/{kumarajiva → master-kumarajiva}/references/voice.md +0 -0
  116. /package/prebuilt/{kumarajiva → master-kumarajiva}/sources/INDEX.md +0 -0
  117. /package/prebuilt/{kumarajiva → master-kumarajiva}/sources/fahua-excerpts.md +0 -0
  118. /package/prebuilt/{kumarajiva → master-kumarajiva}/sources/jingang-bore-excerpts.md +0 -0
  119. /package/prebuilt/{kumarajiva → master-kumarajiva}/sources/zhonglun-excerpts.md +0 -0
  120. /package/prebuilt/{kumarajiva → master-kumarajiva}/tests/fidelity.jsonl +0 -0
  121. /package/prebuilt/{mahasi-sayadaw → master-mahasi-sayadaw}/references/teaching.md +0 -0
  122. /package/prebuilt/{mahasi-sayadaw → master-mahasi-sayadaw}/references/voice.md +0 -0
  123. /package/prebuilt/{mahasi-sayadaw → master-mahasi-sayadaw}/sources/INDEX.md +0 -0
  124. /package/prebuilt/{mahasi-sayadaw → master-mahasi-sayadaw}/sources/teachings-excerpts.md +0 -0
  125. /package/prebuilt/{mahasi-sayadaw → master-mahasi-sayadaw}/tests/fidelity.jsonl +0 -0
  126. /package/prebuilt/{milarepa → master-milarepa}/references/teaching.md +0 -0
  127. /package/prebuilt/{milarepa → master-milarepa}/references/voice.md +0 -0
  128. /package/prebuilt/{milarepa → master-milarepa}/sources/INDEX.md +0 -0
  129. /package/prebuilt/{milarepa → master-milarepa}/sources/grubum-excerpts.md +0 -0
  130. /package/prebuilt/{milarepa → master-milarepa}/sources/namthar-excerpts.md +0 -0
  131. /package/prebuilt/{milarepa → master-milarepa}/tests/fidelity.jsonl +0 -0
  132. /package/prebuilt/{ouyi → master-ouyi}/references/voice.md +0 -0
  133. /package/prebuilt/{ouyi → master-ouyi}/sources/mituo-yaojie-excerpts.md +0 -0
  134. /package/prebuilt/{ouyi → master-ouyi}/tests/fidelity.jsonl +0 -0
  135. /package/prebuilt/{tsongkhapa → master-tsongkhapa}/references/teaching.md +0 -0
  136. /package/prebuilt/{tsongkhapa → master-tsongkhapa}/references/voice.md +0 -0
  137. /package/prebuilt/{tsongkhapa → master-tsongkhapa}/sources/INDEX.md +0 -0
  138. /package/prebuilt/{tsongkhapa → master-tsongkhapa}/sources/lamrim-excerpts.md +0 -0
  139. /package/prebuilt/{tsongkhapa → master-tsongkhapa}/tests/fidelity.jsonl +0 -0
  140. /package/prebuilt/{xuanzang → master-xuanzang}/sources/INDEX.md +0 -0
  141. /package/prebuilt/{xuanzang → master-xuanzang}/sources/chengweishi-excerpts.md +0 -0
  142. /package/prebuilt/{xuanzang → master-xuanzang}/sources/xinjing-excerpts.md +0 -0
  143. /package/prebuilt/{xuanzang → master-xuanzang}/tests/fidelity.jsonl +0 -0
  144. /package/prebuilt/{xuyun → master-xuyun}/references/teaching.md +0 -0
  145. /package/prebuilt/{xuyun → master-xuyun}/references/voice.md +0 -0
  146. /package/prebuilt/{xuyun → master-xuyun}/sources/INDEX.md +0 -0
  147. /package/prebuilt/{xuyun → master-xuyun}/sources/lengyanjing-excerpts.md +0 -0
  148. /package/prebuilt/{xuyun → master-xuyun}/sources/tanjing-excerpts.md +0 -0
  149. /package/prebuilt/{xuyun → master-xuyun}/tests/fidelity.jsonl +0 -0
  150. /package/prebuilt/{yinguang → master-yinguang}/sources/jingtu-sanjing-excerpts.md +0 -0
  151. /package/prebuilt/{yinguang → master-yinguang}/tests/fidelity.jsonl +0 -0
  152. /package/prebuilt/{zhiyi → master-zhiyi}/references/voice.md +0 -0
  153. /package/prebuilt/{zhiyi → master-zhiyi}/sources/INDEX.md +0 -0
  154. /package/prebuilt/{zhiyi → master-zhiyi}/sources/fahua-xuanyi-excerpts.md +0 -0
  155. /package/prebuilt/{zhiyi → master-zhiyi}/sources/mohezhiguan-excerpts.md +0 -0
  156. /package/prebuilt/{zhiyi → master-zhiyi}/tests/fidelity.jsonl +0 -0
@@ -0,0 +1,159 @@
1
+ """Tests for master-debate meta.json debate_protocol schema.
2
+
3
+ Verifies:
4
+ 1. master-debate/meta.json exists, has a `debate_protocol` object
5
+ 2. Required scalar fields with valid types + sane ranges
6
+ 3. min_rounds <= default_rounds <= max_rounds
7
+ 4. per_pair_overrides keys are <slug_a>-vs-<slug_b> with both slugs being
8
+ real masters under prebuilt/
9
+ 5. Each pair listed in per_pair_overrides is also covered bidirectionally
10
+ by cross_critique (i.e. the per-pair list does not invent debate pairs
11
+ that v0.7.1 didn't actually arm with ammo)
12
+ 6. per_pair_overrides values have valid default_rounds in
13
+ [debate_protocol.min_rounds, debate_protocol.max_rounds]
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ from pathlib import Path
19
+
20
+ import pytest
21
+
22
+
23
+ REPO_ROOT = Path(__file__).resolve().parents[2]
24
+ PREBUILT = REPO_ROOT / "prebuilt"
25
+ DEBATE_META = PREBUILT / "master-debate" / "meta.json"
26
+
27
+
28
+ def _load_json(p: Path) -> dict:
29
+ return json.loads(p.read_text(encoding="utf-8"))
30
+
31
+
32
+ @pytest.fixture(scope="module")
33
+ def debate_meta():
34
+ assert DEBATE_META.exists(), f"missing {DEBATE_META}"
35
+ return _load_json(DEBATE_META)
36
+
37
+
38
+ @pytest.fixture(scope="module")
39
+ def protocol(debate_meta):
40
+ proto = debate_meta.get("debate_protocol")
41
+ assert isinstance(proto, dict), "debate_protocol must be an object"
42
+ return proto
43
+
44
+
45
+ @pytest.fixture(scope="module")
46
+ def known_slugs():
47
+ return {p.parent.name.removeprefix("master-")
48
+ for p in PREBUILT.glob("master-*/meta.json")}
49
+
50
+
51
+ def test_protocol_required_scalars(protocol):
52
+ for key, typ in [
53
+ ("default_rounds", int),
54
+ ("max_rounds", int),
55
+ ("min_rounds", int),
56
+ ("selector", str),
57
+ ("stop_on_consensus", bool),
58
+ ("subagent_isolation", bool),
59
+ ("moderator_summary", bool),
60
+ ]:
61
+ assert key in protocol, f"missing debate_protocol.{key}"
62
+ assert isinstance(protocol[key], typ), (
63
+ f"debate_protocol.{key} must be {typ.__name__}, "
64
+ f"got {type(protocol[key]).__name__}"
65
+ )
66
+
67
+
68
+ def test_protocol_round_ranges(protocol):
69
+ min_r = protocol["min_rounds"]
70
+ def_r = protocol["default_rounds"]
71
+ max_r = protocol["max_rounds"]
72
+ assert min_r >= 1, "min_rounds must be >= 1"
73
+ assert min_r <= def_r <= max_r, (
74
+ f"require min_rounds <= default_rounds <= max_rounds, "
75
+ f"got {min_r} <= {def_r} <= {max_r}"
76
+ )
77
+ assert max_r <= 10, "max_rounds sanity cap"
78
+
79
+
80
+ def test_protocol_selector_alternating(protocol):
81
+ assert protocol["selector"] == "alternating", (
82
+ "v0.8 only ships alternating selector"
83
+ )
84
+
85
+
86
+ def test_protocol_subagent_isolation_on(protocol):
87
+ assert protocol["subagent_isolation"] is True, (
88
+ "v0.8 requires subagent_isolation=true (the whole point of the refactor)"
89
+ )
90
+
91
+
92
+ def test_per_pair_overrides_is_object(protocol):
93
+ overrides = protocol.get("per_pair_overrides")
94
+ assert isinstance(overrides, dict), (
95
+ "debate_protocol.per_pair_overrides must be an object"
96
+ )
97
+
98
+
99
+ def test_per_pair_keys_well_formed_and_real_slugs(protocol, known_slugs):
100
+ overrides = protocol["per_pair_overrides"]
101
+ for key in overrides:
102
+ assert "-vs-" in key, f"per-pair key '{key}' must contain '-vs-'"
103
+ parts = key.split("-vs-")
104
+ assert len(parts) == 2, f"per-pair key '{key}' must split into exactly 2 slugs"
105
+ a, b = parts
106
+ assert a != b, f"per-pair key '{key}' self-pair"
107
+ assert a in known_slugs, f"per-pair key '{key}': slug '{a}' not a known master"
108
+ assert b in known_slugs, f"per-pair key '{key}': slug '{b}' not a known master"
109
+
110
+
111
+ def test_per_pair_keys_are_alphabetically_sorted(protocol):
112
+ """SKILL.md tells the orchestrator to compute the lookup key by sorting
113
+ the two slugs alphabetically. If a key here isn't sorted, the orchestrator
114
+ silently misses the override. Lock the convention in."""
115
+ for key in protocol["per_pair_overrides"]:
116
+ a, b = key.split("-vs-", 1)
117
+ assert a < b, (
118
+ f"per-pair key '{key}' must be alphabetically sorted "
119
+ f"('{a}' < '{b}'); orchestrators compute the key by sorting slugs."
120
+ )
121
+
122
+
123
+ def test_per_pair_default_rounds_in_protocol_range(protocol):
124
+ min_r = protocol["min_rounds"]
125
+ max_r = protocol["max_rounds"]
126
+ for key, val in protocol["per_pair_overrides"].items():
127
+ assert isinstance(val, dict), f"override for '{key}' must be object"
128
+ dr = val.get("default_rounds")
129
+ assert isinstance(dr, int), f"override '{key}'.default_rounds must be int"
130
+ assert min_r <= dr <= max_r, (
131
+ f"override '{key}'.default_rounds={dr} out of "
132
+ f"[{min_r}, {max_r}]"
133
+ )
134
+
135
+
136
+ def test_per_pair_overrides_cross_critique_covered(protocol):
137
+ """Each per-pair override must correspond to a pair both directions of
138
+ which appear in cross_critique entries — otherwise the override would
139
+ fire a debate the v0.7.1 ammo system didn't arm."""
140
+ overrides = protocol["per_pair_overrides"]
141
+
142
+ # Build {(src, tgt)} set from all cross_critique entries.
143
+ pairs = set()
144
+ for meta_path in PREBUILT.glob("master-*/meta.json"):
145
+ src = meta_path.parent.name.removeprefix("master-")
146
+ data = _load_json(meta_path)
147
+ for e in data.get("cross_critique", []) or []:
148
+ tgt = e.get("target_master")
149
+ if isinstance(tgt, str):
150
+ pairs.add((src, tgt))
151
+
152
+ for key in overrides:
153
+ a, b = key.split("-vs-", 1)
154
+ assert (a, b) in pairs, (
155
+ f"override '{key}': cross_critique missing direction {a}→{b}"
156
+ )
157
+ assert (b, a) in pairs, (
158
+ f"override '{key}': cross_critique missing direction {b}→{a}"
159
+ )
@@ -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)