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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/ETHICS.md +23 -17
- package/GEMINI.md +1 -1
- package/README.md +57 -295
- package/README_EN.md +59 -276
- package/SKILL.md +5 -5
- package/bin/cli.mjs +545 -78
- package/gemini-extension.json +1 -1
- package/hooks/run-hook.cmd +18 -5
- package/hooks/session-start +4 -1
- package/hooks/tests/test_run_hook.sh +114 -0
- package/hooks/tests/test_run_hook_cmd.sh +94 -0
- package/masters/.gitkeep +0 -0
- package/package.json +10 -3
- package/prebuilt/{compare → compare-masters}/SKILL.md +5 -5
- package/prebuilt/master-ajahn-chah/SKILL.md +13 -11
- package/prebuilt/master-ajahn-chah/meta.json +8 -0
- package/prebuilt/master-ajahn-chah/references/voice.md +1 -1
- package/prebuilt/master-atisha/SKILL.md +13 -11
- package/prebuilt/master-atisha/meta.json +8 -0
- package/prebuilt/master-atisha/references/voice.md +1 -1
- package/prebuilt/master-buddhaghosa/SKILL.md +13 -11
- package/prebuilt/master-buddhaghosa/meta.json +8 -0
- package/prebuilt/master-buddhaghosa/references/voice.md +1 -1
- package/prebuilt/master-curriculum/SKILL.md +1 -1
- package/prebuilt/master-debate/SKILL.md +1 -1
- package/prebuilt/master-fazang/SKILL.md +3 -3
- package/prebuilt/master-fazang/meta.json +8 -0
- package/prebuilt/master-help/SKILL.md +86 -0
- package/prebuilt/master-help/tests/fidelity.jsonl +10 -0
- package/prebuilt/master-huineng/SKILL.md +3 -3
- package/prebuilt/master-huineng/meta.json +8 -0
- package/prebuilt/master-kumarajiva/SKILL.md +3 -3
- package/prebuilt/master-kumarajiva/meta.json +20 -1
- package/prebuilt/master-mahasi-sayadaw/SKILL.md +13 -11
- package/prebuilt/master-mahasi-sayadaw/meta.json +8 -0
- package/prebuilt/master-mahasi-sayadaw/references/voice.md +2 -2
- package/prebuilt/master-milarepa/SKILL.md +13 -11
- package/prebuilt/master-milarepa/meta.json +8 -0
- package/prebuilt/master-milarepa/references/voice.md +1 -1
- package/prebuilt/master-nagarjuna/SKILL.md +3 -3
- package/prebuilt/master-nagarjuna/meta.json +25 -2
- package/prebuilt/master-ouyi/SKILL.md +3 -3
- package/prebuilt/master-ouyi/meta.json +8 -0
- package/prebuilt/master-tsongkhapa/SKILL.md +13 -11
- package/prebuilt/master-tsongkhapa/meta.json +32 -3
- package/prebuilt/master-tsongkhapa/references/voice.md +1 -1
- package/prebuilt/master-xuanzang/SKILL.md +3 -3
- package/prebuilt/master-xuanzang/meta.json +8 -0
- package/prebuilt/master-xuyun/SKILL.md +3 -3
- package/prebuilt/master-xuyun/meta.json +8 -0
- package/prebuilt/master-yinguang/SKILL.md +3 -3
- package/prebuilt/master-yinguang/meta.json +8 -0
- package/prebuilt/master-zhiyi/SKILL.md +3 -3
- package/prebuilt/master-zhiyi/meta.json +8 -0
- package/prompts/correction_handler.md +104 -0
- package/prompts/doctrine_reviewer.md +61 -0
- package/prompts/intake.md +62 -0
- package/prompts/merger.md +62 -0
- package/prompts/rag_instructions.md +54 -0
- package/prompts/sutra_analyzer.md +83 -0
- package/prompts/teaching_builder.md +41 -0
- package/prompts/voice_analyzer.md +92 -0
- package/prompts/voice_builder.md +48 -0
- package/prompts/voice_reviewer.md +66 -0
- package/references/README.md +12 -0
- package/references/ethics-runtime.md +112 -0
- package/references/fojin-api.md +223 -0
- package/references/source-conventions.md +129 -0
- package/references/teaching-modes.md +91 -0
- package/references/traditions.md +72 -0
- package/references/workflow-details.md +361 -0
- package/requirements.txt +6 -0
- package/routing.json +209 -0
- package/scripts/check-gate-liveness.py +222 -0
- package/scripts/select-fidelity-smoke.py +78 -0
- package/scripts/test-fidelity.py +339 -51
- package/scripts/tests/test_check_gate_liveness.py +232 -0
- package/scripts/tests/test_check_response.py +190 -0
- package/scripts/tests/test_fidelity_providers.py +202 -0
- package/scripts/tests/test_select_fidelity_smoke.py +142 -0
- package/scripts/tests/test_validate.py +145 -0
- package/scripts/tests/test_validate_citation_contract.py +408 -0
- package/scripts/tests/test_validate_fidelity.py +2 -2
- package/scripts/tests/test_validate_workflow.py +284 -0
- package/scripts/validate-citation-contract.py +193 -0
- package/scripts/validate-fidelity.py +6 -1
- package/scripts/validate-routing.py +254 -0
- package/scripts/validate.py +63 -36
- package/scripts/verify_citations.py +8 -1
- package/skill-catalog.json +210 -0
- package/tools/cross_reference.py +365 -0
- package/tools/fojin_bridge.py +146 -0
- package/tools/master_builder.py +341 -0
- package/tools/rag_query.py +336 -0
- package/tools/skill_writer.py +230 -0
- package/tools/sutra_collector.py +237 -0
- package/tools/verify_sources.py +512 -0
- package/tools/version_manager.py +88 -0
- /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())
|
package/scripts/validate.py
CHANGED
|
@@ -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
|
-
#
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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
|
+
}
|