master-skill 0.12.0 → 0.12.2
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 +2 -2
- package/prebuilt/master-curriculum/references/jingtu.md +2 -2
- package/prebuilt/master-debate/SKILL.md +3 -3
- package/prebuilt/master-kumarajiva/SKILL.md +6 -0
- package/prebuilt/master-kumarajiva/meta.json +10 -0
- package/prebuilt/master-ouyi/SKILL.md +5 -0
- package/prebuilt/master-ouyi/meta.json +5 -0
- package/prebuilt/master-xuanzang/SKILL.md +6 -0
- package/prebuilt/master-xuanzang/meta.json +10 -0
- package/prebuilt/master-yinguang/SKILL.md +16 -13
- package/prebuilt/master-yinguang/meta.json +18 -15
- package/prebuilt/master-yinguang/references/teaching.md +6 -6
- package/prebuilt/master-yinguang/references/voice.md +1 -1
- package/prebuilt/master-yinguang/sources/INDEX.md +7 -6
- package/prebuilt/master-yinguang/sources/wenchao-excerpts.md +5 -5
- package/prebuilt/master-yinguang/sources/yihanbianfu-excerpts.md +4 -4
- package/prebuilt/master-yinguang/tests/fidelity.jsonl +7 -7
- package/prebuilt/master-zhiyi/SKILL.md +4 -1
- package/prebuilt/master-zhiyi/meta.json +5 -0
- package/references/source-conventions.md +2 -2
- package/scripts/check-pe-subsystem.py +64 -0
- package/scripts/reaudit-report.py +45 -3
- package/scripts/validate-citation-references.py +80 -7
- package/scripts/validate-self-audit-sources.py +116 -0
- package/scripts/verify_citations.py +92 -6
- package/tools/verify_sources.py +157 -5
package/tools/verify_sources.py
CHANGED
|
@@ -365,13 +365,13 @@ def classify_cbeta_volumes(
|
|
|
365
365
|
return mismatched, sorted(unknown)
|
|
366
366
|
|
|
367
367
|
|
|
368
|
-
def
|
|
369
|
-
"""向 CBETA
|
|
368
|
+
def fetch_cbeta_works(full_ids: list[str]) -> dict[str, dict | None]:
|
|
369
|
+
"""向 CBETA 问每个经号的卷(或卷区间)与题名;问不到的记 None(未知,不是不符)。"""
|
|
370
370
|
import urllib.error
|
|
371
371
|
import urllib.parse
|
|
372
372
|
import urllib.request
|
|
373
373
|
|
|
374
|
-
out: dict[str,
|
|
374
|
+
out: dict[str, dict | None] = {}
|
|
375
375
|
for full_id in full_ids:
|
|
376
376
|
short = full_to_short_cbeta(full_id)
|
|
377
377
|
if not short:
|
|
@@ -382,12 +382,129 @@ def fetch_cbeta_volumes(full_ids: list[str]) -> dict[str, str | None]:
|
|
|
382
382
|
with urllib.request.urlopen(url, timeout=CBETA_TIMEOUT) as resp:
|
|
383
383
|
payload = json.loads(resp.read().decode("utf-8"))
|
|
384
384
|
results = payload.get("results") or []
|
|
385
|
-
out[full_id] =
|
|
385
|
+
out[full_id] = (
|
|
386
|
+
{"vol": results[0].get("vol"), "title": results[0].get("title")}
|
|
387
|
+
if results
|
|
388
|
+
else None
|
|
389
|
+
)
|
|
386
390
|
except (urllib.error.URLError, OSError, ValueError, KeyError, IndexError):
|
|
387
391
|
out[full_id] = None
|
|
388
392
|
return out
|
|
389
393
|
|
|
390
394
|
|
|
395
|
+
def _title_syllables(title: str | None) -> list[set[str]]:
|
|
396
|
+
"""题名 → 逐字读音集合。去掉括注,只留汉字;多音字保留全部读音。"""
|
|
397
|
+
from pypinyin import Style, pinyin
|
|
398
|
+
|
|
399
|
+
bare = re.sub(r"[((][^))]*[))]", "", title or "")
|
|
400
|
+
bare = "".join(ch for ch in bare if "\u3400" <= ch <= "\u9fff" or "\uf900" <= ch <= "\ufaff")
|
|
401
|
+
return [set(readings) for readings in pinyin(bare, style=Style.NORMAL, heteronym=True)]
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def titles_agree(declared: str, cbeta: str | None) -> bool | None:
|
|
405
|
+
"""声明题名按读音是否为 CBETA 题名的子序列。
|
|
406
|
+
|
|
407
|
+
经号在 FoJin 查得到、卷号也对,仍可能是另一部书:master-yinguang 把《印光
|
|
408
|
+
法师文钞》声明成 X62n1182–1184,CBETA 那三号是《徹悟禪師語錄》《淨業知津》
|
|
409
|
+
《念佛百問》,卷号 X62 分毫不差,这道周检一直是绿的。
|
|
410
|
+
|
|
411
|
+
声明用简体、常用简称(《大佛顶首楞严经》),CBETA 用繁体全称,逐字比两边都
|
|
412
|
+
会误报。按读音比,繁简同音即对得上,简称是全称的子序列也对得上;另一部书一
|
|
413
|
+
个音都对不上。用读音而不用繁简转换表,是因为仓库已依赖 pypinyin。已知边界:
|
|
414
|
+
过短的题名可能碰巧是别书题名的子序列。
|
|
415
|
+
|
|
416
|
+
返回 None 表示比不了(任一侧没有汉字),既不是对也不是错。
|
|
417
|
+
"""
|
|
418
|
+
mine, theirs = _title_syllables(declared), _title_syllables(cbeta)
|
|
419
|
+
if not mine or not theirs:
|
|
420
|
+
return None
|
|
421
|
+
position = 0
|
|
422
|
+
for readings in theirs:
|
|
423
|
+
if position < len(mine) and mine[position] & readings:
|
|
424
|
+
position += 1
|
|
425
|
+
return position == len(mine)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def collect_declared_titles() -> dict[str, list[str]]:
|
|
429
|
+
"""{完整经号: [各 meta.json 为它声明的题名]} —— 同一部经可能被几位祖师声明。"""
|
|
430
|
+
titles: dict[str, list[str]] = {}
|
|
431
|
+
for teacher in sorted(os.listdir(PREBUILT_DIR)):
|
|
432
|
+
meta_path = os.path.join(PREBUILT_DIR, teacher, "meta.json")
|
|
433
|
+
if not os.path.isfile(meta_path):
|
|
434
|
+
continue
|
|
435
|
+
with open(meta_path, encoding="utf-8") as f:
|
|
436
|
+
meta = json.load(f)
|
|
437
|
+
for src in meta.get("sources", []):
|
|
438
|
+
if src.get("type") == "cbeta" and src.get("id") and src.get("title"):
|
|
439
|
+
known = titles.setdefault(src["id"], [])
|
|
440
|
+
if src["title"] not in known:
|
|
441
|
+
known.append(src["title"])
|
|
442
|
+
return titles
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def classify_cbeta_titles(
|
|
446
|
+
declared: dict[str, list[str]], cbeta_titles: dict[str, str | None]
|
|
447
|
+
) -> tuple[dict[str, tuple[list[str], str | None]], list[str]]:
|
|
448
|
+
"""把声明题名分成「与 CBETA 对不上」与「比不了」两类,三态同卷号检查。
|
|
449
|
+
|
|
450
|
+
一个 id 若有一条题名对不上,就算不符 —— 另一条比不了的题名不能把它盖住。
|
|
451
|
+
"""
|
|
452
|
+
mismatched: dict[str, tuple[list[str], str | None]] = {}
|
|
453
|
+
unknown: list[str] = []
|
|
454
|
+
for full_id, mine in declared.items():
|
|
455
|
+
theirs = cbeta_titles.get(full_id)
|
|
456
|
+
verdicts = {title: titles_agree(title, theirs) for title in mine}
|
|
457
|
+
wrong = [title for title, verdict in verdicts.items() if verdict is False]
|
|
458
|
+
if wrong:
|
|
459
|
+
mismatched[full_id] = (wrong, theirs)
|
|
460
|
+
elif not mine or any(verdict is None for verdict in verdicts.values()):
|
|
461
|
+
unknown.append(full_id)
|
|
462
|
+
return mismatched, sorted(unknown)
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def collect_frontmatter_fojin_ids() -> list[tuple[str, str, str, str]]:
|
|
466
|
+
"""(祖师目录, 题名, cbeta_id, fojin_text_id),取自各 SKILL.md frontmatter 的 sources。"""
|
|
467
|
+
import yaml
|
|
468
|
+
|
|
469
|
+
rows: list[tuple[str, str, str, str]] = []
|
|
470
|
+
for teacher in sorted(os.listdir(PREBUILT_DIR)):
|
|
471
|
+
path = os.path.join(PREBUILT_DIR, teacher, "SKILL.md")
|
|
472
|
+
if not os.path.isfile(path):
|
|
473
|
+
continue
|
|
474
|
+
with open(path, encoding="utf-8") as f:
|
|
475
|
+
parts = f.read().split("---", 2)
|
|
476
|
+
if len(parts) < 3 or parts[0].strip():
|
|
477
|
+
continue
|
|
478
|
+
front = yaml.safe_load(parts[1]) or {}
|
|
479
|
+
for src in front.get("sources") or []:
|
|
480
|
+
if isinstance(src, dict) and src.get("cbeta_id") and src.get("fojin_text_id") is not None:
|
|
481
|
+
rows.append(
|
|
482
|
+
(teacher, str(src.get("title", "")), str(src["cbeta_id"]), str(src["fojin_text_id"]))
|
|
483
|
+
)
|
|
484
|
+
return rows
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def classify_frontmatter_fojin_ids(
|
|
488
|
+
rows: list[tuple[str, str, str, str]], short_to_text: dict[str, object]
|
|
489
|
+
) -> tuple[list[tuple[str, str, str, str, str]], list[str]]:
|
|
490
|
+
"""frontmatter 的 fojin_text_id 与 FoJin 对该经号的解析结果不符的条目。
|
|
491
|
+
|
|
492
|
+
这个 id 不进审计,却是人设给读者拼链接用的:master-zhiyi 把《法華玄義》
|
|
493
|
+
(T1716)写成 52,那是《法華文句》的 text id。frontmatter 里完整号与短号
|
|
494
|
+
(`T1716`)两种写法都有,一律折成短号再比。查不到的记为未知,不算错。
|
|
495
|
+
"""
|
|
496
|
+
mismatched: list[tuple[str, str, str, str, str]] = []
|
|
497
|
+
unknown: list[str] = []
|
|
498
|
+
for teacher, title, cbeta_id, written in rows:
|
|
499
|
+
short = full_to_short_cbeta(cbeta_id) if FULL_CBETA_RE.match(cbeta_id) else cbeta_id
|
|
500
|
+
actual = short_to_text.get(short)
|
|
501
|
+
if actual is None:
|
|
502
|
+
unknown.append(f"{teacher}:{cbeta_id}")
|
|
503
|
+
elif str(actual) != written:
|
|
504
|
+
mismatched.append((teacher, cbeta_id, title, written, str(actual)))
|
|
505
|
+
return mismatched, sorted(unknown)
|
|
506
|
+
|
|
507
|
+
|
|
391
508
|
def verify_ids(bridge, cbeta_map: dict[str, list[str]], titles: dict[str, str]) -> dict[str, dict]:
|
|
392
509
|
"""Verify all CBETA IDs and return {full_cbeta_id: {text_id, short_id, title, ...}}.
|
|
393
510
|
|
|
@@ -566,7 +683,8 @@ def _run_legacy_link_verification(*, fix: bool) -> int:
|
|
|
566
683
|
|
|
567
684
|
# Step 3b: 卷号。FoJin 的查询把卷号丢掉了,所以上面那一步结构上看不见它。
|
|
568
685
|
print("\n[3b/4] Checking declared volume numbers against CBETA...")
|
|
569
|
-
|
|
686
|
+
cbeta_works = fetch_cbeta_works(sorted(combined_map))
|
|
687
|
+
cbeta_vols = {k: (v or {}).get("vol") for k, v in cbeta_works.items()}
|
|
570
688
|
mismatched, unknown_to_cbeta = classify_cbeta_volumes(combined_map, cbeta_vols)
|
|
571
689
|
if mismatched:
|
|
572
690
|
print(f" Declared IDs CBETA disagrees with ({len(mismatched)}):")
|
|
@@ -579,6 +697,22 @@ def _run_legacy_link_verification(*, fix: bool) -> int:
|
|
|
579
697
|
if not mismatched and not unknown_to_cbeta:
|
|
580
698
|
print(f" All {len(combined_map)} declared IDs sit in a volume CBETA gives this work")
|
|
581
699
|
|
|
700
|
+
# Step 3c: 题名。卷号对、FoJin 查得到,仍可能是另一部书(见 titles_agree)。
|
|
701
|
+
print("\n[3c/4] Checking declared titles against CBETA...")
|
|
702
|
+
declared_titles = collect_declared_titles()
|
|
703
|
+
title_mismatched, title_unknown = classify_cbeta_titles(
|
|
704
|
+
{cid: declared_titles.get(cid, []) for cid in cbeta_map},
|
|
705
|
+
{k: (v or {}).get("title") for k, v in cbeta_works.items()},
|
|
706
|
+
)
|
|
707
|
+
for full_id, (mine, theirs) in sorted(title_mismatched.items()):
|
|
708
|
+
teachers = ", ".join(combined_map.get(full_id, ["?"]))
|
|
709
|
+
print(f" [WRONG] {full_id} declared as 《{' / '.join(mine)}》 -> CBETA: 《{theirs}》 (used by: {teachers})")
|
|
710
|
+
if title_unknown:
|
|
711
|
+
print(f" Could not compare titles for {len(title_unknown)} ID(s) — "
|
|
712
|
+
"unknown, not wrong: " + ", ".join(title_unknown))
|
|
713
|
+
if not title_mismatched and not title_unknown:
|
|
714
|
+
print(f" All {len(cbeta_map)} declared titles match the work CBETA gives each ID")
|
|
715
|
+
|
|
582
716
|
found = {k: v for k, v in verified.items() if v["text_id"] is not None}
|
|
583
717
|
all_absent = {k: v for k, v in verified.items() if v["text_id"] is None}
|
|
584
718
|
known_absent = load_known_absent()
|
|
@@ -618,6 +752,22 @@ def _run_legacy_link_verification(*, fix: bool) -> int:
|
|
|
618
752
|
f"{found[cid]['text_id']}) —— 从清单里删掉这一条"
|
|
619
753
|
)
|
|
620
754
|
|
|
755
|
+
# Step 3d: SKILL.md frontmatter 的 fojin_text_id(见 classify_frontmatter_fojin_ids)。
|
|
756
|
+
print("\n[3d/4] Checking SKILL.md frontmatter fojin_text_id values against FoJin...")
|
|
757
|
+
short_to_text = {
|
|
758
|
+
info.get("short_cbeta_id"): info["text_id"] for info in found.values()
|
|
759
|
+
}
|
|
760
|
+
fm_mismatched, fm_unknown = classify_frontmatter_fojin_ids(
|
|
761
|
+
collect_frontmatter_fojin_ids(), short_to_text
|
|
762
|
+
)
|
|
763
|
+
for teacher, cbeta_id, title, written, actual in fm_mismatched:
|
|
764
|
+
print(f" [WRONG] {teacher}: 《{title}》 {cbeta_id} fojin_text_id={written} -> FoJin resolves {actual}")
|
|
765
|
+
if fm_unknown:
|
|
766
|
+
print(f" FoJin did not resolve {len(fm_unknown)} frontmatter ID(s) — "
|
|
767
|
+
"unknown, not wrong: " + ", ".join(fm_unknown))
|
|
768
|
+
if not fm_mismatched and not fm_unknown:
|
|
769
|
+
print(" Every frontmatter fojin_text_id matches what FoJin resolves")
|
|
770
|
+
|
|
621
771
|
# Step 4: Update URLs
|
|
622
772
|
# Build replacement map: full_cbeta_id -> str(internal_text_id)
|
|
623
773
|
id_replacement_map: dict[str, str] = {}
|
|
@@ -660,6 +810,8 @@ def _run_legacy_link_verification(*, fix: bool) -> int:
|
|
|
660
810
|
print(f" Stale known-absent entries:{len(stale_absent):>4}")
|
|
661
811
|
print(f" URL replacements: {len(all_changes)}")
|
|
662
812
|
print(f" CBETA id mismatches: {len(mismatched)}")
|
|
813
|
+
print(f" CBETA title mismatches: {len(title_mismatched)}")
|
|
814
|
+
print(f" Frontmatter FoJin id mismatches: {len(fm_mismatched)}")
|
|
663
815
|
if unknown_to_cbeta:
|
|
664
816
|
print(f" CBETA unreachable for: {len(unknown_to_cbeta)} (not counted as wrong)")
|
|
665
817
|
if dry_run and all_changes:
|