master-skill 0.12.5 → 0.12.7
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 +2 -2
- package/README_EN.md +2 -2
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
- package/prebuilt/master-atisha/SKILL.md +5 -1
- package/prebuilt/master-atisha/meta.json +15 -2
- package/prebuilt/master-atisha/references/teaching.md +8 -6
- package/prebuilt/master-atisha/references/voice.md +4 -4
- package/prebuilt/master-atisha/sources/INDEX.md +3 -2
- package/prebuilt/master-atisha/sources/bodhipathapradipa-excerpts.md +27 -2
- package/prebuilt/master-buddhaghosa/references/teaching.md +2 -2
- package/prebuilt/master-curriculum/references/huayan.md +1 -1
- package/prebuilt/master-curriculum/references/theravada-vipassana.md +3 -3
- package/prebuilt/master-kumarajiva/references/teaching.md +1 -1
- package/prebuilt/master-milarepa/SKILL.md +6 -6
- package/prebuilt/master-milarepa/meta.json +6 -6
- package/prebuilt/master-milarepa/references/teaching.md +16 -16
- package/prebuilt/master-milarepa/references/voice.md +2 -2
- package/prebuilt/master-milarepa/sources/INDEX.md +3 -3
- package/prebuilt/master-milarepa/sources/grubum-excerpts.md +10 -10
- package/prebuilt/master-milarepa/sources/namthar-excerpts.md +18 -18
- package/prebuilt/master-milarepa/tests/fidelity.jsonl +7 -7
- package/prebuilt/master-tsongkhapa/SKILL.md +8 -0
- package/prebuilt/master-tsongkhapa/meta.json +25 -0
- package/prebuilt/master-tsongkhapa/references/teaching.md +6 -6
- package/prebuilt/master-tsongkhapa/references/voice.md +2 -2
- package/prebuilt/master-tsongkhapa/sources/INDEX.md +1 -0
- package/prebuilt/master-tsongkhapa/sources/lamrim-excerpts.md +33 -8
- package/prebuilt/master-xuyun/references/teaching.md +2 -2
- package/prebuilt/master-yinguang/meta.json +1 -1
- package/prebuilt/master-yinguang/references/teaching.md +3 -3
- package/prebuilt/master-yinguang/references/voice.md +1 -1
- package/prebuilt/master-zhiyi/references/teaching.md +1 -1
- package/scripts/validate-citation-references.py +1 -1
- package/scripts/validate-self-audit-sources.py +2 -2
- package/scripts/verify_citations.py +6 -4
- package/tools/fojin-known-absent.json +58 -1
- package/tools/verify_sources.py +171 -1
package/tools/verify_sources.py
CHANGED
|
@@ -507,7 +507,9 @@ def classify_frontmatter_fojin_ids(
|
|
|
507
507
|
|
|
508
508
|
_DOC_CITATION = re.compile(r"【([^】]*)】")
|
|
509
509
|
_DOC_FOJIN_LINK = re.compile(r"https://fojin\.app/texts/([0-9]+)")
|
|
510
|
-
|
|
510
|
+
# B(大藏经补编)与 G(佛教大藏经):master-tsongkhapa / master-atisha 引法尊译本。
|
|
511
|
+
# 只认 T/X/J 时,这两部藏的「原典」块整块被 collect_excerpt_quotes 跳过,周检照绿。
|
|
512
|
+
_DOC_CBETA_ID = re.compile(r"(?<![0-9A-Za-z])([TXJBG])(?:[0-9]{1,3}n)?(B?[0-9]{3,5})[a-z]?(?![0-9A-Za-z])")
|
|
511
513
|
_DOC_TEMPLATE = re.compile(r"\{|卷N|[A-Za-z][xX]{3,}")
|
|
512
514
|
|
|
513
515
|
|
|
@@ -800,6 +802,159 @@ def classify_excerpt_quotes(
|
|
|
800
802
|
return mismatched, unknown
|
|
801
803
|
|
|
802
804
|
|
|
805
|
+
BDRC_RESOURCE_URL = "https://ldspdi.bdrc.io/resource/{}.json"
|
|
806
|
+
_BDRC_RESOURCE = "http://purl.bdrc.io/resource/"
|
|
807
|
+
_BDRC_WORK_ID = re.compile(r"^BDRC:(W[0-9][A-Za-z0-9_]*)$")
|
|
808
|
+
_LATIN_RUN = re.compile(r"[A-Za-z][A-Za-z' .+-]*[A-Za-z']")
|
|
809
|
+
|
|
810
|
+
|
|
811
|
+
def _wylie_key(text: str) -> str:
|
|
812
|
+
"""Wylie 题名比对用的归一:小写,`/` `_` `-` 与括号折成空格,ALA-LC 的 ʾ 当撇号。"""
|
|
813
|
+
text = text.lower().replace("\u02be", "'").replace("\u2019", "'")
|
|
814
|
+
return " ".join(re.sub(r"[/_()\-]", " ", text).split())
|
|
815
|
+
|
|
816
|
+
|
|
817
|
+
def collect_bdrc_sources() -> list[tuple[str, str, str | None]]:
|
|
818
|
+
"""(祖师目录, BDRC 作品号, 声明的藏文题名或 None),取自各 meta.json 的 sources。
|
|
819
|
+
|
|
820
|
+
藏文题名先取 SKILL.md frontmatter 里同号的 `tibetan_title`,没有就取 meta.json
|
|
821
|
+
题名括注里的拉丁字母段(「密勒日巴尊者传(rNam thar)」→ rNam thar)。
|
|
822
|
+
`BDRC:Pha-chos-Bu-chos` 这种拿 Wylie 当 id 的写法不是作品号,不收。
|
|
823
|
+
"""
|
|
824
|
+
import yaml
|
|
825
|
+
|
|
826
|
+
rows: list[tuple[str, str, str | None]] = []
|
|
827
|
+
for teacher in sorted(os.listdir(PREBUILT_DIR)):
|
|
828
|
+
meta_path = os.path.join(PREBUILT_DIR, teacher, "meta.json")
|
|
829
|
+
if not os.path.isfile(meta_path):
|
|
830
|
+
continue
|
|
831
|
+
with open(meta_path, encoding="utf-8") as f:
|
|
832
|
+
meta = json.load(f)
|
|
833
|
+
front_titles: dict[str, str] = {}
|
|
834
|
+
skill_path = os.path.join(PREBUILT_DIR, teacher, "SKILL.md")
|
|
835
|
+
if os.path.isfile(skill_path):
|
|
836
|
+
with open(skill_path, encoding="utf-8") as f:
|
|
837
|
+
parts = f.read().split("---", 2)
|
|
838
|
+
if len(parts) == 3 and not parts[0].strip():
|
|
839
|
+
for src in (yaml.safe_load(parts[1]) or {}).get("sources") or []:
|
|
840
|
+
if isinstance(src, dict) and src.get("bdrc_id") and src.get("tibetan_title"):
|
|
841
|
+
front_titles[str(src["bdrc_id"])] = str(src["tibetan_title"])
|
|
842
|
+
for src in meta.get("sources", []):
|
|
843
|
+
match = _BDRC_WORK_ID.match(str(src.get("id", "")))
|
|
844
|
+
if not match:
|
|
845
|
+
continue
|
|
846
|
+
rid = match.group(1)
|
|
847
|
+
declared = front_titles.get(rid)
|
|
848
|
+
if declared is None:
|
|
849
|
+
brackets = re.findall(r"[((]([^))]*)[))]", str(src.get("title") or ""))
|
|
850
|
+
runs = [run for part in brackets for run in _LATIN_RUN.findall(part)]
|
|
851
|
+
declared = runs[-1] if runs else None
|
|
852
|
+
rows.append((teacher, rid, declared))
|
|
853
|
+
return rows
|
|
854
|
+
|
|
855
|
+
|
|
856
|
+
def bdrc_titles(document: dict, rid: str) -> list[str]:
|
|
857
|
+
"""ldspdi 返回的 JSON 图里 `rid` 这个节点自己的题名。
|
|
858
|
+
|
|
859
|
+
只收三处:节点的 prefLabel / altLabel,和它 hasTitle 指向的标题节点的 label。
|
|
860
|
+
备注、目录说明(catalogInfo)不收 —— 一部全集的说明里提到「rnam thar」,不等于
|
|
861
|
+
它就是那部传记。
|
|
862
|
+
"""
|
|
863
|
+
def literals(props: dict, names: set[str]) -> list[str]:
|
|
864
|
+
return [
|
|
865
|
+
value["value"]
|
|
866
|
+
for key, values in props.items()
|
|
867
|
+
if key.rsplit("/", 1)[-1].split("#")[-1] in names
|
|
868
|
+
for value in values
|
|
869
|
+
if value.get("type") == "literal"
|
|
870
|
+
]
|
|
871
|
+
|
|
872
|
+
node = document.get(_BDRC_RESOURCE + rid, {})
|
|
873
|
+
titles = literals(node, {"prefLabel", "altLabel"})
|
|
874
|
+
for key, values in node.items():
|
|
875
|
+
if key.endswith("/hasTitle"):
|
|
876
|
+
for value in values:
|
|
877
|
+
titles += literals(document.get(value.get("value", ""), {}), {"label"})
|
|
878
|
+
return titles
|
|
879
|
+
|
|
880
|
+
|
|
881
|
+
def bdrc_linked_ids(document: dict, rid: str, properties: tuple[str, ...]) -> list[str]:
|
|
882
|
+
"""`rid` 节点在给定属性上指向的 BDRC 资源号(`instanceOf` → `WA…`)。"""
|
|
883
|
+
node = document.get(_BDRC_RESOURCE + rid, {})
|
|
884
|
+
return [
|
|
885
|
+
value["value"].rsplit("/", 1)[-1]
|
|
886
|
+
for key, values in node.items()
|
|
887
|
+
if key.rsplit("/", 1)[-1] in properties
|
|
888
|
+
for value in values
|
|
889
|
+
if value.get("type") == "uri" and value.get("value", "").startswith(_BDRC_RESOURCE)
|
|
890
|
+
]
|
|
891
|
+
|
|
892
|
+
|
|
893
|
+
def fetch_bdrc_record(rid: str) -> tuple[bool, list[str]] | None:
|
|
894
|
+
"""BDRC 作品号 → (是否存在, 题名)。题名含它复制的 MW 实例与所属 WA 作品的题名。
|
|
895
|
+
|
|
896
|
+
影像实例(W…)节点本身不带题名,题名在 MW 与 WA 上,所以顺着
|
|
897
|
+
instanceReproductionOf / instanceOf 各取一层。查无此号(404)返回
|
|
898
|
+
(False, []);网络或服务端出错返回 None —— 未知,不算错。
|
|
899
|
+
"""
|
|
900
|
+
import urllib.error
|
|
901
|
+
import urllib.request
|
|
902
|
+
|
|
903
|
+
def get(resource: str) -> dict:
|
|
904
|
+
request = urllib.request.Request(
|
|
905
|
+
BDRC_RESOURCE_URL.format(resource),
|
|
906
|
+
headers={"User-Agent": "Mozilla/5.0 (compatible; master-skill verify_sources)", "Accept": "application/json"},
|
|
907
|
+
)
|
|
908
|
+
with urllib.request.urlopen(request, timeout=30) as response:
|
|
909
|
+
return json.loads(response.read().decode("utf-8"))
|
|
910
|
+
|
|
911
|
+
try:
|
|
912
|
+
document = get(rid)
|
|
913
|
+
except urllib.error.HTTPError as error:
|
|
914
|
+
return (False, []) if error.code == 404 else None
|
|
915
|
+
except (urllib.error.URLError, OSError, ValueError):
|
|
916
|
+
return None
|
|
917
|
+
titles = bdrc_titles(document, rid)
|
|
918
|
+
for other in bdrc_linked_ids(document, rid, ("instanceReproductionOf", "instanceOf")):
|
|
919
|
+
try:
|
|
920
|
+
titles += bdrc_titles(get(other), other)
|
|
921
|
+
except (urllib.error.URLError, OSError, ValueError):
|
|
922
|
+
return None
|
|
923
|
+
return True, titles
|
|
924
|
+
|
|
925
|
+
|
|
926
|
+
def classify_bdrc_records(
|
|
927
|
+
sources: list[tuple[str, str, str | None]],
|
|
928
|
+
records: dict[str, tuple[bool, list[str]] | None],
|
|
929
|
+
) -> tuple[list[tuple[str, str, str | None, str]], list[tuple[str, str]]]:
|
|
930
|
+
"""声明的 BDRC 作品号分成「不存在或是另一部书」与「比不了」两类。
|
|
931
|
+
|
|
932
|
+
master-milarepa 把《密勒日巴尊者传》声明成 W22272(实为宗喀巴全集),把
|
|
933
|
+
《道歌集》声明成 W1KG14334(BDRC 查无此号),挂了 73 处,周检一直是绿的:
|
|
934
|
+
上面各步只核 CBETA,而 library.bdrc.io 对任何号都打开一个页面。
|
|
935
|
+
|
|
936
|
+
比法:声明的藏文题名(归一后按整音节)须出现在记录的某个题名里。已知边界:
|
|
937
|
+
声明写得太泛(只写「rNam thar」),别人的传记也对得上 —— 这一步抓得住不存在
|
|
938
|
+
的号和另一类书,抓不住同类的另一部。
|
|
939
|
+
"""
|
|
940
|
+
mismatched: list[tuple[str, str, str | None, str]] = []
|
|
941
|
+
unknown: list[tuple[str, str]] = []
|
|
942
|
+
for teacher, rid, declared in sources:
|
|
943
|
+
record = records.get(rid)
|
|
944
|
+
if record is None:
|
|
945
|
+
unknown.append((f"{teacher}:BDRC:{rid}", "BDRC did not answer"))
|
|
946
|
+
continue
|
|
947
|
+
exists, titles = record
|
|
948
|
+
if not exists:
|
|
949
|
+
mismatched.append((teacher, rid, declared, "BDRC has no such record"))
|
|
950
|
+
elif not declared:
|
|
951
|
+
unknown.append((f"{teacher}:BDRC:{rid}", "no Tibetan title declared to compare"))
|
|
952
|
+
elif not any(f" {_wylie_key(declared)} " in f" {_wylie_key(title)} " for title in titles):
|
|
953
|
+
shown = " | ".join(titles[:3]) if titles else "(no title)"
|
|
954
|
+
mismatched.append((teacher, rid, declared, f"BDRC titles: {shown}"))
|
|
955
|
+
return mismatched, unknown
|
|
956
|
+
|
|
957
|
+
|
|
803
958
|
def verify_ids(bridge, cbeta_map: dict[str, list[str]], titles: dict[str, str]) -> dict[str, dict]:
|
|
804
959
|
"""Verify all CBETA IDs and return {full_cbeta_id: {text_id, short_id, title, ...}}.
|
|
805
960
|
|
|
@@ -1105,6 +1260,20 @@ def _run_legacy_link_verification(*, fix: bool) -> int:
|
|
|
1105
1260
|
if not quote_mismatched and not quote_unknown:
|
|
1106
1261
|
print(f" All {len(quotes)} quotations appear clause by clause in the cited fascicle")
|
|
1107
1262
|
|
|
1263
|
+
# Step 3g: 声明的 BDRC 作品号是否存在、是否是那部书(见 classify_bdrc_records)。
|
|
1264
|
+
print("\n[3g/4] Checking declared BDRC work ids against BDRC...")
|
|
1265
|
+
bdrc_sources = collect_bdrc_sources()
|
|
1266
|
+
bdrc_records = {rid: fetch_bdrc_record(rid) for rid in sorted({rid for _, rid, _ in bdrc_sources})}
|
|
1267
|
+
bdrc_mismatched, bdrc_unknown = classify_bdrc_records(bdrc_sources, bdrc_records)
|
|
1268
|
+
for teacher, rid, declared, reason in bdrc_mismatched:
|
|
1269
|
+
print(f" [WRONG] {teacher}: BDRC:{rid} declared as {declared or '(no Tibetan title)'} — {reason}")
|
|
1270
|
+
if bdrc_unknown:
|
|
1271
|
+
print(f" Could not check {len(bdrc_unknown)} BDRC id(s) — unknown, not wrong:")
|
|
1272
|
+
for where, reason in bdrc_unknown:
|
|
1273
|
+
print(f" {where}: {reason}")
|
|
1274
|
+
if not bdrc_mismatched and not bdrc_unknown:
|
|
1275
|
+
print(f" All {len(bdrc_sources)} declared BDRC work ids resolve to a record with the declared title")
|
|
1276
|
+
|
|
1108
1277
|
# Step 4: Update URLs
|
|
1109
1278
|
# Build replacement map: full_cbeta_id -> str(internal_text_id)
|
|
1110
1279
|
id_replacement_map: dict[str, str] = {}
|
|
@@ -1151,6 +1320,7 @@ def _run_legacy_link_verification(*, fix: bool) -> int:
|
|
|
1151
1320
|
print(f" Frontmatter FoJin id mismatches: {len(fm_mismatched)}")
|
|
1152
1321
|
print(f" Doc citation links to another work: {len(doc_mismatched)}")
|
|
1153
1322
|
print(f" Excerpt quotes not in the cited text: {len(quote_mismatched)}")
|
|
1323
|
+
print(f" BDRC records that do not match: {len(bdrc_mismatched)}")
|
|
1154
1324
|
if unknown_to_cbeta:
|
|
1155
1325
|
print(f" CBETA unreachable for: {len(unknown_to_cbeta)} (not counted as wrong)")
|
|
1156
1326
|
if dry_run and all_changes:
|