master-skill 0.11.0 → 0.12.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 +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/README.md +48 -55
- package/README_EN.md +72 -59
- package/bin/cli.mjs +12 -7
- package/gemini-extension.json +1 -1
- package/hooks/session-start +68 -77
- package/hooks/session_start.py +152 -0
- package/package.json +5 -2
- package/prebuilt/compare-masters/SKILL.md +21 -2
- package/prebuilt/master-ajahn-chah/meta.json +6 -0
- package/prebuilt/master-ajahn-chah/tests/fidelity.jsonl +6 -6
- package/prebuilt/master-atisha/tests/fidelity.jsonl +4 -4
- package/prebuilt/master-curriculum/references/tiantai.md +1 -1
- package/prebuilt/master-debate/SKILL.md +14 -2
- package/prebuilt/master-fazang/tests/fidelity.jsonl +2 -2
- package/prebuilt/master-help/SKILL.md +9 -1
- package/prebuilt/master-huineng/tests/fidelity.jsonl +4 -4
- package/prebuilt/master-kumarajiva/tests/fidelity.jsonl +3 -3
- package/prebuilt/master-mahasi-sayadaw/tests/fidelity.jsonl +4 -4
- package/prebuilt/master-milarepa/tests/fidelity.jsonl +3 -3
- package/prebuilt/master-nagarjuna/tests/fidelity.jsonl +6 -6
- package/prebuilt/master-ouyi/meta.json +5 -0
- package/prebuilt/master-ouyi/references/teaching.md +3 -3
- package/prebuilt/master-ouyi/tests/fidelity.jsonl +3 -3
- package/prebuilt/master-tsongkhapa/meta.json +6 -0
- package/prebuilt/master-tsongkhapa/tests/fidelity.jsonl +2 -2
- package/prebuilt/master-xuanzang/tests/fidelity.jsonl +3 -3
- package/prebuilt/master-xuyun/tests/fidelity.jsonl +6 -6
- package/prebuilt/master-zhiyi/meta.json +2 -2
- package/prebuilt/master-zhiyi/tests/fidelity.jsonl +2 -2
- package/scripts/check-audit-ignores.py +105 -0
- package/scripts/check-eval-sdk-surface.py +142 -0
- package/scripts/check-gate-liveness.py +205 -6
- package/scripts/reaudit-report.py +163 -0
- package/scripts/regrade-report.py +157 -0
- package/scripts/smoke-eval-sdk.py +174 -0
- package/scripts/test-fidelity.py +684 -52
- package/scripts/validate-citation-references.py +150 -0
- package/scripts/validate-citation-templates.py +176 -0
- package/scripts/validate-fixture-terms.py +127 -0
- package/scripts/verify-adjudication.py +316 -0
- package/scripts/verify_citations.py +739 -39
- package/tools/cross_reference.py +44 -10
- package/tools/fojin-known-absent.json +14 -0
- package/tools/fojin_bridge.py +138 -8
- package/tools/rag_query.py +45 -2
- package/tools/skill_writer.py +50 -7
- package/tools/verify_sources.py +240 -15
- package/hooks/tests/test_run_hook.sh +0 -114
- package/hooks/tests/test_run_hook_cmd.sh +0 -94
- package/hooks/tests/test_session_start.sh +0 -149
- package/scripts/tests/test_check_gate_liveness.py +0 -232
- package/scripts/tests/test_check_manifest_versions.py +0 -217
- package/scripts/tests/test_check_response.py +0 -190
- package/scripts/tests/test_debate_protocol.py +0 -159
- package/scripts/tests/test_fidelity_providers.py +0 -202
- package/scripts/tests/test_injection_hardening.py +0 -174
- package/scripts/tests/test_select_fidelity_smoke.py +0 -142
- package/scripts/tests/test_validate.py +0 -145
- package/scripts/tests/test_validate_citation_contract.py +0 -408
- package/scripts/tests/test_validate_cross_critique.py +0 -149
- package/scripts/tests/test_validate_curriculum_sources.py +0 -144
- package/scripts/tests/test_validate_fidelity.py +0 -59
- package/scripts/tests/test_validate_lore_triggers_content.py +0 -372
- package/scripts/tests/test_validate_persona_fidelity.py +0 -317
- package/scripts/tests/test_validate_promptfoo_configs.py +0 -386
- package/scripts/tests/test_validate_workflow.py +0 -284
|
@@ -1,408 +0,0 @@
|
|
|
1
|
-
"""Tests for the cross-tradition citation contract validator."""
|
|
2
|
-
from __future__ import annotations
|
|
3
|
-
|
|
4
|
-
import importlib.util
|
|
5
|
-
import json
|
|
6
|
-
import sys
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
|
|
9
|
-
import pytest
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
@pytest.fixture
|
|
13
|
-
def validator():
|
|
14
|
-
script_path = Path(__file__).resolve().parents[1] / "validate-citation-contract.py"
|
|
15
|
-
spec = importlib.util.spec_from_file_location("validate_citation_contract", script_path)
|
|
16
|
-
module = importlib.util.module_from_spec(spec)
|
|
17
|
-
sys.modules["validate_citation_contract"] = module
|
|
18
|
-
spec.loader.exec_module(module)
|
|
19
|
-
return module
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
@pytest.fixture
|
|
23
|
-
def fake_tree(tmp_path: Path) -> Path:
|
|
24
|
-
return tmp_path / "prebuilt"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def contract(*types: str) -> dict:
|
|
28
|
-
return {
|
|
29
|
-
"version": 1,
|
|
30
|
-
"claim_policy": "declared_sources_only",
|
|
31
|
-
"required_for": [
|
|
32
|
-
"doctrinal_claim",
|
|
33
|
-
"practice_guidance",
|
|
34
|
-
"text_interpretation",
|
|
35
|
-
],
|
|
36
|
-
"allowed_source_types": sorted(types),
|
|
37
|
-
"minimum_claim_coverage": 0.9,
|
|
38
|
-
"live_retrieval_allowed": True,
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def write_meta(
|
|
43
|
-
prebuilt: Path,
|
|
44
|
-
slug: str,
|
|
45
|
-
source_types: list[str],
|
|
46
|
-
value: dict | None,
|
|
47
|
-
*,
|
|
48
|
-
kind: str | None = None,
|
|
49
|
-
) -> None:
|
|
50
|
-
directory = prebuilt / f"master-{slug}"
|
|
51
|
-
directory.mkdir(parents=True, exist_ok=True)
|
|
52
|
-
data = {
|
|
53
|
-
"slug": slug,
|
|
54
|
-
"sources": [
|
|
55
|
-
{"type": source_type, "id": f"id-{index}", "title": f"source-{index}"}
|
|
56
|
-
for index, source_type in enumerate(source_types)
|
|
57
|
-
],
|
|
58
|
-
}
|
|
59
|
-
if kind is not None:
|
|
60
|
-
data["kind"] = kind
|
|
61
|
-
if value is not None:
|
|
62
|
-
data["citation_contract"] = value
|
|
63
|
-
(directory / "meta.json").write_text(json.dumps(data), encoding="utf-8")
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
@pytest.mark.parametrize(
|
|
67
|
-
"source_types",
|
|
68
|
-
[
|
|
69
|
-
["cbeta"],
|
|
70
|
-
["tibetan_canon", "tibetan_treatise"],
|
|
71
|
-
["compiled_teaching", "pali_canon", "pali_treatise"],
|
|
72
|
-
],
|
|
73
|
-
)
|
|
74
|
-
def test_valid_source_family_contracts_pass(validator, fake_tree, source_types):
|
|
75
|
-
write_meta(fake_tree, "demo", source_types, contract(*source_types))
|
|
76
|
-
assert validator.validate(fake_tree) == []
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def test_missing_contract_fails(validator, fake_tree):
|
|
80
|
-
write_meta(fake_tree, "demo", ["cbeta"], None)
|
|
81
|
-
errors = validator.validate(fake_tree)
|
|
82
|
-
assert any(
|
|
83
|
-
"master-demo/meta.json" in error and "citation_contract" in error
|
|
84
|
-
for error in errors
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
@pytest.mark.parametrize("allowed", [[], ["cbeta", "pali_canon"]])
|
|
89
|
-
def test_source_type_drift_fails(validator, fake_tree, allowed):
|
|
90
|
-
value = contract("cbeta")
|
|
91
|
-
value["allowed_source_types"] = allowed
|
|
92
|
-
write_meta(fake_tree, "demo", ["cbeta"], value)
|
|
93
|
-
errors = validator.validate(fake_tree)
|
|
94
|
-
assert any(
|
|
95
|
-
"master-demo/meta.json" in error and "allowed_source_types" in error
|
|
96
|
-
for error in errors
|
|
97
|
-
)
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
@pytest.mark.parametrize(
|
|
101
|
-
("field", "invalid"),
|
|
102
|
-
[
|
|
103
|
-
("version", 2),
|
|
104
|
-
("claim_policy", "any_source"),
|
|
105
|
-
("required_for", ["doctrinal_claim"]),
|
|
106
|
-
("minimum_claim_coverage", True),
|
|
107
|
-
("minimum_claim_coverage", 0.8),
|
|
108
|
-
("live_retrieval_allowed", "yes"),
|
|
109
|
-
],
|
|
110
|
-
)
|
|
111
|
-
def test_fixed_contract_values_are_enforced(
|
|
112
|
-
validator,
|
|
113
|
-
fake_tree,
|
|
114
|
-
field,
|
|
115
|
-
invalid,
|
|
116
|
-
):
|
|
117
|
-
value = contract("cbeta")
|
|
118
|
-
value[field] = invalid
|
|
119
|
-
write_meta(fake_tree, "demo", ["cbeta"], value)
|
|
120
|
-
errors = validator.validate(fake_tree)
|
|
121
|
-
assert any(
|
|
122
|
-
"master-demo/meta.json" in error and field in error for error in errors
|
|
123
|
-
)
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
def test_meta_skill_without_sources_is_ignored(validator, fake_tree):
|
|
127
|
-
write_meta(fake_tree, "debate", [], None, kind="meta-skill")
|
|
128
|
-
assert validator.validate(fake_tree) == []
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
def test_meta_skill_must_not_declare_persona_contract(validator, fake_tree):
|
|
132
|
-
write_meta(
|
|
133
|
-
fake_tree,
|
|
134
|
-
"debate",
|
|
135
|
-
["cbeta"],
|
|
136
|
-
contract("cbeta"),
|
|
137
|
-
kind="meta-skill",
|
|
138
|
-
)
|
|
139
|
-
errors = validator.validate(fake_tree)
|
|
140
|
-
assert any(
|
|
141
|
-
"master-debate/meta.json" in error and "meta-skill" in error
|
|
142
|
-
for error in errors
|
|
143
|
-
)
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
def test_persona_without_sources_is_not_treated_as_meta_skill(validator, fake_tree):
|
|
147
|
-
write_meta(fake_tree, "demo", [], None)
|
|
148
|
-
errors = validator.validate(fake_tree)
|
|
149
|
-
assert any(
|
|
150
|
-
"master-demo/meta.json" in error and "sources" in error for error in errors
|
|
151
|
-
)
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
@pytest.mark.parametrize("mutation", ["missing", "extra"])
|
|
155
|
-
def test_contract_keys_are_exact(validator, fake_tree, mutation):
|
|
156
|
-
value = contract("cbeta")
|
|
157
|
-
if mutation == "missing":
|
|
158
|
-
del value["claim_policy"]
|
|
159
|
-
else:
|
|
160
|
-
value["unexpected"] = "value"
|
|
161
|
-
write_meta(fake_tree, "demo", ["cbeta"], value)
|
|
162
|
-
errors = validator.validate(fake_tree)
|
|
163
|
-
assert any(
|
|
164
|
-
"master-demo/meta.json" in error and "citation_contract" in error
|
|
165
|
-
for error in errors
|
|
166
|
-
)
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
@pytest.mark.parametrize("source_type", ["", " ", None, True])
|
|
170
|
-
def test_source_types_must_be_non_empty_strings(
|
|
171
|
-
validator,
|
|
172
|
-
fake_tree,
|
|
173
|
-
source_type,
|
|
174
|
-
):
|
|
175
|
-
write_meta(fake_tree, "demo", [source_type], contract("cbeta"))
|
|
176
|
-
errors = validator.validate(fake_tree)
|
|
177
|
-
assert any(
|
|
178
|
-
"master-demo/meta.json" in error and "sources[].type" in error
|
|
179
|
-
for error in errors
|
|
180
|
-
)
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
def test_cli_exit_codes_and_persona_count(
|
|
184
|
-
validator,
|
|
185
|
-
fake_tree,
|
|
186
|
-
monkeypatch,
|
|
187
|
-
capsys,
|
|
188
|
-
):
|
|
189
|
-
monkeypatch.setattr(validator, "PREBUILT_DIR", fake_tree)
|
|
190
|
-
monkeypatch.setattr(validator, "EXPECTED_PERSONA_SLUGS", ("demo",))
|
|
191
|
-
write_meta(fake_tree, "demo", ["cbeta"], None)
|
|
192
|
-
|
|
193
|
-
assert validator.main() == 1
|
|
194
|
-
assert "master-demo/meta.json" in capsys.readouterr().err
|
|
195
|
-
|
|
196
|
-
write_meta(fake_tree, "demo", ["cbeta"], contract("cbeta"))
|
|
197
|
-
assert validator.main() == 0
|
|
198
|
-
captured = capsys.readouterr()
|
|
199
|
-
assert captured.err == ""
|
|
200
|
-
assert captured.out.strip() == "citation contracts OK (1 personas)"
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
def test_repository_mode_fails_when_prebuilt_directory_is_missing(
|
|
204
|
-
validator,
|
|
205
|
-
tmp_path,
|
|
206
|
-
):
|
|
207
|
-
errors = validator.validate_repository(tmp_path / "missing")
|
|
208
|
-
assert any("prebuilt directory" in error for error in errors)
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
def test_repository_mode_fails_with_zero_personas(validator, fake_tree):
|
|
212
|
-
write_meta(fake_tree, "debate", [], None, kind="meta-skill")
|
|
213
|
-
errors = validator.validate_repository(fake_tree)
|
|
214
|
-
assert any("persona roster" in error and "missing" in error for error in errors)
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
def test_repository_mode_fails_with_fourteen_personas(validator, fake_tree):
|
|
218
|
-
for slug in validator.EXPECTED_PERSONA_SLUGS[:-1]:
|
|
219
|
-
write_meta(fake_tree, slug, ["cbeta"], contract("cbeta"))
|
|
220
|
-
|
|
221
|
-
errors = validator.validate_repository(fake_tree)
|
|
222
|
-
missing = validator.EXPECTED_PERSONA_SLUGS[-1]
|
|
223
|
-
assert any(
|
|
224
|
-
"persona roster" in error and f"master-{missing}" in error
|
|
225
|
-
for error in errors
|
|
226
|
-
)
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
def test_repository_mode_accepts_the_real_fifteen_persona_roster(validator):
|
|
230
|
-
repository = Path(__file__).resolve().parents[2]
|
|
231
|
-
assert validator.validate_repository(repository / "prebuilt") == []
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
def test_repository_wording_uses_declared_source_contract():
|
|
235
|
-
repository = Path(__file__).resolve().parents[2]
|
|
236
|
-
runtime_paths = [
|
|
237
|
-
repository / "SKILL.md",
|
|
238
|
-
repository / "prebuilt" / "compare-masters" / "SKILL.md",
|
|
239
|
-
repository / "prompts" / "doctrine_reviewer.md",
|
|
240
|
-
repository / "references" / "ethics-runtime.md",
|
|
241
|
-
repository / "references" / "source-conventions.md",
|
|
242
|
-
repository / "docs" / "PRD.md",
|
|
243
|
-
repository / "docs" / "v1-framework-roadmap.md",
|
|
244
|
-
repository / "ETHICS.md",
|
|
245
|
-
repository / "README.md",
|
|
246
|
-
repository / "README_EN.md",
|
|
247
|
-
repository / "CONTRIBUTING.md",
|
|
248
|
-
]
|
|
249
|
-
forbidden = [
|
|
250
|
-
"NO DOCTRINAL CLAIM WITHOUT CBETA CITATION",
|
|
251
|
-
"每位祖师的回答必须附 CBETA 引用",
|
|
252
|
-
"CBETA 经证覆盖率 ≥ 90%",
|
|
253
|
-
"primary_cbeta_ids 过滤结果",
|
|
254
|
-
"当前版本仅汉传",
|
|
255
|
-
"无 CBETA 经号的教义断言不得写入",
|
|
256
|
-
"每一条教义断言必须附一个**真实**的 CBETA 经号",
|
|
257
|
-
]
|
|
258
|
-
for path in runtime_paths:
|
|
259
|
-
content = path.read_text(encoding="utf-8")
|
|
260
|
-
for phrase in forbidden:
|
|
261
|
-
assert phrase not in content, (
|
|
262
|
-
f"{path}: stale source-family rule: {phrase}"
|
|
263
|
-
)
|
|
264
|
-
|
|
265
|
-
root_text = runtime_paths[0].read_text(encoding="utf-8")
|
|
266
|
-
compare_text = runtime_paths[1].read_text(encoding="utf-8")
|
|
267
|
-
reviewer_text = runtime_paths[2].read_text(encoding="utf-8")
|
|
268
|
-
conventions_text = runtime_paths[4].read_text(encoding="utf-8")
|
|
269
|
-
|
|
270
|
-
for text in (root_text, compare_text):
|
|
271
|
-
assert "NO DOCTRINAL CLAIM WITHOUT A DECLARED SOURCE CITATION" in text
|
|
272
|
-
assert "citation_contract.allowed_source_types" in text
|
|
273
|
-
assert "meta.json.sources[]" in text
|
|
274
|
-
assert "live_retrieval_allowed" in text
|
|
275
|
-
|
|
276
|
-
assert "primary_cbeta_ids" not in compare_text
|
|
277
|
-
assert "minimum_claim_coverage" in reviewer_text
|
|
278
|
-
assert "同等适用 citation contract" in conventions_text
|
|
279
|
-
contributing_text = runtime_paths[-1].read_text(encoding="utf-8")
|
|
280
|
-
assert "validate-citation-contract.py" in contributing_text
|
|
281
|
-
assert '"citation_contract"' in contributing_text
|
|
282
|
-
assert "meta.json.sources[]" in contributing_text
|
|
283
|
-
for source_family in (
|
|
284
|
-
"CBETA",
|
|
285
|
-
"BDRC / Toh",
|
|
286
|
-
"PTS / SuttaCentral",
|
|
287
|
-
"compiled teachings",
|
|
288
|
-
):
|
|
289
|
-
assert source_family in conventions_text
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
def test_non_cbeta_runtime_instructions_are_source_family_aware():
|
|
293
|
-
repository = Path(__file__).resolve().parents[2]
|
|
294
|
-
slugs = (
|
|
295
|
-
"atisha",
|
|
296
|
-
"tsongkhapa",
|
|
297
|
-
"milarepa",
|
|
298
|
-
"buddhaghosa",
|
|
299
|
-
"mahasi-sayadaw",
|
|
300
|
-
"ajahn-chah",
|
|
301
|
-
)
|
|
302
|
-
for slug in slugs:
|
|
303
|
-
persona = repository / "prebuilt" / f"master-{slug}"
|
|
304
|
-
meta = json.loads((persona / "meta.json").read_text(encoding="utf-8"))
|
|
305
|
-
assert "cbeta" not in meta["citation_contract"]["allowed_source_types"]
|
|
306
|
-
instructions = (persona / "SKILL.md").read_text(encoding="utf-8")
|
|
307
|
-
for stale in ("cbeta_id", "--sources cbeta", "以 CBETA 汉文为主"):
|
|
308
|
-
assert stale not in instructions, f"master-{slug}: stale {stale}"
|
|
309
|
-
for required in (
|
|
310
|
-
"source_type",
|
|
311
|
-
"source_id",
|
|
312
|
-
"meta.json.sources[]",
|
|
313
|
-
"citation_contract.allowed_source_types",
|
|
314
|
-
):
|
|
315
|
-
assert required in instructions, f"master-{slug}: missing {required}"
|
|
316
|
-
|
|
317
|
-
rag = (repository / "prompts" / "rag_instructions.md").read_text(
|
|
318
|
-
encoding="utf-8"
|
|
319
|
-
)
|
|
320
|
-
assert "--sources cbeta" not in rag
|
|
321
|
-
for required in (
|
|
322
|
-
"source_type",
|
|
323
|
-
"source_id",
|
|
324
|
-
"meta.json.sources[]",
|
|
325
|
-
"citation_contract.allowed_source_types",
|
|
326
|
-
):
|
|
327
|
-
assert required in rag
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
def test_live_retrieval_never_uses_sources_outside_the_declared_contract():
|
|
331
|
-
repository = Path(__file__).resolve().parents[2]
|
|
332
|
-
persona_skills = []
|
|
333
|
-
for path in sorted((repository / "prebuilt").glob("master-*/SKILL.md")):
|
|
334
|
-
meta_path = path.parent / "meta.json"
|
|
335
|
-
if not meta_path.is_file():
|
|
336
|
-
continue
|
|
337
|
-
metadata = json.loads(meta_path.read_text(encoding="utf-8"))
|
|
338
|
-
if isinstance(metadata.get("sources"), list) and metadata["sources"]:
|
|
339
|
-
persona_skills.append(path)
|
|
340
|
-
assert len(persona_skills) == 15
|
|
341
|
-
for path in persona_skills:
|
|
342
|
-
content = path.read_text(encoding="utf-8")
|
|
343
|
-
for stale in (
|
|
344
|
-
"声明经典之外",
|
|
345
|
-
"所列经典之外",
|
|
346
|
-
"三部经之外",
|
|
347
|
-
"上述三部经之外",
|
|
348
|
-
):
|
|
349
|
-
assert stale not in content, f"{path}: contradictory live trigger {stale}"
|
|
350
|
-
assert "先人工扩充 `sources[]` / citation contract" in content
|
|
351
|
-
assert "已声明来源" in content
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
def test_create_master_docs_match_the_real_generator_cli():
|
|
355
|
-
repository = Path(__file__).resolve().parents[2]
|
|
356
|
-
root = (repository / "SKILL.md").read_text(encoding="utf-8")
|
|
357
|
-
workflow = (repository / "references" / "workflow-details.md").read_text(
|
|
358
|
-
encoding="utf-8"
|
|
359
|
-
)
|
|
360
|
-
conventions = (repository / "references" / "source-conventions.md").read_text(
|
|
361
|
-
encoding="utf-8"
|
|
362
|
-
)
|
|
363
|
-
combined = root + workflow + conventions
|
|
364
|
-
assert "sutra_collector.py --name" in combined
|
|
365
|
-
assert "--output collected_data.json" in combined
|
|
366
|
-
assert "verify_sources.py --check-links collected_data.json" in combined
|
|
367
|
-
assert "master_builder.py --spec generated-master.json" in combined
|
|
368
|
-
assert "verify_sources.py --final-check" in combined
|
|
369
|
-
assert "masters/master-{slug}/" in root
|
|
370
|
-
assert "masters/master-{slug}/" in workflow
|
|
371
|
-
assert "masters/master-{slug}/" in conventions
|
|
372
|
-
assert "masters/{slug}/" not in combined
|
|
373
|
-
assert "生成器内存" in root
|
|
374
|
-
assert "master_builder.py --name" not in combined
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
def test_create_master_prompts_and_contribution_paths_are_source_neutral():
|
|
378
|
-
repository = Path(__file__).resolve().parents[2]
|
|
379
|
-
source_prompts = [
|
|
380
|
-
repository / "prompts" / "sutra_analyzer.md",
|
|
381
|
-
repository / "prompts" / "teaching_builder.md",
|
|
382
|
-
repository / "prompts" / "voice_builder.md",
|
|
383
|
-
]
|
|
384
|
-
for path in source_prompts:
|
|
385
|
-
content = path.read_text(encoding="utf-8")
|
|
386
|
-
for stale in (
|
|
387
|
-
"`cbeta_id`",
|
|
388
|
-
"`fojin_url`",
|
|
389
|
-
"所有经文引用必须附 FoJin 链接",
|
|
390
|
-
"https://fojin.app/texts/{text_id}",
|
|
391
|
-
):
|
|
392
|
-
assert stale not in content, f"{path}: stale {stale}"
|
|
393
|
-
assert "source_id" in content, f"{path}: missing source_id"
|
|
394
|
-
|
|
395
|
-
intake = (repository / "prompts" / "intake.md").read_text(encoding="utf-8")
|
|
396
|
-
for tradition in ("印度", "汉传", "藏传", "南传"):
|
|
397
|
-
assert tradition in intake
|
|
398
|
-
assert "当前版本仅汉传" not in intake
|
|
399
|
-
|
|
400
|
-
pull_request = (
|
|
401
|
-
repository / ".github" / "PULL_REQUEST_TEMPLATE.md"
|
|
402
|
-
).read_text(encoding="utf-8")
|
|
403
|
-
issue = (
|
|
404
|
-
repository / ".github" / "ISSUE_TEMPLATE" / "new_master.yml"
|
|
405
|
-
).read_text(encoding="utf-8")
|
|
406
|
-
for content in (pull_request, issue):
|
|
407
|
-
assert "声明来源" in content
|
|
408
|
-
assert "validate-citation-contract.py" in pull_request
|
|
@@ -1,149 +0,0 @@
|
|
|
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 == []
|