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.
- 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-fazang/SKILL.md +6 -3
- package/prebuilt/master-fazang/meta.json +5 -0
- package/prebuilt/master-fazang/references/teaching.md +10 -5
- package/prebuilt/master-fazang/sources/INDEX.md +1 -1
- package/prebuilt/master-fazang/sources/jinshizi-excerpts.md +12 -8
- package/prebuilt/master-fazang/sources/wujiao-zhang-excerpts.md +20 -12
- package/prebuilt/master-fazang/tests/fidelity.jsonl +2 -2
- package/prebuilt/master-kumarajiva/SKILL.md +9 -0
- package/prebuilt/master-kumarajiva/meta.json +15 -0
- package/prebuilt/master-kumarajiva/references/teaching.md +4 -2
- package/prebuilt/master-kumarajiva/references/voice.md +1 -1
- package/prebuilt/master-kumarajiva/sources/zhonglun-excerpts.md +6 -2
- package/prebuilt/master-nagarjuna/references/teaching.md +5 -3
- package/prebuilt/master-nagarjuna/sources/dazhidulun-excerpts.md +13 -19
- package/prebuilt/master-nagarjuna/sources/shizhu-yixing-excerpts.md +5 -5
- package/prebuilt/master-nagarjuna/sources/zhonglun-excerpts.md +22 -1
- package/prebuilt/master-ouyi/SKILL.md +8 -0
- package/prebuilt/master-ouyi/meta.json +10 -0
- package/prebuilt/master-ouyi/references/teaching.md +2 -2
- package/prebuilt/master-ouyi/sources/jiaoguan-gangzong-excerpts.md +9 -7
- package/prebuilt/master-ouyi/sources/mituo-yaojie-excerpts.md +7 -6
- package/prebuilt/master-xuanzang/SKILL.md +6 -0
- package/prebuilt/master-xuanzang/meta.json +10 -0
- package/prebuilt/master-xuanzang/sources/chengweishi-excerpts.md +2 -2
- package/prebuilt/master-xuyun/SKILL.md +6 -0
- package/prebuilt/master-xuyun/meta.json +17 -1
- package/prebuilt/master-xuyun/references/teaching.md +5 -5
- package/prebuilt/master-xuyun/sources/lengyanjing-excerpts.md +1 -1
- 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 +6 -1
- package/prebuilt/master-zhiyi/references/teaching.md +3 -3
- package/prebuilt/master-zhiyi/references/voice.md +1 -1
- package/prebuilt/master-zhiyi/sources/INDEX.md +1 -1
- package/prebuilt/master-zhiyi/sources/fahua-xuanyi-excerpts.md +18 -15
- package/prebuilt/master-zhiyi/sources/mohezhiguan-excerpts.md +6 -2
- 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 +496 -5
|
@@ -20,13 +20,13 @@
|
|
|
20
20
|
|
|
21
21
|
空、假、中三谛非前后次第,非一非异,三谛互具互融。一空一切空,假中皆空;一假一切假,空中皆假;一中一切中,空假皆中。三谛圆融无碍,即是实相。此为天台教观的理论基石。
|
|
22
22
|
|
|
23
|
-
> 出处:【《法华玄义》卷二下】→ https://fojin.app/texts/
|
|
23
|
+
> 出处:【《法华玄义》卷二下】→ https://fojin.app/texts/7889
|
|
24
24
|
|
|
25
25
|
### 3. 五时八教判教体系
|
|
26
26
|
|
|
27
27
|
五时:华严时、阿含时、方等时、般若时、法华涅槃时。八教分化仪四教(顿、渐、秘密、不定)与化法四教(藏、通、别、圆)。以此体系统摄佛陀一代时教,判定诸经深浅先后,法华为纯圆独妙,会三归一。
|
|
28
28
|
|
|
29
|
-
> 出处:【《法华玄义》卷一上】→ https://fojin.app/texts/
|
|
29
|
+
> 出处:【《法华玄义》卷一上】→ https://fojin.app/texts/7889
|
|
30
30
|
|
|
31
31
|
### 4. 止观双修(一心三观)
|
|
32
32
|
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
|------|------|------|
|
|
47
47
|
| 《妙法莲华经》 | 天台宗根本经典,开权显实,会三归一 | [阅读原文](https://fojin.app/texts/6513) |
|
|
48
48
|
| 《摩诃止观》 | 天台三大部之一,圆顿止观之集大成 | [阅读原文](https://fojin.app/texts/53) |
|
|
49
|
-
| 《妙法莲华经玄义》 | 天台三大部之一,法华经深义之系统阐发 | [阅读原文](https://fojin.app/texts/
|
|
49
|
+
| 《妙法莲华经玄义》 | 天台三大部之一,法华经深义之系统阐发 | [阅读原文](https://fojin.app/texts/7889) |
|
|
50
50
|
| 《妙法莲华经文句》 | 天台三大部之一,法华经逐句注释 | [阅读原文](https://fojin.app/texts/52) |
|
|
51
51
|
| 《修习止观坐禅法要》(小止观) | 止观入门之作,初学必读 | [阅读原文](https://fojin.app/texts/8085) |
|
|
52
52
|
| 《观音玄义》 | 天台五小部之一,性具善恶之重要出处 | [阅读原文](https://fojin.app/texts/7898) |
|
|
@@ -92,5 +92,5 @@
|
|
|
92
92
|
|
|
93
93
|
- "建议阅读《修习止观坐禅法要》(小止观)作为入门 → [FoJin 原文](https://fojin.app/texts/8085)"
|
|
94
94
|
- "可参考《摩诃止观》深入圆顿止观之理 → [FoJin 原文](https://fojin.app/texts/53)"
|
|
95
|
-
- "关于法华经义理,可详阅《法华玄义》 → [FoJin 原文](https://fojin.app/texts/
|
|
95
|
+
- "关于法华经义理,可详阅《法华玄义》 → [FoJin 原文](https://fojin.app/texts/7889)"
|
|
96
96
|
- "可在 FoJin 词典中查阅相关佛学术语的详解"
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
| 文件 | 来源经典 | CBETA | FoJin | 覆盖主题 |
|
|
8
8
|
|---|---|---|---|---|
|
|
9
9
|
| `mohezhiguan-excerpts.md` | 《摩訶止觀》 | T1911 | [53](https://fojin.app/texts/53) | 一念三千、一心三观、二十五方便、十境十乘、六即佛、圆顿止观 |
|
|
10
|
-
| `fahua-xuanyi-excerpts.md` | 《妙法蓮華經玄義》 | T1716 | [
|
|
10
|
+
| `fahua-xuanyi-excerpts.md` | 《妙法蓮華經玄義》 | T1716 | [7889](https://fojin.app/texts/7889) | 五重玄义、五时判教、八教、三谛圆融、开权显实、火宅三车 |
|
|
11
11
|
|
|
12
12
|
## 引用规范
|
|
13
13
|
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# 《妙法蓮華經玄義》关键片段
|
|
2
2
|
|
|
3
|
-
> 智顗大师说,弟子灌顶记。CBETA ID: T1716。FoJin: https://fojin.app/texts/
|
|
3
|
+
> 智顗大师说,弟子灌顶记。CBETA ID: T1716。FoJin: https://fojin.app/texts/7889
|
|
4
4
|
> 本文件为教学引用用,节选自 CBETA 公开资料。完整经文请访问 FoJin 或 CBETA。
|
|
5
5
|
|
|
6
6
|
## 五重玄义(卷一上)
|
|
7
7
|
|
|
8
8
|
解经总纲——**释名、辨体、明宗、论用、判教相**。
|
|
9
9
|
|
|
10
|
-
**引用格式:**【《法華玄義》卷一上,T1716】→ https://fojin.app/texts/
|
|
10
|
+
**引用格式:**【《法華玄義》卷一上,T1716】→ https://fojin.app/texts/7889
|
|
11
11
|
|
|
12
12
|
**教义要点:**
|
|
13
13
|
- 释名:解经题
|
|
@@ -20,11 +20,13 @@
|
|
|
20
20
|
|
|
21
21
|
## 五时判教(卷十上)
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
要义(整理,非原文):
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
五时:华严时、阿含时(鹿苑)、方等时、般若时、法华涅槃时,配乳、酪、生酥、熟酥、醍醐五味。
|
|
26
26
|
|
|
27
|
-
**引用格式:**【《法華玄義》卷十上,T1716】→ https://fojin.app/texts/
|
|
27
|
+
**引用格式:**【《法華玄義》卷十上,T1716】→ https://fojin.app/texts/7889
|
|
28
|
+
|
|
29
|
+
注:上面一句是整理,不是《法华玄义》原文,不可加引号作引文。玄义卷十以五味讨论判教;下列年数是后世天台家的通说。
|
|
28
30
|
|
|
29
31
|
**教义要点:**
|
|
30
32
|
- **华严时**(21日):顿说大法,如日出先照高山
|
|
@@ -41,7 +43,7 @@
|
|
|
41
43
|
**化仪四教**(佛说法的方式):顿、渐、秘密、不定
|
|
42
44
|
**化法四教**(所说内容的深浅):藏、通、别、圆
|
|
43
45
|
|
|
44
|
-
**引用格式:**【《法華玄義》卷十上,T1716】→ https://fojin.app/texts/
|
|
46
|
+
**引用格式:**【《法華玄義》卷十上,T1716】→ https://fojin.app/texts/7889
|
|
45
47
|
|
|
46
48
|
**教义要点:**
|
|
47
49
|
- 化仪如药方(怎么说)
|
|
@@ -53,28 +55,29 @@
|
|
|
53
55
|
|
|
54
56
|
---
|
|
55
57
|
|
|
56
|
-
##
|
|
58
|
+
## 三谛圆融(卷二)
|
|
57
59
|
|
|
58
60
|
原典(节选):
|
|
59
61
|
|
|
60
|
-
>
|
|
62
|
+
> 圓三諦者,非但中道具足佛法,真、俗亦然。三諦圓融,一三、三一,如《止觀》中說云云。
|
|
61
63
|
|
|
62
|
-
|
|
64
|
+
**引用格式:**【《法華玄義》卷二,T1716】→ https://fojin.app/texts/7889
|
|
63
65
|
|
|
64
66
|
**教义要点:**
|
|
65
67
|
- 空、假、中三谛非前后次第
|
|
66
|
-
-
|
|
68
|
+
- 三谛互具互融,一三、三一
|
|
69
|
+
- 玄义此处指向《摩诃止观》详说;「一空一切空……」一段见 `mohezhiguan-excerpts.md` §一心三观
|
|
67
70
|
- 为天台教观的理论基石
|
|
68
71
|
|
|
69
72
|
---
|
|
70
73
|
|
|
71
|
-
##
|
|
74
|
+
## 开权显实(卷一)
|
|
72
75
|
|
|
73
|
-
|
|
76
|
+
原典(节选):
|
|
74
77
|
|
|
75
|
-
>
|
|
78
|
+
> 九、開權顯實者,一切諸法莫不皆妙,一色一香無非中道,眾生情隔於妙耳。大悲順物,不與世諍,是故明諸權、實不同。
|
|
76
79
|
|
|
77
|
-
|
|
80
|
+
**引用格式:**【《法華玄義》卷一,T1716】→ https://fojin.app/texts/7889
|
|
78
81
|
|
|
79
82
|
**教义要点:**
|
|
80
83
|
- "权":方便权教(三乘、诸经权说)
|
|
@@ -87,7 +90,7 @@
|
|
|
87
90
|
|
|
88
91
|
## 火宅三车(引法华经譬喻品)
|
|
89
92
|
|
|
90
|
-
**引用格式:**【《法華玄義》卷八下引《法華經》卷二,T1716/T0262】→ https://fojin.app/texts/
|
|
93
|
+
**引用格式:**【《法華玄義》卷八下引《法華經》卷二,T1716/T0262】→ https://fojin.app/texts/7889
|
|
91
94
|
|
|
92
95
|
**教义要点:**
|
|
93
96
|
- 长者以羊车(声闻)、鹿车(缘觉)、牛车(菩萨)诱子出火宅
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
|
|
24
24
|
原典(节选):
|
|
25
25
|
|
|
26
|
-
>
|
|
26
|
+
> 一空一切空,无假中而不空,总空观也。一假一切假,无空中而不假,总假观也。一中一切中,无空假而不中,总中观也。即《中论》所说不可思议一心三观。
|
|
27
27
|
|
|
28
28
|
**引用格式:**【《摩訶止觀》卷五上,T1911】→ https://fojin.app/texts/53
|
|
29
29
|
|
|
@@ -99,10 +99,14 @@
|
|
|
99
99
|
|
|
100
100
|
原典(节选):
|
|
101
101
|
|
|
102
|
-
>
|
|
102
|
+
> 止观明静,前代未闻。智者,大隋开皇十四年四月二十六日,于荆州玉泉寺,一夏敷扬、二时慈霔……
|
|
103
|
+
>
|
|
104
|
+
> 「圆顿」者,初缘实相,造境即中,无不真实。系缘法界,一念法界,一色一香无非中道。己界及佛界、众生界亦然。……法性寂然名「止」,寂而常照名「观」。虽言初后,无二无别,是名「圆顿止观」。
|
|
103
105
|
|
|
104
106
|
**引用格式:**【《摩訶止觀》卷一上,T1911】→ https://fojin.app/texts/53
|
|
105
107
|
|
|
108
|
+
注:第一段是灌顶序,记智者大师于隋开皇十四年(594)在荆州玉泉寺讲出本书。
|
|
109
|
+
|
|
106
110
|
**教义要点:**
|
|
107
111
|
- "圆顿"区别于渐次、不定三种止观
|
|
108
112
|
- 不历次第,初心即观实相
|
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
|
|
7
7
|
| 系统 | 适用 | 编号格式 | 示例 |
|
|
8
8
|
|------|------|---------|------|
|
|
9
|
-
| **CBETA** | 汉文藏经 | `<藏别><册号>n<经号>` | `T08n0235`(金刚经)/ `X62n1182
|
|
9
|
+
| **CBETA** | 汉文藏经 | `<藏别><册号>n<经号>` | `T08n0235`(金刚经)/ `X62n1182`(彻悟禅师语录) |
|
|
10
10
|
| **BDRC** | 藏文文献 | `BDRC:W<编号>`;具体版本可用 `BDRC:MW<编号>` | `BDRC:W22084`(作品 ID) |
|
|
11
11
|
| **SuttaCentral** | 巴利经藏 | `MN 10` / `SC:MN 10`,或语料库声明 `SuttaCentral` | `MN 10`(中部第 10 经《念处经》) |
|
|
12
12
|
| **Toh.** | 藏文大藏经德格版 | `Toh <序号>` | `Toh 4465`(阿底峡《菩提道灯论》) |
|
|
13
13
|
| **PTS** | 巴利圣典协会版本 | `PTS:<作品简称>`;册页 / 章节单列为 locator | `PTS:Vism`,locator `I.85` |
|
|
14
|
-
| **compiled teachings** | 经授权或合规收录的编纂开示 | `<权利人或语料名>:<作品 ID>` | `AjahnChah:FoodForTheHeart` |
|
|
14
|
+
| **compiled teachings** | 经授权或合规收录的编纂开示 | `<权利人或语料名>:<作品 ID>` | `AjahnChah:FoodForTheHeart` / `Yinguang:WenchaoZhengbian`(印光文钞,CBETA 未收) |
|
|
15
15
|
|
|
16
16
|
## 来源中立 Citation Contract
|
|
17
17
|
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Check which Windows subsystem a PE executable was linked for.
|
|
3
|
+
|
|
4
|
+
The field is the Subsystem word of the PE optional header: 2 for a GUI
|
|
5
|
+
program, 3 for a console one (`file` prints the latter as `PE32+ executable
|
|
6
|
+
(console)`). The desktop manager is kept a console program. A GUI-subsystem
|
|
7
|
+
build stops double-clicking from opening a console window, but on
|
|
8
|
+
release-desktop run 34858072308 PowerShell did not wait for it, closed the
|
|
9
|
+
pipe, and `--help > help.txt` panicked. The release smoke test runs this with
|
|
10
|
+
`--expect 3` so the switch cannot come back unmeasured.
|
|
11
|
+
|
|
12
|
+
Output is ASCII only. This runs on the Windows release runner, whose console
|
|
13
|
+
encoding is cp1252; a check mark there raised UnicodeEncodeError after the
|
|
14
|
+
check itself had passed.
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
python scripts/check-pe-subsystem.py <exe> --expect 2
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import argparse
|
|
23
|
+
import struct
|
|
24
|
+
import sys
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
SUBSYSTEMS = {2: "Windows GUI", 3: "Windows console"}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def pe_subsystem(data: bytes) -> int:
|
|
31
|
+
"""Return the Subsystem field of a PE image's optional header."""
|
|
32
|
+
if data[:2] != b"MZ":
|
|
33
|
+
raise ValueError("not a PE file: no MZ header")
|
|
34
|
+
(pe_offset,) = struct.unpack_from("<I", data, 0x3C)
|
|
35
|
+
if data[pe_offset:pe_offset + 4] != b"PE\0\0":
|
|
36
|
+
raise ValueError("not a PE file: no PE signature")
|
|
37
|
+
# The optional header follows the 4-byte signature and the 20-byte COFF
|
|
38
|
+
# header; Subsystem sits 68 bytes into it in both PE32 and PE32+.
|
|
39
|
+
(subsystem,) = struct.unpack_from("<H", data, pe_offset + 24 + 68)
|
|
40
|
+
return subsystem
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def main(argv: list[str] | None = None) -> int:
|
|
44
|
+
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
45
|
+
parser.add_argument("exe", type=Path)
|
|
46
|
+
parser.add_argument("--expect", type=int, required=True, help="2 = GUI, 3 = console")
|
|
47
|
+
args = parser.parse_args(argv)
|
|
48
|
+
try:
|
|
49
|
+
with args.exe.open("rb") as handle:
|
|
50
|
+
subsystem = pe_subsystem(handle.read(4096))
|
|
51
|
+
except (OSError, ValueError, struct.error) as exc:
|
|
52
|
+
print(f"FAIL {args.exe}: {exc}")
|
|
53
|
+
return 1
|
|
54
|
+
found = f"{subsystem} ({SUBSYSTEMS.get(subsystem, 'other')})"
|
|
55
|
+
if subsystem != args.expect:
|
|
56
|
+
wanted = f"{args.expect} ({SUBSYSTEMS.get(args.expect, 'other')})"
|
|
57
|
+
print(f"FAIL {args.exe}: subsystem {found}, expected {wanted}")
|
|
58
|
+
return 1
|
|
59
|
+
print(f"OK {args.exe}: subsystem {found}")
|
|
60
|
+
return 0
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
if __name__ == "__main__":
|
|
64
|
+
sys.exit(main())
|
|
@@ -13,6 +13,12 @@ The ¥3.89 sweep is re-measurable for nothing, and a family added to the auditor
|
|
|
13
13
|
has to show what it bought instead of asserting it.
|
|
14
14
|
|
|
15
15
|
python3 scripts/reaudit-report.py eval/reports/0.11.0-06b8142-deepseek.json
|
|
16
|
+
python3 scripts/reaudit-report.py eval/reports/0.11.0-06b8142-deepseek.json --online
|
|
17
|
+
|
|
18
|
+
`--online` also asks FoJin about the run's live citations, the ones that passed
|
|
19
|
+
offline on a link alone: does each link open, and is it the work the citation
|
|
20
|
+
names (same sutra number, agreeing title)? It needs the network and reads
|
|
21
|
+
nothing back into the report.
|
|
16
22
|
|
|
17
23
|
The report file itself is never rewritten: it is the record of what that run
|
|
18
24
|
measured with that instrument, and editing it would be rewriting the experiment.
|
|
@@ -29,6 +35,7 @@ from verify_citations import ( # noqa: E402
|
|
|
29
35
|
load_declared_ids,
|
|
30
36
|
load_member_aliases,
|
|
31
37
|
load_title_aliases,
|
|
38
|
+
verify_online,
|
|
32
39
|
)
|
|
33
40
|
|
|
34
41
|
|
|
@@ -80,6 +87,7 @@ def reaudit(report: dict) -> dict:
|
|
|
80
87
|
continue
|
|
81
88
|
|
|
82
89
|
checked = unparsed = 0
|
|
90
|
+
live: list[dict] = []
|
|
83
91
|
fabricated: list[str] = []
|
|
84
92
|
noncitation: list[str] = []
|
|
85
93
|
for result in suite["results"]:
|
|
@@ -92,6 +100,7 @@ def reaudit(report: dict) -> dict:
|
|
|
92
100
|
len(audit["offline"]) + len(audit["live"]) + len(audit["fabricated"])
|
|
93
101
|
)
|
|
94
102
|
unparsed += len(audit["unparsed"])
|
|
103
|
+
live.extend(audit["live_detail"])
|
|
95
104
|
fabricated.extend(audit["fabricated"])
|
|
96
105
|
noncitation.extend(audit.get("noncitation", ()))
|
|
97
106
|
recomputed = {"checked": checked, "unparsed": unparsed}
|
|
@@ -104,6 +113,8 @@ def reaudit(report: dict) -> dict:
|
|
|
104
113
|
"recorded": recorded,
|
|
105
114
|
"recomputed": recomputed,
|
|
106
115
|
"fabricated": sorted(set(fabricated)),
|
|
116
|
+
# Passed on a FoJin link alone; only `--online` checks the link.
|
|
117
|
+
"live": live,
|
|
107
118
|
# 判定为「不是引文」的【…】块。不计入覆盖率的分母,但必须
|
|
108
119
|
# 数出来:排除而不申报,和静默跳过没有区别。
|
|
109
120
|
"noncitation": sorted(set(noncitation)),
|
|
@@ -121,12 +132,41 @@ def _coverage(counts: dict | None) -> str:
|
|
|
121
132
|
return f"{counts['checked']}/{total} {counts['checked'] / total:.0%}"
|
|
122
133
|
|
|
123
134
|
|
|
135
|
+
def _print_online(out: dict) -> None:
|
|
136
|
+
"""Ask FoJin whether each stored live citation's link is the work it names."""
|
|
137
|
+
cited = [(s["master"], c) for s in out["suites"] for c in s.get("live", ())]
|
|
138
|
+
if not cited:
|
|
139
|
+
print("\nlive 引文在线核验:这次运行没有 live 引文")
|
|
140
|
+
return
|
|
141
|
+
tids = [c["text_id"] for _, c in cited]
|
|
142
|
+
res = verify_online(tids, citations=[c for _, c in cited])
|
|
143
|
+
print(f"\nlive 引文在线核验:{len(cited)} 条,{len(set(tids))} 个 FoJin 链接")
|
|
144
|
+
if res.unreachable:
|
|
145
|
+
print(f" ⚠ 未能核验:FoJin 不可达({res.unreachable})")
|
|
146
|
+
return
|
|
147
|
+
groups: dict[tuple[str, str], int] = {}
|
|
148
|
+
for master, citation in cited:
|
|
149
|
+
key = (master, citation["text_id"])
|
|
150
|
+
groups[key] = groups.get(key, 0) + 1
|
|
151
|
+
for (master, tid), count in sorted(groups.items()):
|
|
152
|
+
verdict = res.verdicts.get(tid)
|
|
153
|
+
if verdict is True:
|
|
154
|
+
continue
|
|
155
|
+
mark = "✗" if verdict is False else "?"
|
|
156
|
+
print(f" {mark} {master} ×{count} {res.reasons.get(tid, '未能核验')}")
|
|
157
|
+
passed = sum(count for (_, tid), count in groups.items() if res.verdicts.get(tid) is True)
|
|
158
|
+
print(f" ✓ {passed} 条的链接是所引之书")
|
|
159
|
+
|
|
160
|
+
|
|
124
161
|
def main(argv: list[str]) -> int:
|
|
125
|
-
|
|
162
|
+
args = argv[1:]
|
|
163
|
+
online = "--online" in args
|
|
164
|
+
paths = [arg for arg in args if arg != "--online"]
|
|
165
|
+
if len(paths) != 1:
|
|
126
166
|
print(__doc__.strip().splitlines()[0])
|
|
127
|
-
print(f"usage: {Path(argv[0]).name} <eval/reports/*.json>")
|
|
167
|
+
print(f"usage: {Path(argv[0]).name} <eval/reports/*.json> [--online]")
|
|
128
168
|
return 2
|
|
129
|
-
path = Path(
|
|
169
|
+
path = Path(paths[0])
|
|
130
170
|
report = json.loads(path.read_text())
|
|
131
171
|
out = reaudit(report)
|
|
132
172
|
|
|
@@ -156,6 +196,8 @@ def main(argv: list[str]) -> int:
|
|
|
156
196
|
)
|
|
157
197
|
for block in skipped:
|
|
158
198
|
print(f" 【{block}】")
|
|
199
|
+
if online:
|
|
200
|
+
_print_online(out)
|
|
159
201
|
return 0
|
|
160
202
|
|
|
161
203
|
|
|
@@ -14,17 +14,29 @@ as the prescribed format while `meta.json` declares five sources, none of them
|
|
|
14
14
|
That was found by a ¥3.89 graded run over 211 fixtures, which caught it only
|
|
15
15
|
because one fixture happened to trigger it. This finds every instance of the
|
|
16
16
|
class deterministically, for free, on every PR.
|
|
17
|
+
|
|
18
|
+
It reads source ids outside 【…】 too. A routing table is an instruction as
|
|
19
|
+
much as a citation template is, and until 2026-09-14 this gate saw only
|
|
20
|
+
bracketed citations — while six genuine works that personas' own tables and
|
|
21
|
+
prose pointed at had never been declared (see `_bare_ids`).
|
|
17
22
|
"""
|
|
18
23
|
from __future__ import annotations
|
|
19
24
|
|
|
20
25
|
import json
|
|
21
26
|
import re
|
|
22
27
|
import sys
|
|
28
|
+
from collections import Counter
|
|
23
29
|
from dataclasses import dataclass
|
|
24
30
|
from pathlib import Path
|
|
25
31
|
|
|
26
32
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
27
|
-
from verify_citations import
|
|
33
|
+
from verify_citations import ( # noqa: E402
|
|
34
|
+
audit_answer,
|
|
35
|
+
extract_citation_ids,
|
|
36
|
+
load_declared_ids,
|
|
37
|
+
load_member_aliases,
|
|
38
|
+
load_title_aliases,
|
|
39
|
+
)
|
|
28
40
|
|
|
29
41
|
PREBUILT_DIR = Path(__file__).resolve().parent.parent / "prebuilt"
|
|
30
42
|
|
|
@@ -47,12 +59,14 @@ _TEMPLATE_MARKERS = (
|
|
|
47
59
|
# been permitted. Do not add to it to turn a red build green — that is exactly
|
|
48
60
|
# the failure this gate exists to prevent.
|
|
49
61
|
#
|
|
50
|
-
# Empty as of 2026-09-
|
|
62
|
+
# Empty as of 2026-09-14. The first two findings this gate recorded were resolved
|
|
51
63
|
# by declaring the source: `Toh:3861` in master-tsongkhapa/meta.json (月称《入
|
|
52
64
|
# 中论》is a real Tengyur text Tsongkhapa's tradition treats as its own
|
|
53
65
|
# foundation) and `J36nB348` in master-ouyi/meta.json (《灵峰宗论》is Ouyi's own
|
|
54
66
|
# collected works). Neither needed a B1 contract change — both simply belonged
|
|
55
|
-
# in the declared set. See CHANGELOG.md for the maintainer decision.
|
|
67
|
+
# in the declared set. See CHANGELOG.md for the maintainer decision. The six the
|
|
68
|
+
# bare-id sweep found on 2026-09-14 were resolved the same way, never entered
|
|
69
|
+
# here.
|
|
56
70
|
KNOWN_UNDECLARED: dict[tuple[str, str], str] = {}
|
|
57
71
|
|
|
58
72
|
|
|
@@ -77,6 +91,33 @@ def _strip_template_citations(text: str) -> str:
|
|
|
77
91
|
return "".join(out)
|
|
78
92
|
|
|
79
93
|
|
|
94
|
+
# `BDRC W-number` in a persona's own rules names a field, it is not an id:
|
|
95
|
+
# master-tsongkhapa says 不得编造未验证的 BDRC W-number, master-atisha asks for
|
|
96
|
+
# a BDRC W-ID. `_FAMILY_ID` stays loose on purpose — in an answer, reading too
|
|
97
|
+
# much fails safe as fabricated (see the note above `_FOJIN_TEXT_LINK` in
|
|
98
|
+
# verify_citations.py) — so it reads both as ids. Only this prose sweep drops
|
|
99
|
+
# them: a real BDRC work id is W followed by a digit (W22272, W1KG14334), the
|
|
100
|
+
# rule the auditor's own bare-W branch already applies.
|
|
101
|
+
_BDRC_FIELD_NAME = re.compile(r"^BDRC:W(?![0-9])")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _bare_ids(text: str) -> list[str]:
|
|
105
|
+
"""Source ids written outside every 【…】 block: table cells, prose, frontmatter.
|
|
106
|
+
|
|
107
|
+
`audit_answer` reads only bracketed citations, so a persona's routing table
|
|
108
|
+
was invisible to this gate. master-xuanzang's SKILL.md sent 五位百法
|
|
109
|
+
questions to `《百法明门论》,T31n1614` in a table cell; master-ouyi had an
|
|
110
|
+
offline excerpt file for 《教觀綱宗》 T46n1939. Neither id was declared.
|
|
111
|
+
|
|
112
|
+
Callers audit each id alone, as `【id】` with nothing after it, so a FoJin
|
|
113
|
+
link in the same table row cannot make it `live`: in an answer a link can
|
|
114
|
+
vouch for one citation, but in the persona's own material a link is not a
|
|
115
|
+
declaration.
|
|
116
|
+
"""
|
|
117
|
+
ids = extract_citation_ids(_BLOCK.sub(" ", text))
|
|
118
|
+
return [cid for cid in ids if not _BDRC_FIELD_NAME.match(cid)]
|
|
119
|
+
|
|
120
|
+
|
|
80
121
|
@dataclass(frozen=True)
|
|
81
122
|
class Finding:
|
|
82
123
|
master: str
|
|
@@ -84,8 +125,12 @@ class Finding:
|
|
|
84
125
|
path: str
|
|
85
126
|
|
|
86
127
|
|
|
87
|
-
def find_undeclared(prebuilt_dir: Path) -> list[Finding]:
|
|
88
|
-
"""Every citation the personas' own material makes that meta.json omits.
|
|
128
|
+
def find_undeclared(prebuilt_dir: Path, reach: Counter | None = None) -> list[Finding]:
|
|
129
|
+
"""Every citation the personas' own material makes that meta.json omits.
|
|
130
|
+
|
|
131
|
+
`reach`, when given, counts what was read: bracketed citations the audit
|
|
132
|
+
could resolve to an id, and bare ids outside brackets.
|
|
133
|
+
"""
|
|
89
134
|
findings: list[Finding] = []
|
|
90
135
|
for persona in sorted(Path(prebuilt_dir).iterdir()):
|
|
91
136
|
meta_path = persona / "meta.json"
|
|
@@ -100,6 +145,13 @@ def find_undeclared(prebuilt_dir: Path) -> list[Finding]:
|
|
|
100
145
|
try:
|
|
101
146
|
declared = load_declared_ids(persona.name, base=str(prebuilt_dir))
|
|
102
147
|
aliases = load_member_aliases(persona.name, base=str(prebuilt_dir))
|
|
148
|
+
# Declared titles, as reaudit-report.py and test-fidelity.py already
|
|
149
|
+
# pass them. Without them a source with no sutra number is unreadable:
|
|
150
|
+
# master-yinguang's own 【《印光法師文鈔正編》卷一】 examples counted as
|
|
151
|
+
# nothing, and the sweep read fewer citations after the Wenchao was
|
|
152
|
+
# re-declared. An alias only makes a block readable; it cannot turn an
|
|
153
|
+
# undeclared id into a declared one.
|
|
154
|
+
titles = load_title_aliases(persona.name, base=str(prebuilt_dir))
|
|
103
155
|
except (FileNotFoundError, ValueError):
|
|
104
156
|
continue # meta.json exists (meta_path.is_file() above) but is unreadable
|
|
105
157
|
if not declared:
|
|
@@ -111,7 +163,17 @@ def find_undeclared(prebuilt_dir: Path) -> list[Finding]:
|
|
|
111
163
|
if not doc.is_file():
|
|
112
164
|
continue
|
|
113
165
|
text = _strip_template_citations(doc.read_text(encoding="utf-8"))
|
|
114
|
-
|
|
166
|
+
bracketed = audit_answer(declared, text, aliases, titles)
|
|
167
|
+
undeclared = list(bracketed["fabricated"])
|
|
168
|
+
bare = _bare_ids(text)
|
|
169
|
+
for cid in bare:
|
|
170
|
+
undeclared += audit_answer(declared, f"【{cid}】", aliases)["fabricated"]
|
|
171
|
+
if reach is not None:
|
|
172
|
+
reach["bracketed"] += sum(
|
|
173
|
+
len(bracketed[bucket]) for bucket in ("offline", "live", "fabricated")
|
|
174
|
+
)
|
|
175
|
+
reach["bare"] += len(bare)
|
|
176
|
+
for citation in dict.fromkeys(undeclared):
|
|
115
177
|
findings.append(
|
|
116
178
|
Finding(persona.name, citation, str(doc.relative_to(prebuilt_dir.parent)))
|
|
117
179
|
)
|
|
@@ -119,7 +181,18 @@ def find_undeclared(prebuilt_dir: Path) -> list[Finding]:
|
|
|
119
181
|
|
|
120
182
|
|
|
121
183
|
def main() -> int:
|
|
122
|
-
|
|
184
|
+
reach: Counter = Counter()
|
|
185
|
+
findings = find_undeclared(PREBUILT_DIR, reach)
|
|
186
|
+
print(
|
|
187
|
+
f"Read {reach['bracketed']} bracketed citations and {reach['bare']} bare "
|
|
188
|
+
"source ids in the personas' own docs."
|
|
189
|
+
)
|
|
190
|
+
if not reach["bracketed"] or not reach["bare"]:
|
|
191
|
+
print(
|
|
192
|
+
"FAIL: the sweep read nothing of one kind — it is not looking at what "
|
|
193
|
+
"it claims to check."
|
|
194
|
+
)
|
|
195
|
+
return 1
|
|
123
196
|
known, new = [], []
|
|
124
197
|
for f in findings:
|
|
125
198
|
(known if (f.master, f.citation) in KNOWN_UNDECLARED else new).append(f)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Gate: a persona's self-audit list must include every source it declares.
|
|
3
|
+
|
|
4
|
+
Nine personas end SKILL.md with the same pre-answer rule (B1): before replying,
|
|
5
|
+
check that each offline citation's identifier "∈ 本 master frontmatter `sources:`
|
|
6
|
+
声明的对应字段", and strip any claim that fails. The list the model checks
|
|
7
|
+
against is the frontmatter, not meta.json, and nothing compared the two.
|
|
8
|
+
|
|
9
|
+
master-ouyi declares 《灵峰宗论》 `J36nB348` in meta.json, and its own
|
|
10
|
+
references/teaching.md cites it, but its frontmatter never listed it. A model
|
|
11
|
+
following the rule to the letter deletes a correct citation of Ouyi's own
|
|
12
|
+
collected works. The audit would never notice: it reads meta.json.
|
|
13
|
+
|
|
14
|
+
Personas whose rule points at meta.json instead are not examined here.
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
python3 scripts/validate-self-audit-sources.py
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import json
|
|
23
|
+
import sys
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
import yaml
|
|
27
|
+
|
|
28
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
29
|
+
from verify_citations import audit_answer # noqa: E402
|
|
30
|
+
|
|
31
|
+
PREBUILT = Path(__file__).resolve().parent.parent / "prebuilt"
|
|
32
|
+
|
|
33
|
+
# The phrase every frontmatter-pointing rule uses. If the wording changes, the
|
|
34
|
+
# gate examines nothing and says so instead of passing.
|
|
35
|
+
_RULE = "frontmatter `sources:`"
|
|
36
|
+
|
|
37
|
+
# The identifier fields the rule itself enumerates.
|
|
38
|
+
_ID_FIELDS = (
|
|
39
|
+
"cbeta_id", "toh_id", "bdrc_id", "pts_id", "suttacentral", "suttacentral_id", "teaching_id",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _frontmatter_ids(text: str) -> list[str]:
|
|
44
|
+
parts = text.split("---", 2)
|
|
45
|
+
if len(parts) < 3 or parts[0].strip():
|
|
46
|
+
return []
|
|
47
|
+
front = yaml.safe_load(parts[1]) or {}
|
|
48
|
+
ids: list[str] = []
|
|
49
|
+
for src in front.get("sources") or []:
|
|
50
|
+
if isinstance(src, dict):
|
|
51
|
+
ids += [str(src[field]) for field in _ID_FIELDS if src.get(field) is not None]
|
|
52
|
+
return ids
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _covered(declared_id: str, listed: list[str]) -> bool:
|
|
56
|
+
"""Does some frontmatter identifier name `declared_id`, in a spelling the audit accepts?
|
|
57
|
+
|
|
58
|
+
The resolution is the auditor's own. master-zhiyi's frontmatter writes
|
|
59
|
+
`T1716` for the declared `T33n1716`; master-milarepa writes `W22272` for
|
|
60
|
+
`BDRC:W22272`. A model citing either passes the audit, so the list covers
|
|
61
|
+
the source. Reimplementing those rules here would drift from them.
|
|
62
|
+
"""
|
|
63
|
+
return any(
|
|
64
|
+
declared_id in audit_answer({declared_id}, f"【{value}】")["offline"] for value in listed
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def check_persona(persona: Path) -> tuple[list[str], bool]:
|
|
69
|
+
"""Return (problems, whether this persona was examined)."""
|
|
70
|
+
skill, meta = persona / "SKILL.md", persona / "meta.json"
|
|
71
|
+
if not skill.is_file() or not meta.is_file():
|
|
72
|
+
return [], False
|
|
73
|
+
text = skill.read_text(encoding="utf-8")
|
|
74
|
+
if _RULE not in text:
|
|
75
|
+
return [], False
|
|
76
|
+
rule_line = text[: text.index(_RULE)].count("\n") + 1
|
|
77
|
+
listed = _frontmatter_ids(text)
|
|
78
|
+
problems: list[str] = []
|
|
79
|
+
for src in json.loads(meta.read_text(encoding="utf-8")).get("sources", []):
|
|
80
|
+
source_id = src.get("id")
|
|
81
|
+
if source_id and not _covered(source_id, listed):
|
|
82
|
+
problems.append(
|
|
83
|
+
f"{persona.name}: meta.json declares {source_id} ({src.get('title', '')}) "
|
|
84
|
+
f"but the frontmatter `sources:` never lists it. The self-audit rule at "
|
|
85
|
+
f"SKILL.md:{rule_line} checks citations against that list, so a correct "
|
|
86
|
+
f"citation of this source would be stripped."
|
|
87
|
+
)
|
|
88
|
+
return problems, True
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def main(prebuilt: Path = PREBUILT) -> int:
|
|
92
|
+
problems: list[str] = []
|
|
93
|
+
examined: list[str] = []
|
|
94
|
+
for persona in sorted(p for p in prebuilt.iterdir() if p.is_dir()):
|
|
95
|
+
found, looked = check_persona(persona)
|
|
96
|
+
problems += found
|
|
97
|
+
if looked:
|
|
98
|
+
examined.append(persona.name)
|
|
99
|
+
|
|
100
|
+
if not examined:
|
|
101
|
+
print(f"✗ no persona's SKILL.md contains {_RULE!r} — this check examined nothing")
|
|
102
|
+
return 1
|
|
103
|
+
if problems:
|
|
104
|
+
print(f"✗ {len(problems)} declared source(s) missing from a self-audit list:\n")
|
|
105
|
+
for problem in problems:
|
|
106
|
+
print(f" - {problem}")
|
|
107
|
+
return 1
|
|
108
|
+
print(
|
|
109
|
+
f"✓ self-audit sources ok — {len(examined)} persona(s) check citations against "
|
|
110
|
+
f"their frontmatter: {', '.join(examined)}"
|
|
111
|
+
)
|
|
112
|
+
return 0
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
if __name__ == "__main__":
|
|
116
|
+
sys.exit(main())
|