master-skill 0.12.1 → 0.12.3

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 (59) 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-fazang/SKILL.md +6 -3
  11. package/prebuilt/master-fazang/meta.json +5 -0
  12. package/prebuilt/master-fazang/references/teaching.md +10 -5
  13. package/prebuilt/master-fazang/sources/INDEX.md +1 -1
  14. package/prebuilt/master-fazang/sources/jinshizi-excerpts.md +12 -8
  15. package/prebuilt/master-fazang/sources/wujiao-zhang-excerpts.md +20 -12
  16. package/prebuilt/master-fazang/tests/fidelity.jsonl +2 -2
  17. package/prebuilt/master-kumarajiva/SKILL.md +9 -0
  18. package/prebuilt/master-kumarajiva/meta.json +15 -0
  19. package/prebuilt/master-kumarajiva/references/teaching.md +4 -2
  20. package/prebuilt/master-kumarajiva/references/voice.md +1 -1
  21. package/prebuilt/master-kumarajiva/sources/zhonglun-excerpts.md +6 -2
  22. package/prebuilt/master-nagarjuna/references/teaching.md +5 -3
  23. package/prebuilt/master-nagarjuna/sources/dazhidulun-excerpts.md +13 -19
  24. package/prebuilt/master-nagarjuna/sources/shizhu-yixing-excerpts.md +5 -5
  25. package/prebuilt/master-nagarjuna/sources/zhonglun-excerpts.md +22 -1
  26. package/prebuilt/master-ouyi/SKILL.md +8 -0
  27. package/prebuilt/master-ouyi/meta.json +10 -0
  28. package/prebuilt/master-ouyi/references/teaching.md +2 -2
  29. package/prebuilt/master-ouyi/sources/jiaoguan-gangzong-excerpts.md +9 -7
  30. package/prebuilt/master-ouyi/sources/mituo-yaojie-excerpts.md +7 -6
  31. package/prebuilt/master-xuanzang/SKILL.md +6 -0
  32. package/prebuilt/master-xuanzang/meta.json +10 -0
  33. package/prebuilt/master-xuanzang/sources/chengweishi-excerpts.md +2 -2
  34. package/prebuilt/master-xuyun/SKILL.md +6 -0
  35. package/prebuilt/master-xuyun/meta.json +17 -1
  36. package/prebuilt/master-xuyun/references/teaching.md +5 -5
  37. package/prebuilt/master-xuyun/sources/lengyanjing-excerpts.md +1 -1
  38. package/prebuilt/master-yinguang/SKILL.md +16 -13
  39. package/prebuilt/master-yinguang/meta.json +18 -15
  40. package/prebuilt/master-yinguang/references/teaching.md +6 -6
  41. package/prebuilt/master-yinguang/references/voice.md +1 -1
  42. package/prebuilt/master-yinguang/sources/INDEX.md +7 -6
  43. package/prebuilt/master-yinguang/sources/wenchao-excerpts.md +5 -5
  44. package/prebuilt/master-yinguang/sources/yihanbianfu-excerpts.md +4 -4
  45. package/prebuilt/master-yinguang/tests/fidelity.jsonl +7 -7
  46. package/prebuilt/master-zhiyi/SKILL.md +4 -1
  47. package/prebuilt/master-zhiyi/meta.json +6 -1
  48. package/prebuilt/master-zhiyi/references/teaching.md +3 -3
  49. package/prebuilt/master-zhiyi/references/voice.md +1 -1
  50. package/prebuilt/master-zhiyi/sources/INDEX.md +1 -1
  51. package/prebuilt/master-zhiyi/sources/fahua-xuanyi-excerpts.md +18 -15
  52. package/prebuilt/master-zhiyi/sources/mohezhiguan-excerpts.md +6 -2
  53. package/references/source-conventions.md +2 -2
  54. package/scripts/check-pe-subsystem.py +64 -0
  55. package/scripts/reaudit-report.py +45 -3
  56. package/scripts/validate-citation-references.py +80 -7
  57. package/scripts/validate-self-audit-sources.py +116 -0
  58. package/scripts/verify_citations.py +92 -6
  59. package/tools/verify_sources.py +496 -5
@@ -617,6 +617,7 @@ def audit_answer(
617
617
  fabricated: list[str] = []
618
618
  unparsed: list[str] = []
619
619
  noncitation: list[str] = []
620
+ live_detail: list[dict] = []
620
621
 
621
622
  blocks = list(_CITATION_BLOCK.finditer(answer))
622
623
  for idx, m in enumerate(blocks):
@@ -679,11 +680,22 @@ def audit_answer(
679
680
  unresolved.append(cid)
680
681
  if len(unresolved) == 1 and links:
681
682
  live.append((unresolved[0], links[0]))
683
+ # `--online` needs more than the pair to tell whether the link is the
684
+ # work the citation names: the title the block gives it.
685
+ title = _WORK_TITLE.search(m.group(1))
686
+ live_detail.append(
687
+ {
688
+ "cited_id": unresolved[0],
689
+ "text_id": links[0],
690
+ "title": title.group(1) if title else None,
691
+ }
692
+ )
682
693
  else:
683
694
  fabricated.extend(unresolved)
684
695
  return {
685
696
  "offline": offline,
686
697
  "live": live,
698
+ "live_detail": live_detail,
687
699
  "fabricated": fabricated,
688
700
  "unparsed": unparsed,
689
701
  "noncitation": noncitation,
@@ -701,8 +713,58 @@ VERIFY_TIMEOUT = 15
701
713
  VERIFY_WORKERS = 8
702
714
 
703
715
 
704
- def _check_one_text_id(session_factory, base_url: str, tid: str, timeout: int):
705
- """核验单个 text_id,返回 (三态结果, 说明)。异常一律收敛成 None,不外抛。"""
716
+ def _cbeta_key(cid: str) -> tuple[str, str] | None:
717
+ """`T30n1568` / `T1568` / `JB348` → (藏别, 去零经号);不是 CBETA 号 → None"""
718
+ m = _FULL_FORM.match(cid) or _SHORT_FORM.match(cid)
719
+ if not m:
720
+ return None
721
+ return m.group(1), _normalize_cbeta_work_number(m.group(2))
722
+
723
+
724
+ def _titles_agree(cited: str, linked: str) -> bool | None:
725
+ """借用 tools/verify_sources.titles_agree(读音子序列);加载不了返回 None。"""
726
+ tools = os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir, "tools")
727
+ if tools not in sys.path:
728
+ sys.path.insert(0, tools)
729
+ try:
730
+ from verify_sources import titles_agree
731
+ except ImportError:
732
+ return None
733
+ return titles_agree(cited, linked)
734
+
735
+
736
+ def _live_link_mismatch(citation: dict, payload: dict) -> str | None:
737
+ """一个解析得到的 FoJin 文本,为什么仍不是这条 live 引文所说的那部书;可能是则 None。
738
+
739
+ 只验「打得开」时,【《伪经》,T99n9999】→ texts/20 能过:texts/20 是
740
+ 《佛說阿彌陀經》。master-yinguang 的存档引文更隐蔽:X62n1182 → texts/12977,
741
+ 链接的 cbeta_id 正是 X1182,经号自洽,可那部书是《徹悟禪師語錄》而不是引文
742
+ 写的《印光法師文鈔正編》。所以比两样:经号(只对 CBETA 号),书名(引文给了
743
+ 才比)。书名先截掉「·品名」「〈篇名〉」,篇章不是书名的一部分。
744
+ """
745
+ tid = citation.get("text_id")
746
+ cited = citation.get("cited_id") or ""
747
+ linked_id = payload.get("cbeta_id")
748
+ cited_key = _cbeta_key(cited)
749
+ if cited_key and linked_id and _cbeta_key(str(linked_id)) != cited_key:
750
+ return f"texts/{tid} 是 {linked_id},不是引文写的 {cited}"
751
+ title = citation.get("title")
752
+ linked_title = payload.get("title_zh")
753
+ if title and linked_title:
754
+ book = re.split(r"[·・‧〈<]", title, maxsplit=1)[0].strip()
755
+ if book and _titles_agree(book, str(linked_title)) is False:
756
+ return f"引文题名《{title}》对不上 texts/{tid} 的《{linked_title}》"
757
+ return None
758
+
759
+
760
+ def _check_one_text_id(
761
+ session_factory, base_url: str, tid: str, timeout: int, citations=()
762
+ ):
763
+ """核验单个 text_id,返回 (三态结果, 说明)。异常一律收敛成 None,不外抛。
764
+
765
+ `citations` 是用这个 text_id 作链接的 live 引文;给了就还要核对链接是不是
766
+ 它们所说的那部书(见 `_live_link_mismatch`),对不上判 False。
767
+ """
706
768
  try:
707
769
  resp = session_factory().get(f"{base_url}/api/texts/{tid}", timeout=timeout)
708
770
  except Exception as e: # noqa: BLE001 — 传输层失败是"不知道",不是"不存在"
@@ -717,6 +779,11 @@ def _check_one_text_id(session_factory, base_url: str, tid: str, timeout: int):
717
779
  # 200 却不是 JSON:通常是网关错误页,不能据此断定 id 不存在。
718
780
  return None, "200 但正文不是 JSON"
719
781
  if payload:
782
+ if isinstance(payload, dict):
783
+ for citation in citations:
784
+ mismatch = _live_link_mismatch(citation, payload)
785
+ if mismatch:
786
+ return False, mismatch
720
787
  return True, ""
721
788
  # 200 + 空正文归 None,不归 False —— 上面刚写下「404 是唯一该硬失败的信号」,
722
789
  # 这里返回 False 就是在自己的契约上开口子。空信封可能来自 FoJin 换了外层结构、
@@ -758,6 +825,7 @@ def verify_online(
758
825
  base_url: str = "https://fojin.app",
759
826
  timeout: int = VERIFY_TIMEOUT,
760
827
  workers: int = VERIFY_WORKERS,
828
+ citations: list[dict] | None = None,
761
829
  ) -> OnlineVerification:
762
830
  """best-effort:GET /api/texts/{id} 看 live 引文的 text_id 是否真解析。
763
831
 
@@ -770,6 +838,9 @@ def verify_online(
770
838
  return OnlineVerification({}, {}, "requests 未安装")
771
839
 
772
840
  unique = sorted(set(text_ids))
841
+ by_tid: dict[str, list[dict]] = {}
842
+ for citation in citations or ():
843
+ by_tid.setdefault(str(citation.get("text_id")), []).append(citation)
773
844
  if not unique:
774
845
  return OnlineVerification({}, {})
775
846
 
@@ -785,7 +856,14 @@ def verify_online(
785
856
  reasons: dict[str, str] = {}
786
857
  with ThreadPoolExecutor(max_workers=max(1, min(workers, len(unique)))) as pool:
787
858
  futures = {
788
- pool.submit(_check_one_text_id, session_factory, base_url, tid, timeout): tid
859
+ pool.submit(
860
+ _check_one_text_id,
861
+ session_factory,
862
+ base_url,
863
+ tid,
864
+ timeout,
865
+ by_tid.get(tid, ()),
866
+ ): tid
789
867
  for tid in unique
790
868
  }
791
869
  for future in as_completed(futures):
@@ -807,7 +885,10 @@ def main() -> int:
807
885
  p = argparse.ArgumentParser(description="B1 引证核验器")
808
886
  p.add_argument("--master", required=True, help="master slug,如 huineng")
809
887
  p.add_argument("--answer-file", help="答案文件;省略则从 stdin 读")
810
- p.add_argument("--online", action="store_true", help="额外验证 live 引文 text_id 可解析")
888
+ p.add_argument(
889
+ "--online", action="store_true",
890
+ help="额外验证 live 引文:链接可解析,且是引文所说的那部书(经号、书名)",
891
+ )
811
892
  args = p.parse_args()
812
893
 
813
894
  try:
@@ -830,7 +911,9 @@ def main() -> int:
830
911
  exit_code = 1
831
912
 
832
913
  if args.online and report["live"]:
833
- res = verify_online([tid for _, tid in report["live"]])
914
+ res = verify_online(
915
+ [tid for _, tid in report["live"]], citations=report["live_detail"]
916
+ )
834
917
  if res.unreachable:
835
918
  print(f"⚠ --online 跳过:FoJin 不可达({res.unreachable})", file=sys.stderr)
836
919
  else:
@@ -838,7 +921,10 @@ def main() -> int:
838
921
  # 网络状况,不是引文真伪,拿它判 fabricated 会在 FoJin 抖动时把正确
839
922
  # 引用打成伪造,那比漏检更糟。
840
923
  if res.fabricated:
841
- print(f"✗ live 引文 text_id 无法解析: {res.fabricated}", file=sys.stderr)
924
+ detail = "; ".join(
925
+ f"{t}({res.reasons.get(t, '?')})" for t in res.fabricated
926
+ )
927
+ print(f"✗ live 引文链接无法解析或不是所引之书: {detail}", file=sys.stderr)
842
928
  exit_code = 1
843
929
  if res.unknown:
844
930
  # 报出来而不是静默算过 —— 「没查成」必须与「查过没问题」可区分。