master-skill 0.11.0 → 0.12.0
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 +48 -55
- package/README_EN.md +72 -59
- package/bin/cli.mjs +12 -7
- package/gemini-extension.json +1 -1
- package/hooks/session-start +68 -77
- package/hooks/session_start.py +152 -0
- package/package.json +5 -2
- package/prebuilt/compare-masters/SKILL.md +21 -2
- package/prebuilt/master-ajahn-chah/meta.json +6 -0
- package/prebuilt/master-ajahn-chah/tests/fidelity.jsonl +6 -6
- package/prebuilt/master-atisha/tests/fidelity.jsonl +4 -4
- package/prebuilt/master-curriculum/references/tiantai.md +1 -1
- package/prebuilt/master-debate/SKILL.md +14 -2
- package/prebuilt/master-fazang/tests/fidelity.jsonl +2 -2
- package/prebuilt/master-help/SKILL.md +9 -1
- package/prebuilt/master-huineng/tests/fidelity.jsonl +4 -4
- package/prebuilt/master-kumarajiva/tests/fidelity.jsonl +3 -3
- package/prebuilt/master-mahasi-sayadaw/tests/fidelity.jsonl +4 -4
- package/prebuilt/master-milarepa/tests/fidelity.jsonl +3 -3
- package/prebuilt/master-nagarjuna/tests/fidelity.jsonl +6 -6
- package/prebuilt/master-ouyi/meta.json +5 -0
- package/prebuilt/master-ouyi/references/teaching.md +3 -3
- package/prebuilt/master-ouyi/tests/fidelity.jsonl +3 -3
- package/prebuilt/master-tsongkhapa/meta.json +6 -0
- package/prebuilt/master-tsongkhapa/tests/fidelity.jsonl +2 -2
- package/prebuilt/master-xuanzang/tests/fidelity.jsonl +3 -3
- package/prebuilt/master-xuyun/tests/fidelity.jsonl +6 -6
- package/prebuilt/master-zhiyi/meta.json +2 -2
- package/prebuilt/master-zhiyi/tests/fidelity.jsonl +2 -2
- package/scripts/check-audit-ignores.py +105 -0
- package/scripts/check-eval-sdk-surface.py +142 -0
- package/scripts/check-gate-liveness.py +205 -6
- package/scripts/reaudit-report.py +163 -0
- package/scripts/regrade-report.py +157 -0
- package/scripts/smoke-eval-sdk.py +174 -0
- package/scripts/test-fidelity.py +684 -52
- package/scripts/validate-citation-references.py +150 -0
- package/scripts/validate-citation-templates.py +176 -0
- package/scripts/validate-fixture-terms.py +127 -0
- package/scripts/verify-adjudication.py +316 -0
- package/scripts/verify_citations.py +739 -39
- package/tools/cross_reference.py +44 -10
- package/tools/fojin-known-absent.json +14 -0
- package/tools/fojin_bridge.py +138 -8
- package/tools/rag_query.py +45 -2
- package/tools/skill_writer.py +50 -7
- package/tools/verify_sources.py +240 -15
- package/hooks/tests/test_run_hook.sh +0 -114
- package/hooks/tests/test_run_hook_cmd.sh +0 -94
- package/hooks/tests/test_session_start.sh +0 -149
- package/scripts/tests/test_check_gate_liveness.py +0 -232
- package/scripts/tests/test_check_manifest_versions.py +0 -217
- package/scripts/tests/test_check_response.py +0 -190
- package/scripts/tests/test_debate_protocol.py +0 -159
- package/scripts/tests/test_fidelity_providers.py +0 -202
- package/scripts/tests/test_injection_hardening.py +0 -174
- package/scripts/tests/test_select_fidelity_smoke.py +0 -142
- package/scripts/tests/test_validate.py +0 -145
- package/scripts/tests/test_validate_citation_contract.py +0 -408
- package/scripts/tests/test_validate_cross_critique.py +0 -149
- package/scripts/tests/test_validate_curriculum_sources.py +0 -144
- package/scripts/tests/test_validate_fidelity.py +0 -59
- package/scripts/tests/test_validate_lore_triggers_content.py +0 -372
- package/scripts/tests/test_validate_persona_fidelity.py +0 -317
- package/scripts/tests/test_validate_promptfoo_configs.py +0 -386
- package/scripts/tests/test_validate_workflow.py +0 -284
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Gate: a persona must not instruct a citation its own contract forbids.
|
|
3
|
+
|
|
4
|
+
`validate-citation-contract.py` checks that `meta.json` declares the right
|
|
5
|
+
*fields*. Nothing checked whether the persona's own SKILL.md, sources/ and
|
|
6
|
+
references/ tell it to cite sources that `meta.json` never declares — and that
|
|
7
|
+
is a shipped defect, not a hypothetical: `master-tsongkhapa`'s SKILL.md gives
|
|
8
|
+
|
|
9
|
+
印度大乘论典所引:`【月称《入中论》§第六章】(Toh 3861)`
|
|
10
|
+
|
|
11
|
+
as the prescribed format while `meta.json` declares five sources, none of them
|
|
12
|
+
`Toh:3861`. Every use of that instruction violates the B1 citation rule.
|
|
13
|
+
|
|
14
|
+
That was found by a ¥3.89 graded run over 211 fixtures, which caught it only
|
|
15
|
+
because one fixture happened to trigger it. This finds every instance of the
|
|
16
|
+
class deterministically, for free, on every PR.
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import re
|
|
22
|
+
import sys
|
|
23
|
+
from dataclasses import dataclass
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
27
|
+
from verify_citations import audit_answer, load_declared_ids, load_member_aliases # noqa: E402
|
|
28
|
+
|
|
29
|
+
PREBUILT_DIR = Path(__file__).resolve().parent.parent / "prebuilt"
|
|
30
|
+
|
|
31
|
+
_BLOCK = re.compile(r"【([^】]*)】")
|
|
32
|
+
# Same attribution region verify_citations uses, so a tag documented after the
|
|
33
|
+
# block ((BDRC: Wxxxxx)) is judged together with the block it belongs to.
|
|
34
|
+
_LINK_WINDOW = 120
|
|
35
|
+
|
|
36
|
+
# Documentation showing the citation *format* is not a citation. Every marker
|
|
37
|
+
# here appears in shipped persona docs; none can occur in a real reference.
|
|
38
|
+
_TEMPLATE_MARKERS = (
|
|
39
|
+
re.compile(r"\{"), # 【《{title}》,{source_id}】
|
|
40
|
+
re.compile(r"[A-Za-z][xX]{3,}"), # (BDRC: Wxxxxx) / Txxnxxxx
|
|
41
|
+
re.compile(r"卷N"), # 【《法華玄義》卷N,T1716】
|
|
42
|
+
re.compile(r"典籍名|章节名"), # 【《典籍名》§章节】
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Open findings, each awaiting a maintainer decision. This is a ratchet, not an
|
|
46
|
+
# allowlist: an entry here is a defect that has been *seen*, not one that has
|
|
47
|
+
# been permitted. Do not add to it to turn a red build green — that is exactly
|
|
48
|
+
# the failure this gate exists to prevent.
|
|
49
|
+
#
|
|
50
|
+
# Empty as of 2026-09-03. Both findings this gate ever recorded were resolved
|
|
51
|
+
# by declaring the source: `Toh:3861` in master-tsongkhapa/meta.json (月称《入
|
|
52
|
+
# 中论》is a real Tengyur text Tsongkhapa's tradition treats as its own
|
|
53
|
+
# foundation) and `J36nB348` in master-ouyi/meta.json (《灵峰宗论》is Ouyi's own
|
|
54
|
+
# collected works). Neither needed a B1 contract change — both simply belonged
|
|
55
|
+
# in the declared set. See CHANGELOG.md for the maintainer decision.
|
|
56
|
+
KNOWN_UNDECLARED: dict[tuple[str, str], str] = {}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def is_template_block(text: str) -> bool:
|
|
60
|
+
"""Is this citation documenting the format rather than citing a source?"""
|
|
61
|
+
return any(marker.search(text) for marker in _TEMPLATE_MARKERS)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _strip_template_citations(text: str) -> str:
|
|
65
|
+
"""Drop format-documentation citations, with their attribution regions."""
|
|
66
|
+
blocks = list(_BLOCK.finditer(text))
|
|
67
|
+
out: list[str] = []
|
|
68
|
+
prev = 0
|
|
69
|
+
for index, match in enumerate(blocks):
|
|
70
|
+
next_start = blocks[index + 1].start() if index + 1 < len(blocks) else len(text)
|
|
71
|
+
region_end = min(next_start, match.end() + _LINK_WINDOW)
|
|
72
|
+
context = match.group(1) + text[match.end():region_end]
|
|
73
|
+
if is_template_block(context):
|
|
74
|
+
out.append(text[prev:match.start()])
|
|
75
|
+
prev = region_end
|
|
76
|
+
out.append(text[prev:])
|
|
77
|
+
return "".join(out)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass(frozen=True)
|
|
81
|
+
class Finding:
|
|
82
|
+
master: str
|
|
83
|
+
citation: str
|
|
84
|
+
path: str
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def find_undeclared(prebuilt_dir: Path) -> list[Finding]:
|
|
88
|
+
"""Every citation the personas' own material makes that meta.json omits."""
|
|
89
|
+
findings: list[Finding] = []
|
|
90
|
+
for persona in sorted(Path(prebuilt_dir).iterdir()):
|
|
91
|
+
meta_path = persona / "meta.json"
|
|
92
|
+
if not persona.is_dir() or not meta_path.is_file():
|
|
93
|
+
continue
|
|
94
|
+
# Found by an independent code-review pass (2026-09-03): this used to
|
|
95
|
+
# reimplement its own meta.json parsing instead of calling
|
|
96
|
+
# verify_citations.py's own loader — a real drift risk, since a future
|
|
97
|
+
# change to how sources/notes are parsed there would silently stop
|
|
98
|
+
# applying here. `persona.name` round-trips through resolve_master_dir
|
|
99
|
+
# (it already accepts the full `master-<slug>` form).
|
|
100
|
+
try:
|
|
101
|
+
declared = load_declared_ids(persona.name, base=str(prebuilt_dir))
|
|
102
|
+
aliases = load_member_aliases(persona.name, base=str(prebuilt_dir))
|
|
103
|
+
except (FileNotFoundError, ValueError):
|
|
104
|
+
continue # meta.json exists (meta_path.is_file() above) but is unreadable
|
|
105
|
+
if not declared:
|
|
106
|
+
continue # nothing to audit against; see master-debate
|
|
107
|
+
docs = [persona / "SKILL.md"]
|
|
108
|
+
docs += sorted(persona.glob("sources/*.md"))
|
|
109
|
+
docs += sorted(persona.glob("references/*.md"))
|
|
110
|
+
for doc in docs:
|
|
111
|
+
if not doc.is_file():
|
|
112
|
+
continue
|
|
113
|
+
text = _strip_template_citations(doc.read_text(encoding="utf-8"))
|
|
114
|
+
for citation in dict.fromkeys(audit_answer(declared, text, aliases)["fabricated"]):
|
|
115
|
+
findings.append(
|
|
116
|
+
Finding(persona.name, citation, str(doc.relative_to(prebuilt_dir.parent)))
|
|
117
|
+
)
|
|
118
|
+
return findings
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def main() -> int:
|
|
122
|
+
findings = find_undeclared(PREBUILT_DIR)
|
|
123
|
+
known, new = [], []
|
|
124
|
+
for f in findings:
|
|
125
|
+
(known if (f.master, f.citation) in KNOWN_UNDECLARED else new).append(f)
|
|
126
|
+
|
|
127
|
+
if known:
|
|
128
|
+
print(f"Known undeclared citations ({len(known)}) — open findings, not permissions:")
|
|
129
|
+
for f in known:
|
|
130
|
+
print(f" {f.master}: {f.citation} ({f.path})")
|
|
131
|
+
for key in dict.fromkeys((f.master, f.citation) for f in known):
|
|
132
|
+
print(f"\n {key[0]} / {key[1]}:\n {KNOWN_UNDECLARED[key]}")
|
|
133
|
+
|
|
134
|
+
if new:
|
|
135
|
+
print(f"\nFAIL: {len(new)} citation(s) not declared in the persona's meta.json:")
|
|
136
|
+
for f in new:
|
|
137
|
+
print(f" {f.master}: {f.citation} ({f.path})")
|
|
138
|
+
print(
|
|
139
|
+
"\nThe persona's own material instructs a citation its citation contract "
|
|
140
|
+
"forbids.\nDeclare the source in meta.json, or stop citing it. Adding it to "
|
|
141
|
+
"KNOWN_UNDECLARED\nwithout a maintainer decision defeats the point of this gate."
|
|
142
|
+
)
|
|
143
|
+
return 1
|
|
144
|
+
|
|
145
|
+
print(f"\nOK: no new undeclared citations across {len(list(PREBUILT_DIR.iterdir()))} skills.")
|
|
146
|
+
return 0
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
if __name__ == "__main__":
|
|
150
|
+
sys.exit(main())
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Gate: a skill must not demand verifiable citations and then show a template
|
|
3
|
+
that cannot carry one.
|
|
4
|
+
|
|
5
|
+
`compare-masters/SKILL.md` did exactly that. Line 223 required every doctrinal
|
|
6
|
+
claim to "解析到所选 persona 的 meta.json.sources[]" and line 259 required a
|
|
7
|
+
"可核验的来源引用" — while the output template two hundred lines earlier read
|
|
8
|
+
|
|
9
|
+
> 出处:【《经名》卷N】→ fojin.app 链接
|
|
10
|
+
|
|
11
|
+
with no identifier in it. The model followed the template, and the 2026-09-12
|
|
12
|
+
sweep measured the result: 18 answers, 49 citations, **zero checkable**, audit
|
|
13
|
+
coverage 0%. The rule and the example were in the same file and contradicted
|
|
14
|
+
each other; nothing was comparing them.
|
|
15
|
+
|
|
16
|
+
The check is deliberately narrow. It does NOT require every citation format to
|
|
17
|
+
carry an identifier — `master-buddhaghosa`, `master-mahasi-sayadaw` and
|
|
18
|
+
`master-tsongkhapa` declare formats without one because PTS / SuttaCentral /
|
|
19
|
+
BDRC are corpus-level references with no per-passage id, which the citation
|
|
20
|
+
contract documents as a known boundary. Requiring an id there would force a
|
|
21
|
+
fabricated one, which is worse than an unverifiable citation.
|
|
22
|
+
|
|
23
|
+
What it requires is consistency: a skill that quotes *other* personas must
|
|
24
|
+
defer to their declared `citation_format` rather than inventing a weaker shape
|
|
25
|
+
of its own. Deferring is also what makes the boundary above work — quoting
|
|
26
|
+
buddhaghosa inherits buddhaghosa's format, id-less and honest.
|
|
27
|
+
|
|
28
|
+
The second rule was added after the first one turned out to examine 1 of the 3
|
|
29
|
+
meta-skills and return early on the other two. `master-debate` requires "至少 1
|
|
30
|
+
个本宗 citation" four times over and never says what one looks like; the same
|
|
31
|
+
sweep shows it wrote 24 real sutra ids as `《坛经》(T48n2008)`. The auditor's
|
|
32
|
+
`_CITATION_BLOCK` parses `【…】` only, so all 24 were invisible and the report
|
|
33
|
+
read `audit_coverage: N/A` — the same cell `master-help`, which legitimately
|
|
34
|
+
never cites, prints. Requiring citations and not naming an auditable shape
|
|
35
|
+
produces citations nobody checks, which reads as clean.
|
|
36
|
+
|
|
37
|
+
Usage:
|
|
38
|
+
python3 scripts/validate-citation-templates.py
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
from __future__ import annotations
|
|
42
|
+
|
|
43
|
+
import re
|
|
44
|
+
import sys
|
|
45
|
+
from pathlib import Path
|
|
46
|
+
|
|
47
|
+
PREBUILT = Path(__file__).resolve().parent.parent / "prebuilt"
|
|
48
|
+
|
|
49
|
+
# A literal citation example: 【…】 with no {placeholder} inside, i.e. a shape
|
|
50
|
+
# the writer intended to be copied rather than filled from a declared format.
|
|
51
|
+
_LITERAL_CITATION = re.compile(r"【《[^》{]*》[^】{]*】")
|
|
52
|
+
|
|
53
|
+
# Phrases by which a file asserts its citations are checkable. Kept to the ones
|
|
54
|
+
# this repo actually uses, so the check cannot drift into policing prose.
|
|
55
|
+
_VERIFIABILITY_CLAIMS = ("可核验", "必须解析到", "可核对")
|
|
56
|
+
|
|
57
|
+
# Skills that quote other personas rather than speaking as one.
|
|
58
|
+
_META_SKILLS = ("compare-masters", "master-debate", "master-curriculum")
|
|
59
|
+
|
|
60
|
+
# The only shape `verify_citations._CITATION_BLOCK` parses. A citation written
|
|
61
|
+
# any other way is served to the reader and audited by nothing.
|
|
62
|
+
_AUDITABLE_BLOCK = re.compile(r"【[^】]*】")
|
|
63
|
+
|
|
64
|
+
# A file demanding citations of its output. Narrow on purpose: `master-help`
|
|
65
|
+
# mentions the word without requiring any, and must not be caught.
|
|
66
|
+
_REQUIRES_CITATION = (
|
|
67
|
+
re.compile(r"至少\s*\d+\s*(?:个|条)[^。\n]{0,12}citation"),
|
|
68
|
+
re.compile(r"引用必须"),
|
|
69
|
+
re.compile(r"必须附[^。\n]{0,12}引用"),
|
|
70
|
+
re.compile(r"引经必经查证"),
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _strip_html_comments(text: str) -> str:
|
|
75
|
+
"""Drop <!-- … --> before scanning.
|
|
76
|
+
|
|
77
|
+
The first run of this check flagged the comment that explains what the bad
|
|
78
|
+
template used to look like — reading the note about the defect as the
|
|
79
|
+
defect. That is the same "the test matched the docstring" shape this repo
|
|
80
|
+
keeps producing, one level up. A comment is not an instruction to the
|
|
81
|
+
model: SKILL.md is rendered as markdown, and an HTML comment does not
|
|
82
|
+
appear in what the model is shown.
|
|
83
|
+
|
|
84
|
+
The newlines are kept, so a reported line number still points at the line a
|
|
85
|
+
maintainer will open. Deleting them outright shifts every later line and
|
|
86
|
+
sends the reader to the wrong place — a small lie in the same family.
|
|
87
|
+
"""
|
|
88
|
+
return re.sub(
|
|
89
|
+
r"<!--.*?-->",
|
|
90
|
+
lambda m: "\n" * m.group().count("\n"),
|
|
91
|
+
text,
|
|
92
|
+
flags=re.S,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def check_skill(path: Path) -> tuple[list[str], list[str]]:
|
|
97
|
+
"""Return (problems, rules that actually examined this file).
|
|
98
|
+
|
|
99
|
+
The second element exists so `main` can print what was looked at. The
|
|
100
|
+
first version of this check returned early on two of the three meta-skills
|
|
101
|
+
and printed a clean line anyway; a gate that says nothing about its own
|
|
102
|
+
reach is how this repo has repeatedly shipped an inert one.
|
|
103
|
+
"""
|
|
104
|
+
text = _strip_html_comments(path.read_text(encoding="utf-8"))
|
|
105
|
+
name = path.parent.name
|
|
106
|
+
problems: list[str] = []
|
|
107
|
+
examined: list[str] = []
|
|
108
|
+
|
|
109
|
+
declares_own_format = re.search(r"^citation_format:", text, re.M) is not None
|
|
110
|
+
|
|
111
|
+
# Rule 1 — a verifiability claim must not sit next to an id-less template.
|
|
112
|
+
claims = [c for c in _VERIFIABILITY_CLAIMS if c in text]
|
|
113
|
+
if claims:
|
|
114
|
+
examined.append("claims-vs-template")
|
|
115
|
+
for match in _LITERAL_CITATION.finditer(text):
|
|
116
|
+
# A persona with its own declared format may show an example of it.
|
|
117
|
+
if declares_own_format and name not in _META_SKILLS:
|
|
118
|
+
continue
|
|
119
|
+
line = text[: match.start()].count("\n") + 1
|
|
120
|
+
problems.append(
|
|
121
|
+
f"{name}/SKILL.md:{line}: the file claims citations are "
|
|
122
|
+
f"{claims[0]} but shows the literal template {match.group()!r}, "
|
|
123
|
+
"which carries no identifier. A skill that quotes personas must "
|
|
124
|
+
"defer to their declared `citation_format`."
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# Rule 2 — requiring citations without naming an auditable shape produces
|
|
128
|
+
# citations the auditor cannot see, which reports as `N/A`, not as a miss.
|
|
129
|
+
demand = next((p for p in _REQUIRES_CITATION if p.search(text)), None)
|
|
130
|
+
if demand is not None:
|
|
131
|
+
examined.append("demand-vs-auditable-shape")
|
|
132
|
+
if not _AUDITABLE_BLOCK.search(text):
|
|
133
|
+
line = text[: demand.search(text).start()].count("\n") + 1
|
|
134
|
+
problems.append(
|
|
135
|
+
f"{name}/SKILL.md:{line}: the file requires citations "
|
|
136
|
+
f"(matched {demand.pattern!r}) but never shows the 【…】 form. "
|
|
137
|
+
"`verify_citations._CITATION_BLOCK` parses that shape and no "
|
|
138
|
+
"other, so whatever the model writes instead is served "
|
|
139
|
+
"unaudited and reports as `audit_coverage: N/A`."
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
return problems, examined
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def main() -> int:
|
|
146
|
+
skills = sorted(p for p in PREBUILT.glob("*/SKILL.md"))
|
|
147
|
+
if not skills:
|
|
148
|
+
print("no SKILL.md found under prebuilt/ — this check would pass vacuously")
|
|
149
|
+
return 1
|
|
150
|
+
|
|
151
|
+
problems: list[str] = []
|
|
152
|
+
reach: dict[str, list[str]] = {}
|
|
153
|
+
for path in skills:
|
|
154
|
+
found, examined = check_skill(path)
|
|
155
|
+
problems += found
|
|
156
|
+
for rule in examined:
|
|
157
|
+
reach.setdefault(rule, []).append(path.parent.name)
|
|
158
|
+
|
|
159
|
+
if problems:
|
|
160
|
+
print(f"✗ {len(problems)} citation-template problem(s):\n")
|
|
161
|
+
for problem in problems:
|
|
162
|
+
print(f" - {problem}")
|
|
163
|
+
return 1
|
|
164
|
+
|
|
165
|
+
print(f"✓ citation templates ok — {len(skills)} skills scanned")
|
|
166
|
+
for rule in ("claims-vs-template", "demand-vs-auditable-shape"):
|
|
167
|
+
names = reach.get(rule, [])
|
|
168
|
+
if not names:
|
|
169
|
+
print(f" ✗ {rule}: examined 0 skills — this rule decided nothing")
|
|
170
|
+
return 1
|
|
171
|
+
print(f" {rule}: {len(names)} skill(s) — {', '.join(sorted(names))}")
|
|
172
|
+
return 0
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
if __name__ == "__main__":
|
|
176
|
+
sys.exit(main())
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Gate: a requirement may only be declared undecidable with evidence behind it.
|
|
3
|
+
|
|
4
|
+
`must_convey` is how a fixture says "the substring matcher cannot decide this" —
|
|
5
|
+
the honest answer for 方便 when the answer says 「应病与药」, or for 不是虚无 when
|
|
6
|
+
it says 「空非虚无」. It is also the easiest possible way to launder a failure:
|
|
7
|
+
move the inconvenient requirement there and the build goes green while looking
|
|
8
|
+
more rigorous than before.
|
|
9
|
+
|
|
10
|
+
So a term may sit in `must_convey` only if a committed adjudication under
|
|
11
|
+
`eval/reports/` ruled it an instrument artifact, on a quote that
|
|
12
|
+
`verify-adjudication.py` proved is still in the answer it judges. A term the
|
|
13
|
+
adjudication ruled `upheld` — a real failure — can never be moved there.
|
|
14
|
+
|
|
15
|
+
Same ratchet as `KNOWN_UNDECLARED` in validate-citation-references.py: an entry
|
|
16
|
+
records a decision that was made and shown, not a permission anyone can take.
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import sys
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
25
|
+
PREBUILT = ROOT / "prebuilt"
|
|
26
|
+
REPORTS = ROOT / "eval" / "reports"
|
|
27
|
+
|
|
28
|
+
# Verdicts that license moving a requirement out of hard grading.
|
|
29
|
+
PERMITTING = {"instrument", "fixture"}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def load_fixtures() -> dict[str, list[dict]]:
|
|
33
|
+
fixtures: dict[str, list[dict]] = {}
|
|
34
|
+
for path in sorted(PREBUILT.glob("*/tests/fidelity.jsonl")):
|
|
35
|
+
master = path.parent.parent.name
|
|
36
|
+
fixtures[master] = [
|
|
37
|
+
json.loads(line) for line in path.read_text().splitlines() if line.strip()
|
|
38
|
+
]
|
|
39
|
+
return fixtures
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def load_adjudications() -> list[dict]:
|
|
43
|
+
return [json.loads(p.read_text()) for p in sorted(REPORTS.glob("adjudication-*.json"))]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _permitted(adjudications: list[dict]) -> set[tuple[str, int, str]]:
|
|
47
|
+
"""(master, index, term) triples an adjudication ruled an instrument artifact.
|
|
48
|
+
|
|
49
|
+
Keyed by fixture index, not just (master, term) — a verdict on one fixture
|
|
50
|
+
must not license the same term string at a different, unadjudicated
|
|
51
|
+
fixture for the same master. `缘起` being paraphrased in one answer says
|
|
52
|
+
nothing about whether it is genuinely omitted in another; only the
|
|
53
|
+
specific case a verdict names has evidence behind it.
|
|
54
|
+
"""
|
|
55
|
+
allowed: set[tuple[str, int, str]] = set()
|
|
56
|
+
for adjudication in adjudications:
|
|
57
|
+
for case in adjudication.get("cases", []):
|
|
58
|
+
for verdict in case.get("mention_verdicts", []):
|
|
59
|
+
if verdict.get("verdict") in PERMITTING:
|
|
60
|
+
allowed.add((case["master"], case["index"], verdict["term"]))
|
|
61
|
+
return allowed
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def verify(fixtures: dict[str, list[dict]], adjudications: list[dict]) -> list[str]:
|
|
65
|
+
"""Return every reason a fixture declares more than the evidence supports."""
|
|
66
|
+
problems: list[str] = []
|
|
67
|
+
allowed = _permitted(adjudications)
|
|
68
|
+
|
|
69
|
+
declares_any = any(
|
|
70
|
+
case.get("must_convey") for cases in fixtures.values() for case in cases
|
|
71
|
+
)
|
|
72
|
+
if declares_any and not adjudications:
|
|
73
|
+
problems.append(
|
|
74
|
+
"no adjudication under eval/reports/ — must_convey entries exist but "
|
|
75
|
+
"there is nothing to check them against"
|
|
76
|
+
)
|
|
77
|
+
return problems
|
|
78
|
+
|
|
79
|
+
for master, cases in fixtures.items():
|
|
80
|
+
for index, case in enumerate(cases):
|
|
81
|
+
convey = case.get("must_convey") or []
|
|
82
|
+
mention = set(case.get("must_mention") or [])
|
|
83
|
+
for term in convey:
|
|
84
|
+
where = f"{master} #{index}"
|
|
85
|
+
if term in mention:
|
|
86
|
+
problems.append(
|
|
87
|
+
f"{where}: {term!r} is in both must_mention and must_convey — "
|
|
88
|
+
"a requirement is either graded or undecidable, not both"
|
|
89
|
+
)
|
|
90
|
+
if (master, index, term) not in allowed:
|
|
91
|
+
problems.append(
|
|
92
|
+
f"{where}: {term!r} is declared undecidable but no adjudication "
|
|
93
|
+
f"ruled it an instrument artifact for this specific fixture. A "
|
|
94
|
+
"verdict on a different case does not transfer. Grade it, or "
|
|
95
|
+
"adjudicate this exact case and show the evidence."
|
|
96
|
+
)
|
|
97
|
+
return problems
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def main() -> int:
|
|
101
|
+
fixtures = load_fixtures()
|
|
102
|
+
adjudications = load_adjudications()
|
|
103
|
+
problems = verify(fixtures, adjudications)
|
|
104
|
+
convey = sum(
|
|
105
|
+
len(case.get("must_convey") or [])
|
|
106
|
+
for cases in fixtures.values()
|
|
107
|
+
for case in cases
|
|
108
|
+
)
|
|
109
|
+
graded = sum(
|
|
110
|
+
len(case.get("must_mention") or [])
|
|
111
|
+
for cases in fixtures.values()
|
|
112
|
+
for case in cases
|
|
113
|
+
)
|
|
114
|
+
if problems:
|
|
115
|
+
print(f"FAIL: {len(problems)} fixture term(s) declare more than the evidence supports:")
|
|
116
|
+
for problem in problems:
|
|
117
|
+
print(f" {problem}")
|
|
118
|
+
return 1
|
|
119
|
+
print(
|
|
120
|
+
f"OK: {graded} graded mention requirements, {convey} declared undecidable, "
|
|
121
|
+
f"every one of the latter backed by an adjudicated verdict."
|
|
122
|
+
)
|
|
123
|
+
return 0
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
if __name__ == "__main__":
|
|
127
|
+
sys.exit(main())
|