master-skill 0.10.0 → 0.11.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 (102) 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/ETHICS.md +23 -17
  5. package/GEMINI.md +1 -1
  6. package/README.md +57 -295
  7. package/README_EN.md +59 -276
  8. package/SKILL.md +5 -5
  9. package/bin/cli.mjs +545 -78
  10. package/gemini-extension.json +1 -1
  11. package/hooks/run-hook.cmd +18 -5
  12. package/hooks/session-start +4 -1
  13. package/hooks/tests/test_run_hook.sh +114 -0
  14. package/hooks/tests/test_run_hook_cmd.sh +94 -0
  15. package/masters/.gitkeep +0 -0
  16. package/package.json +10 -3
  17. package/prebuilt/{compare → compare-masters}/SKILL.md +5 -5
  18. package/prebuilt/master-ajahn-chah/SKILL.md +13 -11
  19. package/prebuilt/master-ajahn-chah/meta.json +8 -0
  20. package/prebuilt/master-ajahn-chah/references/voice.md +1 -1
  21. package/prebuilt/master-atisha/SKILL.md +13 -11
  22. package/prebuilt/master-atisha/meta.json +8 -0
  23. package/prebuilt/master-atisha/references/voice.md +1 -1
  24. package/prebuilt/master-buddhaghosa/SKILL.md +13 -11
  25. package/prebuilt/master-buddhaghosa/meta.json +8 -0
  26. package/prebuilt/master-buddhaghosa/references/voice.md +1 -1
  27. package/prebuilt/master-curriculum/SKILL.md +1 -1
  28. package/prebuilt/master-debate/SKILL.md +1 -1
  29. package/prebuilt/master-fazang/SKILL.md +3 -3
  30. package/prebuilt/master-fazang/meta.json +8 -0
  31. package/prebuilt/master-help/SKILL.md +86 -0
  32. package/prebuilt/master-help/tests/fidelity.jsonl +10 -0
  33. package/prebuilt/master-huineng/SKILL.md +3 -3
  34. package/prebuilt/master-huineng/meta.json +8 -0
  35. package/prebuilt/master-kumarajiva/SKILL.md +3 -3
  36. package/prebuilt/master-kumarajiva/meta.json +20 -1
  37. package/prebuilt/master-mahasi-sayadaw/SKILL.md +13 -11
  38. package/prebuilt/master-mahasi-sayadaw/meta.json +8 -0
  39. package/prebuilt/master-mahasi-sayadaw/references/voice.md +2 -2
  40. package/prebuilt/master-milarepa/SKILL.md +13 -11
  41. package/prebuilt/master-milarepa/meta.json +8 -0
  42. package/prebuilt/master-milarepa/references/voice.md +1 -1
  43. package/prebuilt/master-nagarjuna/SKILL.md +3 -3
  44. package/prebuilt/master-nagarjuna/meta.json +25 -2
  45. package/prebuilt/master-ouyi/SKILL.md +3 -3
  46. package/prebuilt/master-ouyi/meta.json +8 -0
  47. package/prebuilt/master-tsongkhapa/SKILL.md +13 -11
  48. package/prebuilt/master-tsongkhapa/meta.json +32 -3
  49. package/prebuilt/master-tsongkhapa/references/voice.md +1 -1
  50. package/prebuilt/master-xuanzang/SKILL.md +3 -3
  51. package/prebuilt/master-xuanzang/meta.json +8 -0
  52. package/prebuilt/master-xuyun/SKILL.md +3 -3
  53. package/prebuilt/master-xuyun/meta.json +8 -0
  54. package/prebuilt/master-yinguang/SKILL.md +3 -3
  55. package/prebuilt/master-yinguang/meta.json +8 -0
  56. package/prebuilt/master-zhiyi/SKILL.md +3 -3
  57. package/prebuilt/master-zhiyi/meta.json +8 -0
  58. package/prompts/correction_handler.md +104 -0
  59. package/prompts/doctrine_reviewer.md +61 -0
  60. package/prompts/intake.md +62 -0
  61. package/prompts/merger.md +62 -0
  62. package/prompts/rag_instructions.md +54 -0
  63. package/prompts/sutra_analyzer.md +83 -0
  64. package/prompts/teaching_builder.md +41 -0
  65. package/prompts/voice_analyzer.md +92 -0
  66. package/prompts/voice_builder.md +48 -0
  67. package/prompts/voice_reviewer.md +66 -0
  68. package/references/README.md +12 -0
  69. package/references/ethics-runtime.md +112 -0
  70. package/references/fojin-api.md +223 -0
  71. package/references/source-conventions.md +129 -0
  72. package/references/teaching-modes.md +91 -0
  73. package/references/traditions.md +72 -0
  74. package/references/workflow-details.md +361 -0
  75. package/requirements.txt +6 -0
  76. package/routing.json +209 -0
  77. package/scripts/check-gate-liveness.py +222 -0
  78. package/scripts/select-fidelity-smoke.py +78 -0
  79. package/scripts/test-fidelity.py +339 -51
  80. package/scripts/tests/test_check_gate_liveness.py +232 -0
  81. package/scripts/tests/test_check_response.py +190 -0
  82. package/scripts/tests/test_fidelity_providers.py +202 -0
  83. package/scripts/tests/test_select_fidelity_smoke.py +142 -0
  84. package/scripts/tests/test_validate.py +145 -0
  85. package/scripts/tests/test_validate_citation_contract.py +408 -0
  86. package/scripts/tests/test_validate_fidelity.py +2 -2
  87. package/scripts/tests/test_validate_workflow.py +284 -0
  88. package/scripts/validate-citation-contract.py +193 -0
  89. package/scripts/validate-fidelity.py +6 -1
  90. package/scripts/validate-routing.py +254 -0
  91. package/scripts/validate.py +63 -36
  92. package/scripts/verify_citations.py +8 -1
  93. package/skill-catalog.json +210 -0
  94. package/tools/cross_reference.py +365 -0
  95. package/tools/fojin_bridge.py +146 -0
  96. package/tools/master_builder.py +341 -0
  97. package/tools/rag_query.py +336 -0
  98. package/tools/skill_writer.py +230 -0
  99. package/tools/sutra_collector.py +237 -0
  100. package/tools/verify_sources.py +512 -0
  101. package/tools/version_manager.py +88 -0
  102. /package/prebuilt/{compare → compare-masters}/tests/fidelity.jsonl +0 -0
@@ -0,0 +1,254 @@
1
+ #!/usr/bin/env python3
2
+ """Validate routing.json — the machine-readable master/mode routing table.
3
+
4
+ `master-skill recommend` and the `/master-help` skill both route off this
5
+ file. It exists because the routing knowledge used to live only as prose:
6
+ a weighted-match paragraph and a 24-row pairing table inside
7
+ prebuilt/compare-masters/SKILL.md, plus a decision tree in
8
+ references/teaching-modes.md. Prose cannot be executed and cannot drift-check
9
+ itself — the original pairing table shipped three key collisions (`戒律`,
10
+ `道次第`, `中观/空性` each matched two or three rows), so which pairing a
11
+ query landed on depended on iteration order.
12
+
13
+ The central invariant this script enforces is therefore **pairwise
14
+ disjointness**: within `mode_rules`, and within `topic_pairings`, no keyword
15
+ may appear in two rows, and no keyword may be a substring of a keyword in
16
+ another row. Substring matters because `recommend` matches by containment —
17
+ if row A had `道次第` and row B had `菩提道次第`, a query mentioning the
18
+ latter would match both and the winner would be positional. Making that a CI
19
+ error forces collisions to be resolved when the data is authored.
20
+
21
+ Keyword data for personas is deliberately NOT duplicated into routing.json;
22
+ it stays in each prebuilt/<slug>/meta.json `search_scope.keywords`, so this
23
+ script also checks that every persona still carries usable keywords.
24
+
25
+ Checks
26
+ ------
27
+ 1. version == 1 and the three top-level sections are well-formed
28
+ 2. every mode in `mode_rules` is a `kind: teaching-mode` skill in
29
+ skill-catalog.json
30
+ 3. every master slug in `topic_pairings` / `default_pairing` is a
31
+ `kind: persona` skill in skill-catalog.json
32
+ 4. `mode_rules` keyword sets are pairwise disjoint (incl. substrings)
33
+ 5. `topic_pairings` keyword sets are pairwise disjoint (incl. substrings)
34
+ 6. `mode_rules` `order` values are exactly 1..N with no gaps or ties
35
+ 7. every catalog persona is reachable from at least one pairing or the
36
+ default pairing (no master can become unrecommendable)
37
+ 8. every catalog persona has a non-empty search_scope.keywords
38
+
39
+ Usage
40
+ -----
41
+ python scripts/validate-routing.py # exit 1 on any problem
42
+ python scripts/validate-routing.py --json
43
+ """
44
+ from __future__ import annotations
45
+
46
+ import argparse
47
+ import json
48
+ import sys
49
+ from pathlib import Path
50
+
51
+ ROOT = Path(__file__).resolve().parent.parent
52
+ ROUTING_PATH = ROOT / "routing.json"
53
+ CATALOG_PATH = ROOT / "skill-catalog.json"
54
+ PREBUILT = ROOT / "prebuilt"
55
+
56
+
57
+ def _read_json(p: Path):
58
+ try:
59
+ return json.loads(p.read_text(encoding="utf-8"))
60
+ except (json.JSONDecodeError, OSError) as err:
61
+ return {"__error__": f"{p.name}: {err}"}
62
+
63
+
64
+ def _disjoint_problems(section: str, rows: list) -> list:
65
+ """Report keyword collisions across rows.
66
+
67
+ Two keywords collide when they are equal or one contains the other.
68
+ Collisions inside a single row are fine (`观智` alongside `十六观智`
69
+ is a deliberate broadening); collisions across rows are not, because
70
+ they make the match order-dependent.
71
+ """
72
+ problems = []
73
+ flat = []
74
+ for row in rows:
75
+ label = row.get("id") or row.get("mode") or "<unnamed>"
76
+ for kw in row.get("keywords", []):
77
+ flat.append((label, kw))
78
+
79
+ for i, (label_a, kw_a) in enumerate(flat):
80
+ for label_b, kw_b in flat[i + 1:]:
81
+ if label_a == label_b:
82
+ continue
83
+ if kw_a == kw_b:
84
+ problems.append(
85
+ f"{section}: keyword {kw_a!r} appears in both "
86
+ f"{label_a!r} and {label_b!r}"
87
+ )
88
+ elif kw_a in kw_b or kw_b in kw_a:
89
+ shorter, longer = sorted((kw_a, kw_b), key=len)
90
+ problems.append(
91
+ f"{section}: keyword {shorter!r} ({label_a!r}) is a "
92
+ f"substring of {longer!r} ({label_b!r}) — a query "
93
+ f"matching the longer one would match both"
94
+ )
95
+ return problems
96
+
97
+
98
+ def validate(root: Path = ROOT) -> list:
99
+ problems = []
100
+
101
+ routing = _read_json(root / "routing.json")
102
+ if "__error__" in routing:
103
+ return [f"cannot read routing.json ({routing['__error__']})"]
104
+ catalog = _read_json(root / "skill-catalog.json")
105
+ if "__error__" in catalog:
106
+ return [f"cannot read skill-catalog.json ({catalog['__error__']})"]
107
+
108
+ # 1 — shape
109
+ if routing.get("version") != 1:
110
+ problems.append("routing.json: version must be 1")
111
+
112
+ mode_rules = routing.get("mode_rules")
113
+ pairings = routing.get("topic_pairings")
114
+ default_pairing = routing.get("default_pairing")
115
+ if not isinstance(mode_rules, list) or not mode_rules:
116
+ problems.append("routing.json: mode_rules must be a non-empty array")
117
+ mode_rules = []
118
+ if not isinstance(pairings, list) or not pairings:
119
+ problems.append("routing.json: topic_pairings must be a non-empty array")
120
+ pairings = []
121
+ if not isinstance(default_pairing, list) or not default_pairing:
122
+ problems.append("routing.json: default_pairing must be a non-empty array")
123
+ default_pairing = []
124
+
125
+ skills = catalog.get("skills", [])
126
+ personas = {s["name"] for s in skills if s.get("kind") == "persona"}
127
+ modes = {s["name"] for s in skills if s.get("kind") == "teaching-mode"}
128
+
129
+ # 2 — modes resolve
130
+ for rule in mode_rules:
131
+ mode = rule.get("mode")
132
+ if mode not in modes:
133
+ problems.append(
134
+ f"mode_rules: {mode!r} is not a kind:teaching-mode skill in "
135
+ f"skill-catalog.json (known: {sorted(modes)})"
136
+ )
137
+ if not rule.get("keywords"):
138
+ problems.append(f"mode_rules: {mode!r} has no keywords")
139
+
140
+ # 3 — masters resolve
141
+ situations = routing.get("situations") or []
142
+ if not isinstance(situations, list):
143
+ problems.append("routing.json: situations must be an array")
144
+ situations = []
145
+
146
+ referenced = set()
147
+ for section, rows in (("topic_pairings", pairings), ("situations", situations)):
148
+ for row in rows:
149
+ rid = row.get("id", "<unnamed>")
150
+ if not row.get("keywords"):
151
+ problems.append(f"{section}: {rid!r} has no keywords")
152
+ row_masters = row.get("masters", [])
153
+ if not row_masters:
154
+ problems.append(f"{section}: {rid!r} has no masters")
155
+ for slug in row_masters:
156
+ referenced.add(slug)
157
+ if slug not in personas:
158
+ problems.append(
159
+ f"{section}: {rid!r} references {slug!r}, which is "
160
+ f"not a kind:persona skill in skill-catalog.json"
161
+ )
162
+ for slug in default_pairing:
163
+ referenced.add(slug)
164
+ if slug not in personas:
165
+ problems.append(
166
+ f"default_pairing: {slug!r} is not a kind:persona skill in "
167
+ f"skill-catalog.json"
168
+ )
169
+
170
+ # 4 / 5 — disjointness within each section
171
+ problems += _disjoint_problems("mode_rules", mode_rules)
172
+ problems += _disjoint_problems("topic_pairings", pairings)
173
+ problems += _disjoint_problems("situations", situations)
174
+
175
+ # 5b — a situation keyword that also triggers a mode is dead code: the
176
+ # mode layer short-circuits first, so the situation row can never fire.
177
+ mode_kws = {kw for r in mode_rules for kw in r.get("keywords", [])}
178
+ for row in situations:
179
+ rid = row.get("id", "<unnamed>")
180
+ for kw in row.get("keywords", []):
181
+ for mkw in mode_kws:
182
+ if kw == mkw or kw in mkw or mkw in kw:
183
+ problems.append(
184
+ f"situations: {rid!r} keyword {kw!r} collides with "
185
+ f"mode_rules keyword {mkw!r} — mode_rules is evaluated "
186
+ f"first, so this situation row is unreachable"
187
+ )
188
+
189
+ # 6 — order is a clean 1..N
190
+ orders = [r.get("order") for r in mode_rules]
191
+ if sorted(o for o in orders if isinstance(o, int)) != list(
192
+ range(1, len(mode_rules) + 1)
193
+ ):
194
+ problems.append(
195
+ f"mode_rules: order values must be exactly 1..{len(mode_rules)} "
196
+ f"with no gaps or ties (got {orders})"
197
+ )
198
+
199
+ # 7 — no unreachable persona
200
+ for slug in sorted(personas - referenced):
201
+ problems.append(
202
+ f"coverage: persona {slug!r} appears in no topic_pairing or "
203
+ f"situation and is not in default_pairing — it can never be "
204
+ f"recommended"
205
+ )
206
+
207
+ # 8 — persona keywords still exist (recommend scores off them)
208
+ for slug in sorted(personas):
209
+ meta_path = PREBUILT / slug / "meta.json"
210
+ if not meta_path.exists():
211
+ problems.append(f"keywords: {slug} has no meta.json")
212
+ continue
213
+ meta = _read_json(meta_path)
214
+ if "__error__" in meta:
215
+ problems.append(f"keywords: {slug} meta.json unreadable")
216
+ continue
217
+ kws = (meta.get("search_scope") or {}).get("keywords")
218
+ if not kws:
219
+ problems.append(
220
+ f"keywords: {slug} has empty search_scope.keywords — "
221
+ f"`recommend` cannot score it"
222
+ )
223
+
224
+ return problems
225
+
226
+
227
+ def main() -> int:
228
+ ap = argparse.ArgumentParser(description=__doc__)
229
+ ap.add_argument("--json", action="store_true", help="machine-readable output")
230
+ args = ap.parse_args()
231
+
232
+ problems = validate()
233
+
234
+ if args.json:
235
+ print(json.dumps({"ok": not problems, "problems": problems}, indent=2))
236
+ elif problems:
237
+ print(f"routing.json validation failed ({len(problems)} problem(s)):\n")
238
+ for p in problems:
239
+ print(f" ✗ {p}")
240
+ print()
241
+ else:
242
+ routing = _read_json(ROUTING_PATH)
243
+ print(
244
+ f"routing.json ok — {len(routing.get('mode_rules', []))} mode rules, "
245
+ f"{len(routing.get('situations', []))} situations, "
246
+ f"{len(routing.get('topic_pairings', []))} topic pairings, "
247
+ f"all keyword sets pairwise disjoint."
248
+ )
249
+
250
+ return 1 if problems else 0
251
+
252
+
253
+ if __name__ == "__main__":
254
+ sys.exit(main())
@@ -14,10 +14,11 @@ from __future__ import annotations
14
14
 
15
15
  import argparse
16
16
  import json
17
- import re
18
17
  import sys
19
18
  from pathlib import Path
20
19
 
20
+ import yaml
21
+
21
22
  PREBUILT_DIR = Path(__file__).resolve().parent.parent / "prebuilt"
22
23
 
23
24
  # --- Required and recommended fields ---
@@ -48,41 +49,20 @@ def parse_frontmatter(path: Path) -> tuple[dict, str, list[str]]:
48
49
  if end is None:
49
50
  return {}, text, lines
50
51
 
51
- # Minimal YAML parse (no pyyaml dependency)
52
- fm: dict = {}
53
- current_key = None
54
- current_list: list | None = None
55
- for line in lines[1:end]:
56
- # list item
57
- if line.startswith(" - ") and current_key:
58
- if current_list is None:
59
- current_list = []
60
- item = line.strip().lstrip("- ").strip()
61
- # Try inline dict (title: xxx)
62
- if ":" in item:
63
- parts = item.split(":", 1)
64
- if current_list and isinstance(current_list[-1], dict):
65
- current_list[-1][parts[0].strip()] = parts[1].strip()
66
- else:
67
- current_list.append({parts[0].strip(): parts[1].strip()})
68
- else:
69
- current_list.append(item)
70
- continue
71
- # Save accumulated list
72
- if current_list is not None and current_key:
73
- fm[current_key] = current_list
74
- current_list = None
75
- # key: value
76
- match = re.match(r"^(\w[\w_-]*):\s*(.*)", line)
77
- if match:
78
- current_key = match.group(1)
79
- value = match.group(2).strip().strip('"').strip("'")
80
- if value:
81
- fm[current_key] = value
82
- # If empty value, might be a list starting next line
83
- # Flush last list
84
- if current_list is not None and current_key:
85
- fm[current_key] = current_list
52
+ # This was a hand-rolled parser, from back when pyyaml was not a
53
+ # dependency. It matched list items only as ` - `, so a 4-space
54
+ # continuation line like ` cbeta_id: T48n2008` matched neither that nor
55
+ # the `key:` regex (which is anchored at column 0) and fell through to the
56
+ # list-flush branch — clearing the accumulated list on every continuation
57
+ # and letting the next ` - ` overwrite it. Every master kept exactly one
58
+ # source and no cbeta_id at all, so the sources[] rules below inspected
59
+ # data that was never in the file.
60
+ try:
61
+ fm = yaml.safe_load("\n".join(lines[1:end])) or {}
62
+ except yaml.YAMLError as exc:
63
+ raise ValueError(f"{path}: invalid YAML frontmatter — {exc}") from exc
64
+ if not isinstance(fm, dict):
65
+ raise ValueError(f"{path}: frontmatter is not a mapping")
86
66
 
87
67
  body = "\n".join(lines[end + 1 :])
88
68
  return fm, body, lines
@@ -274,6 +254,32 @@ def _run_promptfoo_configs_subcheck() -> list[str]:
274
254
  return [f"promptfoo-configs sub-check failed to run: {exc}"]
275
255
 
276
256
 
257
+ def _run_curriculum_sources_subcheck() -> list[str]:
258
+ """Run the curriculum source validator as a sub-check.
259
+
260
+ master-curriculum/SKILL.md claims CI enforces this, but the script was
261
+ wired into no workflow, no npm script and no sub-check — only its own unit
262
+ tests, which build synthetic trees under tmp_path. It has never run against
263
+ the real references/, so a curriculum recommending a sutra no master
264
+ declares would have shipped unnoticed. Returns a list of error strings.
265
+ """
266
+ curriculum_dir = PREBUILT_DIR / "master-curriculum"
267
+ if not curriculum_dir.exists():
268
+ return []
269
+ try:
270
+ import importlib.util
271
+
272
+ spec_path = (
273
+ Path(__file__).resolve().parent / "validate-curriculum-sources.py"
274
+ )
275
+ spec = importlib.util.spec_from_file_location("vcs", spec_path)
276
+ mod = importlib.util.module_from_spec(spec)
277
+ spec.loader.exec_module(mod)
278
+ return mod.validate(PREBUILT_DIR)
279
+ except Exception as exc: # pragma: no cover — surfaces to user
280
+ return [f"curriculum-sources sub-check failed to run: {exc}"]
281
+
282
+
277
283
  def main():
278
284
  parser = argparse.ArgumentParser(description="Master-skill SKILL.md linter")
279
285
  parser.add_argument("--master", type=str, help="Lint a specific master only")
@@ -294,6 +300,11 @@ def main():
294
300
  action="store_true",
295
301
  help="Skip the v0.8 manifest version-drift gate",
296
302
  )
303
+ parser.add_argument(
304
+ "--skip-curriculum-sources",
305
+ action="store_true",
306
+ help="Skip the curriculum source gate",
307
+ )
297
308
  parser.add_argument(
298
309
  "--skip-lore-triggers-content",
299
310
  action="store_true",
@@ -340,6 +351,13 @@ def main():
340
351
  if manifest_errors:
341
352
  has_errors = True
342
353
 
354
+ # --- curriculum source gate (full-tree only, HARD gate) ---
355
+ curriculum_errors: list[str] = []
356
+ if not args.master and not args.skip_curriculum_sources:
357
+ curriculum_errors = _run_curriculum_sources_subcheck()
358
+ if curriculum_errors:
359
+ has_errors = True
360
+
343
361
  # --- v0.8 lore_triggers-content advisory sub-check ---
344
362
  # ADVISORY ONLY: warnings printed but never affect has_errors.
345
363
  lore_warnings: list[str] = []
@@ -354,6 +372,8 @@ def main():
354
372
  out["promptfoo_configs"] = promptfoo_errors
355
373
  if manifest_errors:
356
374
  out["manifest_versions"] = manifest_errors
375
+ if curriculum_errors:
376
+ out["curriculum_sources"] = curriculum_errors
357
377
  if lore_warnings:
358
378
  out["lore_triggers_content_advisory"] = lore_warnings
359
379
  print(json.dumps(out, indent=2, ensure_ascii=False))
@@ -363,6 +383,7 @@ def main():
363
383
  and not persona_errors
364
384
  and not promptfoo_errors
365
385
  and not manifest_errors
386
+ and not curriculum_errors
366
387
  and not lore_warnings
367
388
  )
368
389
  if nothing_to_report:
@@ -386,6 +407,11 @@ def main():
386
407
  print("Manifest version-drift gate (v0.8):")
387
408
  for e in manifest_errors:
388
409
  print(f" [ERROR] {e}")
410
+ if curriculum_errors:
411
+ print()
412
+ print("Curriculum source gate:")
413
+ for e in curriculum_errors:
414
+ print(f" [ERROR] {e}")
389
415
  if lore_warnings:
390
416
  print()
391
417
  print(
@@ -401,6 +427,7 @@ def main():
401
427
  len(persona_errors)
402
428
  + len(promptfoo_errors)
403
429
  + len(manifest_errors)
430
+ + len(curriculum_errors)
404
431
  )
405
432
  total_warns += len(lore_warnings)
406
433
  print(
@@ -33,7 +33,14 @@ _SAFE_MASTER = re.compile(r"^[A-Za-z0-9_-]+$")
33
33
 
34
34
  # CBETA id 形态:T48n2008 / T08n0235(藏经卷+n+编号),及 API 返回的 X1218 / X0303
35
35
  # (无卷号)。无 `n` 的形态只认 T/X 两个集合,避免误吞 Wikidata 的 Q1234 / P5008。
36
- _CBETA_ID = re.compile(r"\b(?:[A-Z]{1,2}\d+n\d+|[TX]\d{3,})\b")
36
+ #
37
+ # 边界不能用 \b:Python 的 \w 覆盖 CJK,故「卷一T99n9999」的「一」与「T」之间
38
+ # 没有 \b,整块引文会被 audit_answer 当作无 id 跳过 —— 而格式跑偏正是模型最可能
39
+ # 编造经号的时候。改判「前后不是拉丁字母或数字」:汉字紧邻属真实引文形态,须命中;
40
+ # 拉丁字母紧邻(FakeSutraT99n9999)通常意味着它只是更长 token 的一部分,不算引文。
41
+ _CBETA_ID = re.compile(
42
+ r"(?<![0-9A-Za-z])(?:[A-Z]{1,2}\d+n\d+|[TX]\d{3,})(?![0-9A-Za-z])"
43
+ )
37
44
  # 引文块 【…】
38
45
  _CITATION_BLOCK = re.compile(r"【([^】]*)】")
39
46
  # live 链接 fojin.app/texts/<数字>
@@ -0,0 +1,210 @@
1
+ {
2
+ "version": 1,
3
+ "skills": [
4
+ {
5
+ "name": "master-nagarjuna",
6
+ "kind": "persona",
7
+ "source": "prebuilt/master-nagarjuna",
8
+ "install_dir": "master-nagarjuna",
9
+ "aliases": [
10
+ "nagarjuna",
11
+ "master-nagarjuna"
12
+ ]
13
+ },
14
+ {
15
+ "name": "master-xuanzang",
16
+ "kind": "persona",
17
+ "source": "prebuilt/master-xuanzang",
18
+ "install_dir": "master-xuanzang",
19
+ "aliases": [
20
+ "xuanzang",
21
+ "master-xuanzang"
22
+ ]
23
+ },
24
+ {
25
+ "name": "master-kumarajiva",
26
+ "kind": "persona",
27
+ "source": "prebuilt/master-kumarajiva",
28
+ "install_dir": "master-kumarajiva",
29
+ "aliases": [
30
+ "kumarajiva",
31
+ "master-kumarajiva"
32
+ ]
33
+ },
34
+ {
35
+ "name": "master-huineng",
36
+ "kind": "persona",
37
+ "source": "prebuilt/master-huineng",
38
+ "install_dir": "master-huineng",
39
+ "aliases": [
40
+ "huineng",
41
+ "master-huineng"
42
+ ]
43
+ },
44
+ {
45
+ "name": "master-zhiyi",
46
+ "kind": "persona",
47
+ "source": "prebuilt/master-zhiyi",
48
+ "install_dir": "master-zhiyi",
49
+ "aliases": [
50
+ "zhiyi",
51
+ "master-zhiyi"
52
+ ]
53
+ },
54
+ {
55
+ "name": "master-fazang",
56
+ "kind": "persona",
57
+ "source": "prebuilt/master-fazang",
58
+ "install_dir": "master-fazang",
59
+ "aliases": [
60
+ "fazang",
61
+ "master-fazang"
62
+ ]
63
+ },
64
+ {
65
+ "name": "master-yinguang",
66
+ "kind": "persona",
67
+ "source": "prebuilt/master-yinguang",
68
+ "install_dir": "master-yinguang",
69
+ "aliases": [
70
+ "yinguang",
71
+ "master-yinguang"
72
+ ]
73
+ },
74
+ {
75
+ "name": "master-ouyi",
76
+ "kind": "persona",
77
+ "source": "prebuilt/master-ouyi",
78
+ "install_dir": "master-ouyi",
79
+ "aliases": [
80
+ "ouyi",
81
+ "master-ouyi"
82
+ ]
83
+ },
84
+ {
85
+ "name": "master-xuyun",
86
+ "kind": "persona",
87
+ "source": "prebuilt/master-xuyun",
88
+ "install_dir": "master-xuyun",
89
+ "aliases": [
90
+ "xuyun",
91
+ "master-xuyun"
92
+ ]
93
+ },
94
+ {
95
+ "name": "master-atisha",
96
+ "kind": "persona",
97
+ "source": "prebuilt/master-atisha",
98
+ "install_dir": "master-atisha",
99
+ "aliases": [
100
+ "atisha",
101
+ "master-atisha"
102
+ ]
103
+ },
104
+ {
105
+ "name": "master-tsongkhapa",
106
+ "kind": "persona",
107
+ "source": "prebuilt/master-tsongkhapa",
108
+ "install_dir": "master-tsongkhapa",
109
+ "aliases": [
110
+ "tsongkhapa",
111
+ "master-tsongkhapa"
112
+ ]
113
+ },
114
+ {
115
+ "name": "master-milarepa",
116
+ "kind": "persona",
117
+ "source": "prebuilt/master-milarepa",
118
+ "install_dir": "master-milarepa",
119
+ "aliases": [
120
+ "milarepa",
121
+ "master-milarepa"
122
+ ]
123
+ },
124
+ {
125
+ "name": "master-buddhaghosa",
126
+ "kind": "persona",
127
+ "source": "prebuilt/master-buddhaghosa",
128
+ "install_dir": "master-buddhaghosa",
129
+ "aliases": [
130
+ "buddhaghosa",
131
+ "master-buddhaghosa"
132
+ ]
133
+ },
134
+ {
135
+ "name": "master-mahasi-sayadaw",
136
+ "kind": "persona",
137
+ "source": "prebuilt/master-mahasi-sayadaw",
138
+ "install_dir": "master-mahasi-sayadaw",
139
+ "aliases": [
140
+ "mahasi-sayadaw",
141
+ "master-mahasi-sayadaw"
142
+ ]
143
+ },
144
+ {
145
+ "name": "master-ajahn-chah",
146
+ "kind": "persona",
147
+ "source": "prebuilt/master-ajahn-chah",
148
+ "install_dir": "master-ajahn-chah",
149
+ "aliases": [
150
+ "ajahn-chah",
151
+ "master-ajahn-chah"
152
+ ]
153
+ },
154
+ {
155
+ "name": "compare-masters",
156
+ "kind": "teaching-mode",
157
+ "source": "prebuilt/compare-masters",
158
+ "install_dir": "compare-masters",
159
+ "aliases": [
160
+ "compare-masters"
161
+ ]
162
+ },
163
+ {
164
+ "name": "master-debate",
165
+ "kind": "teaching-mode",
166
+ "source": "prebuilt/master-debate",
167
+ "install_dir": "master-debate",
168
+ "aliases": [
169
+ "master-debate"
170
+ ]
171
+ },
172
+ {
173
+ "name": "master-curriculum",
174
+ "kind": "teaching-mode",
175
+ "source": "prebuilt/master-curriculum",
176
+ "install_dir": "master-curriculum",
177
+ "aliases": [
178
+ "master-curriculum"
179
+ ]
180
+ },
181
+ {
182
+ "name": "master-help",
183
+ "kind": "teaching-mode",
184
+ "source": "prebuilt/master-help",
185
+ "install_dir": "master-help",
186
+ "aliases": [
187
+ "help",
188
+ "master-help"
189
+ ]
190
+ },
191
+ {
192
+ "name": "create-master",
193
+ "kind": "generator",
194
+ "source": ".",
195
+ "install_dir": "create-master",
196
+ "aliases": [
197
+ "create-master"
198
+ ],
199
+ "bundle_paths": [
200
+ "SKILL.md",
201
+ "tools",
202
+ "prompts",
203
+ "references",
204
+ "requirements.txt",
205
+ "ETHICS.md",
206
+ "masters"
207
+ ]
208
+ }
209
+ ]
210
+ }