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
|
@@ -25,12 +25,15 @@ Usage:
|
|
|
25
25
|
from __future__ import annotations
|
|
26
26
|
|
|
27
27
|
import argparse
|
|
28
|
+
import functools
|
|
28
29
|
import json
|
|
29
30
|
import re
|
|
30
31
|
import subprocess
|
|
31
32
|
import sys
|
|
32
33
|
from pathlib import Path
|
|
33
34
|
|
|
35
|
+
import yaml
|
|
36
|
+
|
|
34
37
|
# A verdict — as opposed to a skip, an error, or a dry run.
|
|
35
38
|
GRADED_STATUSES = {"PASS", "FAIL"}
|
|
36
39
|
|
|
@@ -133,6 +136,116 @@ def check_every_skill_has_fixtures(prebuilt_dir: Path) -> list[str]:
|
|
|
133
136
|
return problems
|
|
134
137
|
|
|
135
138
|
|
|
139
|
+
# A gate is "advisory" when it can exit 0 without doing the work its name
|
|
140
|
+
# promises — the fidelity smoke passing in 10s because no API key is set. That
|
|
141
|
+
# is a legitimate project decision (CONTRIBUTING.md §2: grading is a local /
|
|
142
|
+
# pre-release step, not a CI expense). What is NOT legitimate is it being
|
|
143
|
+
# invisible: a required check's green tick looks identical either way.
|
|
144
|
+
#
|
|
145
|
+
# So each advisory gate must be declared here, saying what it does not check.
|
|
146
|
+
# An undeclared one fails this script, and the declared roster is printed on
|
|
147
|
+
# every run — `npm test` always answers "what did the green tick examine?".
|
|
148
|
+
#
|
|
149
|
+
# Detection is automatic only for the "missing secret -> exit 0" shape, which
|
|
150
|
+
# is the one this repo shipped. A job that goes quiet for some other reason —
|
|
151
|
+
# `Dependency review` skipping because the dependency graph is off — has to be
|
|
152
|
+
# added by hand. `check_declared_gates_still_exist` then keeps the entry from
|
|
153
|
+
# outliving the job, but nothing can force a new *shape* to be noticed. If you
|
|
154
|
+
# add a gate that can pass without working, put it here yourself.
|
|
155
|
+
ADVISORY_GATES = {
|
|
156
|
+
"Fidelity smoke (1 master × 1 fixture)": (
|
|
157
|
+
"grades nothing when ANTHROPIC_API_KEY is unset (it always has been) — "
|
|
158
|
+
"the green tick means structure validation passed, not that a model "
|
|
159
|
+
"response was graded. Set repo variable FIDELITY_GRADING_REQUIRED=true "
|
|
160
|
+
"once the secret exists to make the skip a hard failure."
|
|
161
|
+
),
|
|
162
|
+
"Fidelity tests — full suite (weekly + manual)": (
|
|
163
|
+
"same skip as the smoke, on the weekly cron"
|
|
164
|
+
),
|
|
165
|
+
"Persona-fidelity schema + advisory eval": (
|
|
166
|
+
"llm-rubric eval is `|| true` and is skipped entirely without a key; "
|
|
167
|
+
"only the promptfoo schema + repo-convention validation is real"
|
|
168
|
+
),
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
# The shape of a silent skip: a step that exits 0 because a secret is missing.
|
|
172
|
+
_SKIP_ON_MISSING_SECRET = re.compile(r'\[\s+-z\s+"\$\{[A-Z_]+:-\}"\s+\]')
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _job_display_name(job_id: str, job: dict) -> str:
|
|
176
|
+
"""The name this file can see for a job.
|
|
177
|
+
|
|
178
|
+
NOT, in general, "the string branch protection matches on" — an earlier
|
|
179
|
+
version of this docstring said that and it is false for any matrix job.
|
|
180
|
+
GitHub expands `name: CodeQL (${{ matrix.language }})` into one check run
|
|
181
|
+
per leg (`CodeQL (python)`, …); statically all that is visible here is the
|
|
182
|
+
unexpanded template. So a matrix job can never be matched by an
|
|
183
|
+
ADVISORY_GATES key, and this module cannot police one. The roster covers
|
|
184
|
+
the non-matrix jobs, which are the ones this repo has actually shipped a
|
|
185
|
+
silent skip in; a matrix job that grows one has to be caught by review.
|
|
186
|
+
"""
|
|
187
|
+
name = job.get("name") if isinstance(job, dict) else None
|
|
188
|
+
return str(name) if name else job_id
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _job_skips_on_missing_secret(job: dict) -> bool:
|
|
192
|
+
steps = job.get("steps") or [] if isinstance(job, dict) else []
|
|
193
|
+
for step in steps:
|
|
194
|
+
if not isinstance(step, dict):
|
|
195
|
+
continue
|
|
196
|
+
run = step.get("run")
|
|
197
|
+
if isinstance(run, str) and _SKIP_ON_MISSING_SECRET.search(run):
|
|
198
|
+
return True
|
|
199
|
+
return False
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _iter_jobs(workflow_docs: dict[str, dict]):
|
|
203
|
+
"""Yield (path, display_name, job) for every job in every workflow.
|
|
204
|
+
|
|
205
|
+
Per **job**, not per file: `validate-and-test.yml` holds six jobs, and a
|
|
206
|
+
file-level check would let a newly-silent seventh hide behind its declared
|
|
207
|
+
siblings.
|
|
208
|
+
"""
|
|
209
|
+
for path, doc in sorted(workflow_docs.items()):
|
|
210
|
+
jobs = (doc or {}).get("jobs") or {}
|
|
211
|
+
if not isinstance(jobs, dict):
|
|
212
|
+
continue
|
|
213
|
+
for job_id, job in jobs.items():
|
|
214
|
+
if isinstance(job, dict):
|
|
215
|
+
yield path, _job_display_name(job_id, job), job
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def check_advisory_gates_declared(workflow_docs: dict[str, dict]) -> list[str]:
|
|
219
|
+
"""Every job that can exit 0 on a missing secret must be declared above.
|
|
220
|
+
|
|
221
|
+
Catches the case this repo actually shipped: a *required* branch-protection
|
|
222
|
+
check that has never once graded a response, with nothing in the repo
|
|
223
|
+
saying so.
|
|
224
|
+
"""
|
|
225
|
+
return [
|
|
226
|
+
f"{path}: job {name!r} exits 0 when a secret is missing but is not in "
|
|
227
|
+
"ADVISORY_GATES — a gate that can pass without checking anything must "
|
|
228
|
+
"say so, or stop doing it"
|
|
229
|
+
for path, name, job in _iter_jobs(workflow_docs)
|
|
230
|
+
if _job_skips_on_missing_secret(job) and name not in ADVISORY_GATES
|
|
231
|
+
]
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def check_declared_gates_still_exist(workflow_docs: dict[str, dict]) -> list[str]:
|
|
235
|
+
"""The reverse drift: a declaration outliving the job it describes.
|
|
236
|
+
|
|
237
|
+
A stale entry is worse than none — it asserts a caveat about a gate that no
|
|
238
|
+
longer exists, and hides the day a real gate quietly becomes advisory.
|
|
239
|
+
"""
|
|
240
|
+
live = {name for _, name, _ in _iter_jobs(workflow_docs)}
|
|
241
|
+
return [
|
|
242
|
+
f"ADVISORY_GATES declares {name!r}, but no workflow job has that name "
|
|
243
|
+
"— stale declaration, or the job was renamed"
|
|
244
|
+
for name in sorted(ADVISORY_GATES)
|
|
245
|
+
if name not in live
|
|
246
|
+
]
|
|
247
|
+
|
|
248
|
+
|
|
136
249
|
# ---------------------------------------------------------------------------
|
|
137
250
|
# Repo-level wiring
|
|
138
251
|
# ---------------------------------------------------------------------------
|
|
@@ -164,8 +277,17 @@ def read_testpaths(root: Path) -> list[str]:
|
|
|
164
277
|
return []
|
|
165
278
|
|
|
166
279
|
|
|
167
|
-
|
|
168
|
-
|
|
280
|
+
@functools.lru_cache(maxsize=None)
|
|
281
|
+
def _collect_counts_cached(root: str) -> tuple[tuple[str, int], ...]:
|
|
282
|
+
"""One `pytest --collect-only` per process, not per caller.
|
|
283
|
+
|
|
284
|
+
The CLI calls `run_all` once and never noticed. The test suite calls it six
|
|
285
|
+
times, and each call forked a full collection of the whole suite — measured
|
|
286
|
+
1.11s each, ~7s of a 20s `npm test`. Worse, it grew with the suite: every
|
|
287
|
+
test added made those six slower, so the cost compounded exactly as the
|
|
288
|
+
project got more tests. Cached on the resolved root; the collection cannot
|
|
289
|
+
change within a process.
|
|
290
|
+
"""
|
|
169
291
|
proc = subprocess.run(
|
|
170
292
|
[sys.executable, "-m", "pytest", "--collect-only", "-q"],
|
|
171
293
|
cwd=root, capture_output=True, text=True,
|
|
@@ -175,10 +297,46 @@ def collect_counts(root: Path) -> dict[str, int]:
|
|
|
175
297
|
match = re.match(r"^([\w./-]+\.py)::", line.strip())
|
|
176
298
|
if match:
|
|
177
299
|
counts[match.group(1)] = counts.get(match.group(1), 0) + 1
|
|
178
|
-
return counts
|
|
300
|
+
return tuple(sorted(counts.items()))
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def collect_counts(root: Path) -> dict[str, int]:
|
|
304
|
+
"""Ask pytest what it actually collects, per file."""
|
|
305
|
+
return dict(_collect_counts_cached(str(Path(root).resolve())))
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def read_workflows(root: Path) -> dict[str, dict]:
|
|
309
|
+
wf_dir = root / ".github" / "workflows"
|
|
310
|
+
if not wf_dir.is_dir():
|
|
311
|
+
return {}
|
|
312
|
+
return {
|
|
313
|
+
str(p.relative_to(root)): yaml.safe_load(p.read_text(encoding="utf-8")) or {}
|
|
314
|
+
for p in sorted(wf_dir.glob("*.yml"))
|
|
315
|
+
}
|
|
316
|
+
|
|
179
317
|
|
|
318
|
+
def _declares_a_skip(report: Path) -> bool:
|
|
319
|
+
"""Whether the report says outright that it graded nothing on purpose."""
|
|
320
|
+
data = json.loads(report.read_text(encoding="utf-8"))
|
|
321
|
+
return isinstance(data, dict) and bool(data.get("skipped"))
|
|
180
322
|
|
|
181
|
-
|
|
323
|
+
|
|
324
|
+
def load_fidelity_suites(report: Path) -> list[dict]:
|
|
325
|
+
"""Read a `test-fidelity.py --json` report into a suite list.
|
|
326
|
+
|
|
327
|
+
A declared skip (`{"skipped": true, "reason": "no_api_key"}`) is not a
|
|
328
|
+
suite — it is the advisory path, already accounted for by ADVISORY_GATES.
|
|
329
|
+
Anything else claiming to be a run gets checked for actual verdicts.
|
|
330
|
+
"""
|
|
331
|
+
data = json.loads(report.read_text(encoding="utf-8"))
|
|
332
|
+
if isinstance(data, dict):
|
|
333
|
+
if data.get("skipped"):
|
|
334
|
+
return []
|
|
335
|
+
data = [data]
|
|
336
|
+
return [s for s in data if isinstance(s, dict) and not s.get("skipped")]
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def run_all(root: Path, fidelity_report: Path | None = None) -> list[str]:
|
|
182
340
|
problems: list[str] = []
|
|
183
341
|
|
|
184
342
|
test_files = discover_test_files(root)
|
|
@@ -194,6 +352,32 @@ def run_all(root: Path) -> list[str]:
|
|
|
194
352
|
problems += check_catalog_matches_filesystem(catalog, root / "prebuilt", root)
|
|
195
353
|
|
|
196
354
|
problems += check_every_skill_has_fixtures(root / "prebuilt")
|
|
355
|
+
|
|
356
|
+
workflows = read_workflows(root)
|
|
357
|
+
problems += check_advisory_gates_declared(workflows)
|
|
358
|
+
problems += check_declared_gates_still_exist(workflows)
|
|
359
|
+
|
|
360
|
+
# check_graded_suites_graded_something shipped fully written and unit-tested
|
|
361
|
+
# but unreferenced by run_all — the anti-fake-green script had a check that
|
|
362
|
+
# itself never ran. This is where it runs.
|
|
363
|
+
if fidelity_report is not None:
|
|
364
|
+
# A missing file is a problem, not a pass. `… and fidelity_report.exists()`
|
|
365
|
+
# meant `--fidelity-report /nonexistent.json` printed "every gate examined
|
|
366
|
+
# a non-empty set" about a report it never opened — the exact statement
|
|
367
|
+
# this script exists to make impossible.
|
|
368
|
+
if not fidelity_report.exists():
|
|
369
|
+
problems.append(
|
|
370
|
+
f"{fidelity_report} was named as the fidelity report but does "
|
|
371
|
+
"not exist — nothing was examined"
|
|
372
|
+
)
|
|
373
|
+
else:
|
|
374
|
+
suites = load_fidelity_suites(fidelity_report)
|
|
375
|
+
if not suites and not _declares_a_skip(fidelity_report):
|
|
376
|
+
problems.append(
|
|
377
|
+
f"{fidelity_report} contains no suites and does not declare "
|
|
378
|
+
"a skip — it grades nothing but reads as clean"
|
|
379
|
+
)
|
|
380
|
+
problems += check_graded_suites_graded_something(suites)
|
|
197
381
|
return problems
|
|
198
382
|
|
|
199
383
|
|
|
@@ -201,12 +385,21 @@ def main() -> int:
|
|
|
201
385
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
202
386
|
parser.add_argument("--root", type=Path, default=Path(__file__).resolve().parent.parent)
|
|
203
387
|
parser.add_argument("--json", action="store_true", help="machine-readable output")
|
|
388
|
+
parser.add_argument(
|
|
389
|
+
"--fidelity-report",
|
|
390
|
+
type=Path,
|
|
391
|
+
default=None,
|
|
392
|
+
help="a test-fidelity.py --json report; assert it produced real verdicts",
|
|
393
|
+
)
|
|
204
394
|
args = parser.parse_args()
|
|
205
395
|
|
|
206
|
-
problems = run_all(args.root)
|
|
396
|
+
problems = run_all(args.root, args.fidelity_report)
|
|
207
397
|
|
|
208
398
|
if args.json:
|
|
209
|
-
print(json.dumps(
|
|
399
|
+
print(json.dumps(
|
|
400
|
+
{"problems": problems, "ok": not problems, "advisory_gates": ADVISORY_GATES},
|
|
401
|
+
ensure_ascii=False, indent=2,
|
|
402
|
+
))
|
|
210
403
|
elif problems:
|
|
211
404
|
print(f"✗ {len(problems)} gate-liveness problem(s):\n")
|
|
212
405
|
for p in problems:
|
|
@@ -214,6 +407,12 @@ def main() -> int:
|
|
|
214
407
|
print("\nA gate that examines nothing reports the same green as one that passes.")
|
|
215
408
|
else:
|
|
216
409
|
print("✓ gate liveness ok — every gate examined a non-empty set")
|
|
410
|
+
# Printed on success, not just failure: the roster is the answer to
|
|
411
|
+
# "what did that green tick actually examine?", and it is only useful
|
|
412
|
+
# if you see it without going looking.
|
|
413
|
+
print(f"\n⚠ {len(ADVISORY_GATES)} advisory gate(s) — green does NOT mean these ran:")
|
|
414
|
+
for name, caveat in sorted(ADVISORY_GATES.items()):
|
|
415
|
+
print(f" - {name}\n {caveat}")
|
|
217
416
|
|
|
218
417
|
return 1 if problems else 0
|
|
219
418
|
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Re-run the citation audit over a committed run's stored answers. Offline, free.
|
|
3
|
+
|
|
4
|
+
The fabrication audit is a mechanical string-resolution test — does a citation
|
|
5
|
+
resolve to a declared source — so every change to `verify_citations.py` changes
|
|
6
|
+
what an already-paid-for run would have seen. Coverage is otherwise frozen at run
|
|
7
|
+
time: `eval/reports/0.11.0-06b8142-deepseek.json` records `master-ajahn-chah` at
|
|
8
|
+
0 of 48 citations readable, and that number describes the auditor of 2026-08-31,
|
|
9
|
+
not the persona.
|
|
10
|
+
|
|
11
|
+
Since PR #142 every answer is stored, so the audit can simply be run again.
|
|
12
|
+
The ¥3.89 sweep is re-measurable for nothing, and a family added to the auditor
|
|
13
|
+
has to show what it bought instead of asserting it.
|
|
14
|
+
|
|
15
|
+
python3 scripts/reaudit-report.py eval/reports/0.11.0-06b8142-deepseek.json
|
|
16
|
+
|
|
17
|
+
The report file itself is never rewritten: it is the record of what that run
|
|
18
|
+
measured with that instrument, and editing it would be rewriting the experiment.
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import json
|
|
23
|
+
import sys
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
27
|
+
from verify_citations import ( # noqa: E402
|
|
28
|
+
audit_answer,
|
|
29
|
+
load_declared_ids,
|
|
30
|
+
load_member_aliases,
|
|
31
|
+
load_title_aliases,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def reaudit(report: dict) -> dict:
|
|
36
|
+
"""Re-audit every stored answer. Raises if the run stored none."""
|
|
37
|
+
if not any(
|
|
38
|
+
"response" in result
|
|
39
|
+
for suite in report["suites"]
|
|
40
|
+
for result in suite["results"]
|
|
41
|
+
):
|
|
42
|
+
raise ValueError(
|
|
43
|
+
"no stored answers in this report — nothing to re-audit. Runs before "
|
|
44
|
+
"PR #142 kept only response_length."
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
suites = []
|
|
48
|
+
totals = {
|
|
49
|
+
"recorded": {"checked": 0, "unparsed": 0},
|
|
50
|
+
"recomputed": {"checked": 0, "unparsed": 0},
|
|
51
|
+
}
|
|
52
|
+
for suite in report["suites"]:
|
|
53
|
+
recorded_block = suite.get("audit") or {}
|
|
54
|
+
recorded = {
|
|
55
|
+
"checked": recorded_block.get("citations_checked", 0),
|
|
56
|
+
"unparsed": recorded_block.get("citations_unparsed", 0),
|
|
57
|
+
}
|
|
58
|
+
totals["recorded"]["checked"] += recorded["checked"]
|
|
59
|
+
totals["recorded"]["unparsed"] += recorded["unparsed"]
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
declared = load_declared_ids(suite["master"])
|
|
63
|
+
aliases = load_member_aliases(suite["master"])
|
|
64
|
+
titles = load_title_aliases(suite["master"])
|
|
65
|
+
except (FileNotFoundError, ValueError):
|
|
66
|
+
# No meta.json, so no declared set to audit against. Recorded as
|
|
67
|
+
# unavailable rather than as a clean zero — the distinction this
|
|
68
|
+
# repo lost once already.
|
|
69
|
+
suites.append(
|
|
70
|
+
{
|
|
71
|
+
"master": suite["master"],
|
|
72
|
+
"status": "unavailable",
|
|
73
|
+
"recorded": recorded,
|
|
74
|
+
"recomputed": None,
|
|
75
|
+
"fabricated": [],
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
totals["recomputed"]["checked"] += recorded["checked"]
|
|
79
|
+
totals["recomputed"]["unparsed"] += recorded["unparsed"]
|
|
80
|
+
continue
|
|
81
|
+
|
|
82
|
+
checked = unparsed = 0
|
|
83
|
+
fabricated: list[str] = []
|
|
84
|
+
noncitation: list[str] = []
|
|
85
|
+
for result in suite["results"]:
|
|
86
|
+
if result.get("status") in ("truncated", "api_error"):
|
|
87
|
+
continue
|
|
88
|
+
audit = audit_answer(
|
|
89
|
+
declared, result.get("response") or "", aliases, titles
|
|
90
|
+
)
|
|
91
|
+
checked += (
|
|
92
|
+
len(audit["offline"]) + len(audit["live"]) + len(audit["fabricated"])
|
|
93
|
+
)
|
|
94
|
+
unparsed += len(audit["unparsed"])
|
|
95
|
+
fabricated.extend(audit["fabricated"])
|
|
96
|
+
noncitation.extend(audit.get("noncitation", ()))
|
|
97
|
+
recomputed = {"checked": checked, "unparsed": unparsed}
|
|
98
|
+
totals["recomputed"]["checked"] += checked
|
|
99
|
+
totals["recomputed"]["unparsed"] += unparsed
|
|
100
|
+
suites.append(
|
|
101
|
+
{
|
|
102
|
+
"master": suite["master"],
|
|
103
|
+
"status": "audited",
|
|
104
|
+
"recorded": recorded,
|
|
105
|
+
"recomputed": recomputed,
|
|
106
|
+
"fabricated": sorted(set(fabricated)),
|
|
107
|
+
# 判定为「不是引文」的【…】块。不计入覆盖率的分母,但必须
|
|
108
|
+
# 数出来:排除而不申报,和静默跳过没有区别。
|
|
109
|
+
"noncitation": sorted(set(noncitation)),
|
|
110
|
+
}
|
|
111
|
+
)
|
|
112
|
+
return {"meta": report.get("meta", {}), "suites": suites, "totals": totals}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _coverage(counts: dict | None) -> str:
|
|
116
|
+
if not counts:
|
|
117
|
+
return "n/a"
|
|
118
|
+
total = counts["checked"] + counts["unparsed"]
|
|
119
|
+
if not total:
|
|
120
|
+
return "n/a"
|
|
121
|
+
return f"{counts['checked']}/{total} {counts['checked'] / total:.0%}"
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def main(argv: list[str]) -> int:
|
|
125
|
+
if len(argv) != 2:
|
|
126
|
+
print(__doc__.strip().splitlines()[0])
|
|
127
|
+
print(f"usage: {Path(argv[0]).name} <eval/reports/*.json>")
|
|
128
|
+
return 2
|
|
129
|
+
path = Path(argv[1])
|
|
130
|
+
report = json.loads(path.read_text())
|
|
131
|
+
out = reaudit(report)
|
|
132
|
+
|
|
133
|
+
meta = out["meta"]
|
|
134
|
+
print(f"{path.name} — commit {meta.get('commit_short')}, model {meta.get('model')}")
|
|
135
|
+
print(f"\n{'skill':26} {'as recorded':>13} {'re-audited':>13} fabricated")
|
|
136
|
+
for suite in out["suites"]:
|
|
137
|
+
if suite["status"] == "unavailable":
|
|
138
|
+
print(f"{suite['master']:26} {_coverage(suite['recorded']):>13} "
|
|
139
|
+
f"{'(no meta.json)':>13}")
|
|
140
|
+
continue
|
|
141
|
+
moved = " <--" if suite["recorded"] != suite["recomputed"] else ""
|
|
142
|
+
print(
|
|
143
|
+
f"{suite['master']:26} {_coverage(suite['recorded']):>13} "
|
|
144
|
+
f"{_coverage(suite['recomputed']):>13} "
|
|
145
|
+
f"{', '.join(suite['fabricated'])}{moved}"
|
|
146
|
+
)
|
|
147
|
+
print(
|
|
148
|
+
f"\ntotal coverage {_coverage(out['totals']['recorded'])}"
|
|
149
|
+
f" -> {_coverage(out['totals']['recomputed'])}"
|
|
150
|
+
)
|
|
151
|
+
skipped = sorted({b for s in out["suites"] for b in s.get("noncitation", ())})
|
|
152
|
+
if skipped:
|
|
153
|
+
print(
|
|
154
|
+
f"\n{len(skipped)} 个【…】块判定为非引文,不计入上面的分母"
|
|
155
|
+
"(人格拿它当小标题或复述问题):"
|
|
156
|
+
)
|
|
157
|
+
for block in skipped:
|
|
158
|
+
print(f" 【{block}】")
|
|
159
|
+
return 0
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
if __name__ == "__main__":
|
|
163
|
+
sys.exit(main(sys.argv))
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Re-grade a committed run against the current judge and fixtures. Offline, free.
|
|
3
|
+
|
|
4
|
+
`check_response` is deterministic, and every answer has been stored since PR
|
|
5
|
+
#142. So any change to the judge or to the fixtures can be measured against an
|
|
6
|
+
already-paid-for run for nothing, instead of claimed.
|
|
7
|
+
|
|
8
|
+
That matters most for changes that *relax* something. Moving a requirement into
|
|
9
|
+
`must_convey` — "the substring matcher cannot decide this" — is honest when the
|
|
10
|
+
adjudication says so and is laundering when it does not. Re-grading the ¥3.89
|
|
11
|
+
sweep shows exactly which cases move and whether anything moved the wrong way.
|
|
12
|
+
|
|
13
|
+
python3 scripts/regrade-report.py eval/reports/0.11.0-06b8142-deepseek.json
|
|
14
|
+
|
|
15
|
+
Results are joined to fixtures **by question text**, never by position: a
|
|
16
|
+
fixture added or removed shifts every index after it, and grading an answer
|
|
17
|
+
against someone else's question would produce a confident, meaningless number.
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
import sys
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
26
|
+
|
|
27
|
+
import importlib.util
|
|
28
|
+
|
|
29
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
30
|
+
_spec = importlib.util.spec_from_file_location(
|
|
31
|
+
"_fidelity", ROOT / "scripts" / "test-fidelity.py"
|
|
32
|
+
)
|
|
33
|
+
_fidelity = importlib.util.module_from_spec(_spec)
|
|
34
|
+
sys.modules["_fidelity"] = _fidelity
|
|
35
|
+
_spec.loader.exec_module(_fidelity)
|
|
36
|
+
|
|
37
|
+
from verify_citations import load_declared_ids, load_member_aliases # noqa: E402
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def load_fixtures() -> dict[str, list[dict]]:
|
|
41
|
+
fixtures: dict[str, list[dict]] = {}
|
|
42
|
+
for path in sorted((ROOT / "prebuilt").glob("*/tests/fidelity.jsonl")):
|
|
43
|
+
fixtures[path.parent.parent.name] = [
|
|
44
|
+
json.loads(line) for line in path.read_text().splitlines() if line.strip()
|
|
45
|
+
]
|
|
46
|
+
return fixtures
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def regrade(report: dict, fixtures: dict[str, list[dict]]) -> dict:
|
|
50
|
+
"""Re-run the judge over every stored answer. Raises if the run stored none."""
|
|
51
|
+
if not any(
|
|
52
|
+
"response" in result
|
|
53
|
+
for suite in report["suites"]
|
|
54
|
+
for result in suite["results"]
|
|
55
|
+
):
|
|
56
|
+
raise ValueError("no stored answers in this report — nothing to re-grade")
|
|
57
|
+
|
|
58
|
+
cases: list[dict] = []
|
|
59
|
+
graded_results: list[dict] = []
|
|
60
|
+
by_test_type: dict[str, dict[str, int]] = {}
|
|
61
|
+
|
|
62
|
+
for suite in report["suites"]:
|
|
63
|
+
master = suite["master"]
|
|
64
|
+
cases_for_master = fixtures.get(master, [])
|
|
65
|
+
try:
|
|
66
|
+
declared = load_declared_ids(master) or None
|
|
67
|
+
aliases = load_member_aliases(master) or None
|
|
68
|
+
except (FileNotFoundError, ValueError):
|
|
69
|
+
declared = None
|
|
70
|
+
aliases = None
|
|
71
|
+
|
|
72
|
+
for result in suite["results"]:
|
|
73
|
+
if result.get("status") in ("truncated", "api_error"):
|
|
74
|
+
continue
|
|
75
|
+
index = result["index"]
|
|
76
|
+
fixture = cases_for_master[index] if index < len(cases_for_master) else None
|
|
77
|
+
if fixture is None or fixture.get("q") != result["question"]:
|
|
78
|
+
raise ValueError(
|
|
79
|
+
f"{master} #{index}: the fixture at this position does not match "
|
|
80
|
+
f"the question that was graded. Fixtures moved; re-grading by "
|
|
81
|
+
f"position would compare an answer with someone else's question."
|
|
82
|
+
)
|
|
83
|
+
check = _fidelity.check_response(
|
|
84
|
+
result.get("response") or "",
|
|
85
|
+
fixture,
|
|
86
|
+
declared_ids=declared,
|
|
87
|
+
member_aliases=aliases,
|
|
88
|
+
)
|
|
89
|
+
entry = _fidelity.result_entry(index, fixture, check, result.get("response") or "")
|
|
90
|
+
graded_results.append(entry)
|
|
91
|
+
was = result["status"]
|
|
92
|
+
now = "PASS" if check["passed"] else "FAIL"
|
|
93
|
+
bucket = by_test_type.setdefault(
|
|
94
|
+
result["test_type"], {"graded": 0, "was": 0, "now": 0}
|
|
95
|
+
)
|
|
96
|
+
bucket["graded"] += 1
|
|
97
|
+
bucket["was"] += was == "PASS"
|
|
98
|
+
bucket["now"] += now == "PASS"
|
|
99
|
+
cases.append(
|
|
100
|
+
{
|
|
101
|
+
"master": master,
|
|
102
|
+
"index": index,
|
|
103
|
+
"test_type": result["test_type"],
|
|
104
|
+
"was": was,
|
|
105
|
+
"now": now,
|
|
106
|
+
"needs_review": check["needs_review"],
|
|
107
|
+
}
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
"meta": report.get("meta", {}),
|
|
112
|
+
"cases": cases,
|
|
113
|
+
"by_test_type": by_test_type,
|
|
114
|
+
"mentions": _fidelity.summarize_mentions(graded_results),
|
|
115
|
+
"needs_review": sum(1 for c in cases if c["needs_review"]),
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def main(argv: list[str]) -> int:
|
|
120
|
+
if len(argv) != 2:
|
|
121
|
+
print(f"usage: {Path(argv[0]).name} <eval/reports/*.json>")
|
|
122
|
+
return 2
|
|
123
|
+
path = Path(argv[1])
|
|
124
|
+
out = regrade(json.loads(path.read_text()), load_fixtures())
|
|
125
|
+
meta = out["meta"]
|
|
126
|
+
print(f"{path.name} — commit {meta.get('commit_short')}, model {meta.get('model')}")
|
|
127
|
+
print(f"\n{'test_type':12} {'graded':>7} {'as graded':>11} {'re-graded':>11}")
|
|
128
|
+
total = {"graded": 0, "was": 0, "now": 0}
|
|
129
|
+
for test_type, b in sorted(out["by_test_type"].items()):
|
|
130
|
+
for key in total:
|
|
131
|
+
total[key] += b[key]
|
|
132
|
+
print(f"{test_type:12} {b['graded']:>7} "
|
|
133
|
+
f"{b['was']:>6} {b['was']/b['graded']:>4.0%} "
|
|
134
|
+
f"{b['now']:>6} {b['now']/b['graded']:>4.0%}")
|
|
135
|
+
print(f"{'TOTAL':12} {total['graded']:>7} "
|
|
136
|
+
f"{total['was']:>6} {total['was']/total['graded']:>4.0%} "
|
|
137
|
+
f"{total['now']:>6} {total['now']/total['graded']:>4.0%}")
|
|
138
|
+
|
|
139
|
+
m = out["mentions"]
|
|
140
|
+
print(
|
|
141
|
+
f"\nmention coverage {m['mentions_decided']}/{m['mention_requirements']} "
|
|
142
|
+
f"= {m['mention_coverage']} "
|
|
143
|
+
f"({m['mentions_unverified']} undecidable, "
|
|
144
|
+
f"{m['script_mismatches']} answers in the wrong script)"
|
|
145
|
+
)
|
|
146
|
+
print(f"cases needing adjudication: {out['needs_review']}")
|
|
147
|
+
|
|
148
|
+
moved_wrong = [c for c in out["cases"] if c["was"] == "PASS" and c["now"] == "FAIL"]
|
|
149
|
+
if moved_wrong:
|
|
150
|
+
print(f"\nWARNING: {len(moved_wrong)} case(s) that passed now fail:")
|
|
151
|
+
for c in moved_wrong:
|
|
152
|
+
print(f" {c['master']} #{c['index']} ({c['test_type']})")
|
|
153
|
+
return 0
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
if __name__ == "__main__":
|
|
157
|
+
sys.exit(main(sys.argv))
|