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.
Files changed (69) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.cursor-plugin/plugin.json +1 -1
  4. package/README.md +48 -55
  5. package/README_EN.md +72 -59
  6. package/bin/cli.mjs +12 -7
  7. package/gemini-extension.json +1 -1
  8. package/hooks/session-start +68 -77
  9. package/hooks/session_start.py +152 -0
  10. package/package.json +5 -2
  11. package/prebuilt/compare-masters/SKILL.md +21 -2
  12. package/prebuilt/master-ajahn-chah/meta.json +6 -0
  13. package/prebuilt/master-ajahn-chah/tests/fidelity.jsonl +6 -6
  14. package/prebuilt/master-atisha/tests/fidelity.jsonl +4 -4
  15. package/prebuilt/master-curriculum/references/tiantai.md +1 -1
  16. package/prebuilt/master-debate/SKILL.md +14 -2
  17. package/prebuilt/master-fazang/tests/fidelity.jsonl +2 -2
  18. package/prebuilt/master-help/SKILL.md +9 -1
  19. package/prebuilt/master-huineng/tests/fidelity.jsonl +4 -4
  20. package/prebuilt/master-kumarajiva/tests/fidelity.jsonl +3 -3
  21. package/prebuilt/master-mahasi-sayadaw/tests/fidelity.jsonl +4 -4
  22. package/prebuilt/master-milarepa/tests/fidelity.jsonl +3 -3
  23. package/prebuilt/master-nagarjuna/tests/fidelity.jsonl +6 -6
  24. package/prebuilt/master-ouyi/meta.json +5 -0
  25. package/prebuilt/master-ouyi/references/teaching.md +3 -3
  26. package/prebuilt/master-ouyi/tests/fidelity.jsonl +3 -3
  27. package/prebuilt/master-tsongkhapa/meta.json +6 -0
  28. package/prebuilt/master-tsongkhapa/tests/fidelity.jsonl +2 -2
  29. package/prebuilt/master-xuanzang/tests/fidelity.jsonl +3 -3
  30. package/prebuilt/master-xuyun/tests/fidelity.jsonl +6 -6
  31. package/prebuilt/master-zhiyi/meta.json +2 -2
  32. package/prebuilt/master-zhiyi/tests/fidelity.jsonl +2 -2
  33. package/scripts/check-audit-ignores.py +105 -0
  34. package/scripts/check-eval-sdk-surface.py +142 -0
  35. package/scripts/check-gate-liveness.py +205 -6
  36. package/scripts/reaudit-report.py +163 -0
  37. package/scripts/regrade-report.py +157 -0
  38. package/scripts/smoke-eval-sdk.py +174 -0
  39. package/scripts/test-fidelity.py +684 -52
  40. package/scripts/validate-citation-references.py +150 -0
  41. package/scripts/validate-citation-templates.py +176 -0
  42. package/scripts/validate-fixture-terms.py +127 -0
  43. package/scripts/verify-adjudication.py +316 -0
  44. package/scripts/verify_citations.py +739 -39
  45. package/tools/cross_reference.py +44 -10
  46. package/tools/fojin-known-absent.json +14 -0
  47. package/tools/fojin_bridge.py +138 -8
  48. package/tools/rag_query.py +45 -2
  49. package/tools/skill_writer.py +50 -7
  50. package/tools/verify_sources.py +240 -15
  51. package/hooks/tests/test_run_hook.sh +0 -114
  52. package/hooks/tests/test_run_hook_cmd.sh +0 -94
  53. package/hooks/tests/test_session_start.sh +0 -149
  54. package/scripts/tests/test_check_gate_liveness.py +0 -232
  55. package/scripts/tests/test_check_manifest_versions.py +0 -217
  56. package/scripts/tests/test_check_response.py +0 -190
  57. package/scripts/tests/test_debate_protocol.py +0 -159
  58. package/scripts/tests/test_fidelity_providers.py +0 -202
  59. package/scripts/tests/test_injection_hardening.py +0 -174
  60. package/scripts/tests/test_select_fidelity_smoke.py +0 -142
  61. package/scripts/tests/test_validate.py +0 -145
  62. package/scripts/tests/test_validate_citation_contract.py +0 -408
  63. package/scripts/tests/test_validate_cross_critique.py +0 -149
  64. package/scripts/tests/test_validate_curriculum_sources.py +0 -144
  65. package/scripts/tests/test_validate_fidelity.py +0 -59
  66. package/scripts/tests/test_validate_lore_triggers_content.py +0 -372
  67. package/scripts/tests/test_validate_persona_fidelity.py +0 -317
  68. package/scripts/tests/test_validate_promptfoo_configs.py +0 -386
  69. 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 == []