master-skill 0.12.1 → 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.
Files changed (32) 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 +2 -2
  5. package/README_EN.md +2 -2
  6. package/gemini-extension.json +1 -1
  7. package/package.json +2 -2
  8. package/prebuilt/master-curriculum/references/jingtu.md +2 -2
  9. package/prebuilt/master-debate/SKILL.md +3 -3
  10. package/prebuilt/master-kumarajiva/SKILL.md +6 -0
  11. package/prebuilt/master-kumarajiva/meta.json +10 -0
  12. package/prebuilt/master-ouyi/SKILL.md +5 -0
  13. package/prebuilt/master-ouyi/meta.json +5 -0
  14. package/prebuilt/master-xuanzang/SKILL.md +6 -0
  15. package/prebuilt/master-xuanzang/meta.json +10 -0
  16. package/prebuilt/master-yinguang/SKILL.md +16 -13
  17. package/prebuilt/master-yinguang/meta.json +18 -15
  18. package/prebuilt/master-yinguang/references/teaching.md +6 -6
  19. package/prebuilt/master-yinguang/references/voice.md +1 -1
  20. package/prebuilt/master-yinguang/sources/INDEX.md +7 -6
  21. package/prebuilt/master-yinguang/sources/wenchao-excerpts.md +5 -5
  22. package/prebuilt/master-yinguang/sources/yihanbianfu-excerpts.md +4 -4
  23. package/prebuilt/master-yinguang/tests/fidelity.jsonl +7 -7
  24. package/prebuilt/master-zhiyi/SKILL.md +4 -1
  25. package/prebuilt/master-zhiyi/meta.json +5 -0
  26. package/references/source-conventions.md +2 -2
  27. package/scripts/check-pe-subsystem.py +64 -0
  28. package/scripts/reaudit-report.py +45 -3
  29. package/scripts/validate-citation-references.py +80 -7
  30. package/scripts/validate-self-audit-sources.py +116 -0
  31. package/scripts/verify_citations.py +92 -6
  32. package/tools/verify_sources.py +157 -5
@@ -365,13 +365,13 @@ def classify_cbeta_volumes(
365
365
  return mismatched, sorted(unknown)
366
366
 
367
367
 
368
- def fetch_cbeta_volumes(full_ids: list[str]) -> dict[str, str | None]:
369
- """向 CBETA 问每个经号所属的卷(或卷区间);问不到的记 None(未知,不是不符)。"""
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, str | None] = {}
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] = results[0].get("vol") if results else None
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
- cbeta_vols = fetch_cbeta_volumes(sorted(combined_map))
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: