master-skill 0.10.1 → 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/GEMINI.md +1 -1
- package/README.md +84 -336
- package/README_EN.md +108 -321
- package/bin/cli.mjs +249 -9
- package/gemini-extension.json +1 -1
- package/hooks/session-start +68 -74
- package/hooks/session_start.py +152 -0
- package/package.json +6 -2
- package/prebuilt/{compare → 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/SKILL.md +1 -1
- package/prebuilt/master-curriculum/references/tiantai.md +1 -1
- package/prebuilt/master-debate/SKILL.md +15 -3
- package/prebuilt/master-fazang/tests/fidelity.jsonl +2 -2
- package/prebuilt/master-help/SKILL.md +94 -0
- package/prebuilt/master-help/tests/fidelity.jsonl +10 -0
- package/prebuilt/master-huineng/tests/fidelity.jsonl +4 -4
- package/prebuilt/master-kumarajiva/meta.json +14 -3
- 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/meta.json +19 -4
- 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 +32 -5
- 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/references/teaching-modes.md +8 -1
- package/routing.json +209 -0
- package/scripts/check-audit-ignores.py +105 -0
- package/scripts/check-eval-sdk-surface.py +142 -0
- package/scripts/check-gate-liveness.py +421 -0
- 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 +992 -89
- package/scripts/validate-citation-references.py +150 -0
- package/scripts/validate-citation-templates.py +176 -0
- package/scripts/validate-fidelity.py +6 -1
- package/scripts/validate-fixture-terms.py +127 -0
- package/scripts/validate-routing.py +254 -0
- package/scripts/validate.py +63 -36
- package/scripts/verify-adjudication.py +316 -0
- package/scripts/verify_citations.py +739 -39
- package/skill-catalog.json +83 -20
- 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_manifest_versions.py +0 -217
- package/scripts/tests/test_debate_protocol.py +0 -159
- package/scripts/tests/test_injection_hardening.py +0 -174
- package/scripts/tests/test_select_fidelity_smoke.py +0 -142
- 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 -265
- /package/prebuilt/{compare → compare-masters}/tests/fidelity.jsonl +0 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Gate: an adjudication must prove it read the answers it ruled on.
|
|
3
|
+
|
|
4
|
+
The first full-coverage fidelity run (`eval/reports/0.11.0-06b8142-deepseek.json`)
|
|
5
|
+
failed 62 of 199 graded cases, and 46 of those failed on `must_mention` alone —
|
|
6
|
+
a bare substring match against 2-to-5-character Chinese terms. Reading the
|
|
7
|
+
stored answers showed most of those failures describe spelling, not behaviour:
|
|
8
|
+
`master-fazang` wrote 「五教之判」 for a fixture demanding 判教, `master-nagarjuna`
|
|
9
|
+
wrote 「空非虚无」 for one demanding 不是虚无, `master-ouyi` answered entirely in
|
|
10
|
+
traditional characters against simplified fixtures, and `master-mahasi-sayadaw`
|
|
11
|
+
wrote 「升、降」 where the fixture wanted 升降.
|
|
12
|
+
|
|
13
|
+
Those rulings are judgements, and a judgement file is exactly the artifact this
|
|
14
|
+
repo keeps catching in the act of reporting green without examining anything.
|
|
15
|
+
So every verdict carries a quote from the answer it judges, and this script
|
|
16
|
+
proves each quote is still there, that each term really failed in the run, and
|
|
17
|
+
that the headline numbers follow from the verdicts rather than being typed in.
|
|
18
|
+
|
|
19
|
+
An adjudication that cannot be re-derived from the run it judges is worth
|
|
20
|
+
nothing, and fails here.
|
|
21
|
+
"""
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import json
|
|
25
|
+
import os
|
|
26
|
+
import sys
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
30
|
+
REPORTS = ROOT / "eval" / "reports"
|
|
31
|
+
|
|
32
|
+
MENTION_VERDICTS = {"instrument", "fixture", "upheld"}
|
|
33
|
+
FORBIDDEN_VERDICTS = {"false_failure", "upheld"}
|
|
34
|
+
# `open_question` records a finding without deciding it — the pressure fixtures
|
|
35
|
+
# name one text while the citation contract asks only for a declared source, and
|
|
36
|
+
# which of those `pressure` means is a maintainer's call, not the adjudicator's.
|
|
37
|
+
# It never overturns anything.
|
|
38
|
+
CITE_VERDICTS = {"instrument", "upheld", "open_question"}
|
|
39
|
+
REVIEW_VERDICTS = {"cleared", "cleared_manual", "violation"}
|
|
40
|
+
FAIL_KEYS = (
|
|
41
|
+
"missing_cites",
|
|
42
|
+
"missing_mentions",
|
|
43
|
+
"forbidden_found",
|
|
44
|
+
"boundary_violations",
|
|
45
|
+
"fabricated_cites",
|
|
46
|
+
)
|
|
47
|
+
# recount()'s `overturns` set only ever gains "missing_mentions" / "forbidden_found"
|
|
48
|
+
# / "missing_cites" — there is no case-verdict category for `boundary_violations`
|
|
49
|
+
# or `fabricated_cites`, so a case failing solely on one of those can never be
|
|
50
|
+
# overturned by this gate. That is deliberate, not a gap: `fabricated_cites` is
|
|
51
|
+
# resolved through validate-citation-references.py's KNOWN_UNDECLARED ratchet
|
|
52
|
+
# (declare the source in meta.json, then re-grade — see master-tsongkhapa #2/#3/#9,
|
|
53
|
+
# permanently listed in `failures_not_ruled_on` below until that happens) rather
|
|
54
|
+
# than through a per-term verdict here. `boundary_violations` (first-turn honorific
|
|
55
|
+
# checks) has no live case to adjudicate yet; if one ever needs a verdict, it needs
|
|
56
|
+
# its own case-verdict field and evidence rule, the same way mention/forbidden/cite
|
|
57
|
+
# each got one — not a silent addition to this permitting list.
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _derive_case_verdict(verdicts: list[dict], permitting: set[str]) -> str | None:
|
|
61
|
+
"""Recompute what a case-level `*_case_verdict` should be from its own
|
|
62
|
+
per-term verdicts — the same rule `build_verdicts.py` used to write it.
|
|
63
|
+
None if there were no verdicts to summarize (the field should be absent).
|
|
64
|
+
"""
|
|
65
|
+
if not verdicts:
|
|
66
|
+
return None
|
|
67
|
+
rulings = {v["verdict"] for v in verdicts}
|
|
68
|
+
return "overturned" if rulings and rulings <= permitting else "upheld"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _index_report(report: dict) -> dict:
|
|
72
|
+
return {
|
|
73
|
+
(suite["master"], result["index"]): result
|
|
74
|
+
for suite in report["suites"]
|
|
75
|
+
for result in suite["results"]
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def recount(adjudication: dict, report: dict) -> dict:
|
|
80
|
+
"""Recompute the per-test-type tally from the verdicts alone.
|
|
81
|
+
|
|
82
|
+
A case turns from FAIL to PASS only when *every* check it failed was
|
|
83
|
+
overturned; a `violation` review turns a PASS into a FAIL. Adjudication
|
|
84
|
+
that can only move a number upward is advocacy, not judgement.
|
|
85
|
+
"""
|
|
86
|
+
overturns: dict[tuple[str, int], set[str]] = {}
|
|
87
|
+
violations: set[tuple[str, int]] = set()
|
|
88
|
+
for case in adjudication["cases"]:
|
|
89
|
+
key = (case["master"], case["index"])
|
|
90
|
+
keys = set()
|
|
91
|
+
if case.get("mention_case_verdict") == "overturned":
|
|
92
|
+
keys.add("missing_mentions")
|
|
93
|
+
if case.get("forbidden_case_verdict") == "overturned":
|
|
94
|
+
keys.add("forbidden_found")
|
|
95
|
+
if case.get("cite_case_verdict") == "overturned":
|
|
96
|
+
keys.add("missing_cites")
|
|
97
|
+
overturns[key] = keys
|
|
98
|
+
if case.get("review_verdict") == "violation":
|
|
99
|
+
violations.add(key)
|
|
100
|
+
|
|
101
|
+
tally: dict[str, dict[str, int]] = {}
|
|
102
|
+
for suite in report["suites"]:
|
|
103
|
+
for result in suite["results"]:
|
|
104
|
+
if result.get("status") == "truncated":
|
|
105
|
+
continue
|
|
106
|
+
key = (suite["master"], result["index"])
|
|
107
|
+
bucket = tally.setdefault(
|
|
108
|
+
result["test_type"], {"graded": 0, "passed": 0, "adjudicated": 0}
|
|
109
|
+
)
|
|
110
|
+
bucket["graded"] += 1
|
|
111
|
+
passed = result["status"] == "PASS"
|
|
112
|
+
if passed:
|
|
113
|
+
bucket["passed"] += 1
|
|
114
|
+
failed = {k for k in FAIL_KEYS if result.get(k)}
|
|
115
|
+
if not passed and failed and failed <= overturns.get(key, set()):
|
|
116
|
+
passed = True
|
|
117
|
+
if passed and key in violations:
|
|
118
|
+
passed = False
|
|
119
|
+
if passed:
|
|
120
|
+
bucket["adjudicated"] += 1
|
|
121
|
+
return tally
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def verify(adjudication: dict, report: dict) -> list[str]:
|
|
125
|
+
"""Return every reason this adjudication cannot be trusted. Empty is good."""
|
|
126
|
+
problems: list[str] = []
|
|
127
|
+
cases = adjudication.get("cases") or []
|
|
128
|
+
if not cases:
|
|
129
|
+
problems.append("no cases: an adjudication that ruled on nothing is not a result")
|
|
130
|
+
return problems
|
|
131
|
+
|
|
132
|
+
results = _index_report(report)
|
|
133
|
+
|
|
134
|
+
for case in cases:
|
|
135
|
+
key = (case["master"], case["index"])
|
|
136
|
+
where = f"{case['master']} #{case['index']}"
|
|
137
|
+
result = results.get(key)
|
|
138
|
+
if result is None:
|
|
139
|
+
problems.append(f"{where}: no such case in the run being adjudicated")
|
|
140
|
+
continue
|
|
141
|
+
response = result.get("response") or ""
|
|
142
|
+
|
|
143
|
+
# A case-level `*_case_verdict` is a summary of its own per-term
|
|
144
|
+
# verdicts, not an independent claim. recount() trusts it wholesale to
|
|
145
|
+
# decide whether a FAIL becomes a PASS, so it has to be re-derivable
|
|
146
|
+
# from the terms it summarizes — otherwise editing one field, with no
|
|
147
|
+
# evidence and no per-term change, silently overturns a real failure.
|
|
148
|
+
for field, verdicts_key, permitting in (
|
|
149
|
+
("mention_case_verdict", "mention_verdicts", {"instrument", "fixture"}),
|
|
150
|
+
("forbidden_case_verdict", "forbidden_verdicts", {"false_failure"}),
|
|
151
|
+
("cite_case_verdict", "cite_verdicts", {"instrument"}),
|
|
152
|
+
):
|
|
153
|
+
stored = case.get(field)
|
|
154
|
+
expected = _derive_case_verdict(case.get(verdicts_key) or [], permitting)
|
|
155
|
+
if stored != expected:
|
|
156
|
+
problems.append(
|
|
157
|
+
f"{where}: {field} is {stored!r} but its {verdicts_key} imply "
|
|
158
|
+
f"{expected!r} — a case verdict must follow from its own terms"
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
for verdict in case.get("mention_verdicts", []):
|
|
162
|
+
term, ruling = verdict["term"], verdict["verdict"]
|
|
163
|
+
if ruling not in MENTION_VERDICTS:
|
|
164
|
+
problems.append(f"{where}: unknown mention verdict {ruling!r}")
|
|
165
|
+
if term not in (result.get("missing_mentions") or []):
|
|
166
|
+
problems.append(
|
|
167
|
+
f"{where}: ruled on {term!r}, which is not a missing_mention in the run"
|
|
168
|
+
)
|
|
169
|
+
evidence = verdict.get("evidence") or ""
|
|
170
|
+
if ruling == "instrument" and not evidence:
|
|
171
|
+
problems.append(
|
|
172
|
+
f"{where}: {term!r} ruled an instrument artifact with no evidence quote"
|
|
173
|
+
)
|
|
174
|
+
if evidence and evidence not in response:
|
|
175
|
+
problems.append(
|
|
176
|
+
f"{where}: {term!r} evidence not present in the stored answer: {evidence!r}"
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
for verdict in case.get("cite_verdicts", []):
|
|
180
|
+
citation, ruling = verdict["citation"], verdict["verdict"]
|
|
181
|
+
if ruling not in CITE_VERDICTS:
|
|
182
|
+
problems.append(f"{where}: unknown citation verdict {ruling!r}")
|
|
183
|
+
if citation not in (result.get("missing_cites") or []):
|
|
184
|
+
problems.append(
|
|
185
|
+
f"{where}: ruled on citation {citation!r}, which the run did not miss"
|
|
186
|
+
)
|
|
187
|
+
evidence = verdict.get("evidence") or ""
|
|
188
|
+
if not evidence:
|
|
189
|
+
problems.append(f"{where}: citation {citation!r} ruled with no evidence quote")
|
|
190
|
+
elif evidence not in response:
|
|
191
|
+
problems.append(
|
|
192
|
+
f"{where}: citation {citation!r} evidence not present in the stored answer"
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
for verdict in case.get("forbidden_verdicts", []):
|
|
196
|
+
term, ruling = verdict["term"], verdict["verdict"]
|
|
197
|
+
if ruling not in FORBIDDEN_VERDICTS:
|
|
198
|
+
problems.append(f"{where}: unknown forbidden verdict {ruling!r}")
|
|
199
|
+
if term not in (result.get("forbidden_found") or []):
|
|
200
|
+
problems.append(
|
|
201
|
+
f"{where}: ruled on forbidden {term!r}, which the run did not flag"
|
|
202
|
+
)
|
|
203
|
+
evidence = verdict.get("evidence") or ""
|
|
204
|
+
if not evidence:
|
|
205
|
+
problems.append(f"{where}: forbidden {term!r} ruled with no evidence quote")
|
|
206
|
+
elif evidence not in response:
|
|
207
|
+
problems.append(
|
|
208
|
+
f"{where}: forbidden {term!r} evidence not present in the stored answer"
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
ruling = case.get("review_verdict")
|
|
212
|
+
if ruling is not None:
|
|
213
|
+
if ruling not in REVIEW_VERDICTS:
|
|
214
|
+
problems.append(f"{where}: unknown review verdict {ruling!r}")
|
|
215
|
+
if not result.get("needs_review"):
|
|
216
|
+
problems.append(
|
|
217
|
+
f"{where}: adjudicated a review the run never raised (needs_review is false)"
|
|
218
|
+
)
|
|
219
|
+
evidence = case.get("review_evidence") or ""
|
|
220
|
+
if not evidence:
|
|
221
|
+
problems.append(f"{where}: review ruled with no evidence quote")
|
|
222
|
+
elif evidence not in response:
|
|
223
|
+
problems.append(
|
|
224
|
+
f"{where}: review evidence not present in the stored answer: {evidence!r}"
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
ruled = {(c["master"], c["index"]) for c in adjudication["cases"]}
|
|
228
|
+
unruled = []
|
|
229
|
+
for suite in report["suites"]:
|
|
230
|
+
for result in suite["results"]:
|
|
231
|
+
if result.get("status") == "truncated" or result["status"] == "PASS":
|
|
232
|
+
continue
|
|
233
|
+
if (suite["master"], result["index"]) not in ruled:
|
|
234
|
+
unruled.append(f"{suite['master']} #{result['index']}")
|
|
235
|
+
claimed_unruled = adjudication["summary"].get("failures_not_ruled_on")
|
|
236
|
+
if claimed_unruled is None:
|
|
237
|
+
problems.append(
|
|
238
|
+
"summary omits failures_not_ruled_on: an adjudication has to say how "
|
|
239
|
+
"much of the run it examined"
|
|
240
|
+
)
|
|
241
|
+
elif sorted(claimed_unruled) != sorted(unruled):
|
|
242
|
+
problems.append(
|
|
243
|
+
f"summary misstates its own coverage: {len(unruled)} unruled failures "
|
|
244
|
+
f"in the run, {len(claimed_unruled)} declared"
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
recomputed = recount(adjudication, report)
|
|
248
|
+
claimed = adjudication["summary"].get("by_test_type", {})
|
|
249
|
+
if recomputed != claimed:
|
|
250
|
+
problems.append(
|
|
251
|
+
"summary does not follow from the verdicts: "
|
|
252
|
+
f"recomputed {recomputed} vs recorded {claimed}"
|
|
253
|
+
)
|
|
254
|
+
return problems
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def main() -> int:
|
|
258
|
+
found = sorted(REPORTS.glob("adjudication-*.json"))
|
|
259
|
+
if not found:
|
|
260
|
+
# An adjudication that has been deleted verifies perfectly. This gate
|
|
261
|
+
# exists to keep a hand-ruling honest, so "there are none" has to be a
|
|
262
|
+
# deliberate state rather than the quiet result of removing the file
|
|
263
|
+
# that was inconvenient. Set ADJUDICATION_NONE_EXPECTED=1 to declare a
|
|
264
|
+
# tree that genuinely has not adjudicated anything yet.
|
|
265
|
+
if os.environ.get("ADJUDICATION_NONE_EXPECTED") == "1":
|
|
266
|
+
print("No adjudication files under eval/reports/ — declared expected.")
|
|
267
|
+
return 0
|
|
268
|
+
graded = sorted(
|
|
269
|
+
path for path in REPORTS.glob("*.json")
|
|
270
|
+
if not path.name.startswith("adjudication-")
|
|
271
|
+
)
|
|
272
|
+
if graded:
|
|
273
|
+
print(
|
|
274
|
+
"FAIL: eval/reports/ holds graded runs "
|
|
275
|
+
f"({', '.join(p.name for p in graded)}) but no adjudication of any "
|
|
276
|
+
"of them. Verifying nothing is not the same as verifying clean; "
|
|
277
|
+
"set ADJUDICATION_NONE_EXPECTED=1 if that is really the state."
|
|
278
|
+
)
|
|
279
|
+
return 1
|
|
280
|
+
print("No adjudication files under eval/reports/ — nothing to verify.")
|
|
281
|
+
return 0
|
|
282
|
+
|
|
283
|
+
failed = False
|
|
284
|
+
for path in found:
|
|
285
|
+
adjudication = json.loads(path.read_text())
|
|
286
|
+
report = ROOT / adjudication["summary"]["report"]
|
|
287
|
+
if not report.exists():
|
|
288
|
+
print(f"FAIL: {path.name} judges {report}, which is not in the repo")
|
|
289
|
+
failed = True
|
|
290
|
+
continue
|
|
291
|
+
problems = verify(adjudication, json.loads(report.read_text()))
|
|
292
|
+
cases = len(adjudication["cases"])
|
|
293
|
+
|
|
294
|
+
# The summary states how many cases were ruled on; nothing was checking
|
|
295
|
+
# it against how many are actually here. Dropping the awkward ones left
|
|
296
|
+
# the file self-inconsistent and the gate still printing OK — with a
|
|
297
|
+
# smaller number, in the same sentence that says everything is backed.
|
|
298
|
+
claimed = adjudication.get("summary", {}).get("adjudicated_cases")
|
|
299
|
+
if isinstance(claimed, int) and claimed != cases:
|
|
300
|
+
problems.append(
|
|
301
|
+
f"summary.adjudicated_cases says {claimed} but the file carries "
|
|
302
|
+
f"{cases} — cases were added or removed without the summary "
|
|
303
|
+
"following"
|
|
304
|
+
)
|
|
305
|
+
if problems:
|
|
306
|
+
failed = True
|
|
307
|
+
print(f"FAIL: {path.name} ({cases} cases)")
|
|
308
|
+
for problem in problems:
|
|
309
|
+
print(f" {problem}")
|
|
310
|
+
else:
|
|
311
|
+
print(f"OK: {path.name} — {cases} cases, every verdict backed by the answer text")
|
|
312
|
+
return 1 if failed else 0
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
if __name__ == "__main__":
|
|
316
|
+
sys.exit(main())
|