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.
Files changed (77) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.cursor-plugin/plugin.json +1 -1
  4. package/GEMINI.md +1 -1
  5. package/README.md +84 -336
  6. package/README_EN.md +108 -321
  7. package/bin/cli.mjs +249 -9
  8. package/gemini-extension.json +1 -1
  9. package/hooks/session-start +68 -74
  10. package/hooks/session_start.py +152 -0
  11. package/package.json +6 -2
  12. package/prebuilt/{compare → compare-masters}/SKILL.md +21 -2
  13. package/prebuilt/master-ajahn-chah/meta.json +6 -0
  14. package/prebuilt/master-ajahn-chah/tests/fidelity.jsonl +6 -6
  15. package/prebuilt/master-atisha/tests/fidelity.jsonl +4 -4
  16. package/prebuilt/master-curriculum/SKILL.md +1 -1
  17. package/prebuilt/master-curriculum/references/tiantai.md +1 -1
  18. package/prebuilt/master-debate/SKILL.md +15 -3
  19. package/prebuilt/master-fazang/tests/fidelity.jsonl +2 -2
  20. package/prebuilt/master-help/SKILL.md +94 -0
  21. package/prebuilt/master-help/tests/fidelity.jsonl +10 -0
  22. package/prebuilt/master-huineng/tests/fidelity.jsonl +4 -4
  23. package/prebuilt/master-kumarajiva/meta.json +14 -3
  24. package/prebuilt/master-kumarajiva/tests/fidelity.jsonl +3 -3
  25. package/prebuilt/master-mahasi-sayadaw/tests/fidelity.jsonl +4 -4
  26. package/prebuilt/master-milarepa/tests/fidelity.jsonl +3 -3
  27. package/prebuilt/master-nagarjuna/meta.json +19 -4
  28. package/prebuilt/master-nagarjuna/tests/fidelity.jsonl +6 -6
  29. package/prebuilt/master-ouyi/meta.json +5 -0
  30. package/prebuilt/master-ouyi/references/teaching.md +3 -3
  31. package/prebuilt/master-ouyi/tests/fidelity.jsonl +3 -3
  32. package/prebuilt/master-tsongkhapa/meta.json +32 -5
  33. package/prebuilt/master-tsongkhapa/tests/fidelity.jsonl +2 -2
  34. package/prebuilt/master-xuanzang/tests/fidelity.jsonl +3 -3
  35. package/prebuilt/master-xuyun/tests/fidelity.jsonl +6 -6
  36. package/prebuilt/master-zhiyi/meta.json +2 -2
  37. package/prebuilt/master-zhiyi/tests/fidelity.jsonl +2 -2
  38. package/references/teaching-modes.md +8 -1
  39. package/routing.json +209 -0
  40. package/scripts/check-audit-ignores.py +105 -0
  41. package/scripts/check-eval-sdk-surface.py +142 -0
  42. package/scripts/check-gate-liveness.py +421 -0
  43. package/scripts/reaudit-report.py +163 -0
  44. package/scripts/regrade-report.py +157 -0
  45. package/scripts/smoke-eval-sdk.py +174 -0
  46. package/scripts/test-fidelity.py +992 -89
  47. package/scripts/validate-citation-references.py +150 -0
  48. package/scripts/validate-citation-templates.py +176 -0
  49. package/scripts/validate-fidelity.py +6 -1
  50. package/scripts/validate-fixture-terms.py +127 -0
  51. package/scripts/validate-routing.py +254 -0
  52. package/scripts/validate.py +63 -36
  53. package/scripts/verify-adjudication.py +316 -0
  54. package/scripts/verify_citations.py +739 -39
  55. package/skill-catalog.json +83 -20
  56. package/tools/cross_reference.py +44 -10
  57. package/tools/fojin-known-absent.json +14 -0
  58. package/tools/fojin_bridge.py +138 -8
  59. package/tools/rag_query.py +45 -2
  60. package/tools/skill_writer.py +50 -7
  61. package/tools/verify_sources.py +240 -15
  62. package/hooks/tests/test_run_hook.sh +0 -114
  63. package/hooks/tests/test_run_hook_cmd.sh +0 -94
  64. package/hooks/tests/test_session_start.sh +0 -149
  65. package/scripts/tests/test_check_manifest_versions.py +0 -217
  66. package/scripts/tests/test_debate_protocol.py +0 -159
  67. package/scripts/tests/test_injection_hardening.py +0 -174
  68. package/scripts/tests/test_select_fidelity_smoke.py +0 -142
  69. package/scripts/tests/test_validate_citation_contract.py +0 -408
  70. package/scripts/tests/test_validate_cross_critique.py +0 -149
  71. package/scripts/tests/test_validate_curriculum_sources.py +0 -144
  72. package/scripts/tests/test_validate_fidelity.py +0 -59
  73. package/scripts/tests/test_validate_lore_triggers_content.py +0 -372
  74. package/scripts/tests/test_validate_persona_fidelity.py +0 -317
  75. package/scripts/tests/test_validate_promptfoo_configs.py +0 -386
  76. package/scripts/tests/test_validate_workflow.py +0 -265
  77. /package/prebuilt/{compare → compare-masters}/tests/fidelity.jsonl +0 -0
@@ -0,0 +1,421 @@
1
+ #!/usr/bin/env python3
2
+ """Assert that this repo's gates actually examined something.
3
+
4
+ Three shipped defects were the same shape — a gate examined an empty set and
5
+ reported success:
6
+
7
+ - `pytest.ini` listed `testpaths = tests` while CI passed `scripts/tests/`
8
+ explicitly, so neither suite ever ran the other's cases.
9
+ - `tests/test_voice_rules.py` globbed `prebuilt/<slug>/voice.md` when
10
+ voice.md lives under `references/`. The empty glob parametrized every case
11
+ over an empty set: nothing asserted, reported green.
12
+ - The fidelity smoke — a branch-protection-required check — writes
13
+ `{"skipped": true, "reason": "no_api_key"}` and exits 0 when the secret is
14
+ missing, which it always has been.
15
+
16
+ None of those was a wrong assertion. Each was an assertion that never ran, and
17
+ a passing check is indistinguishable from a check that did nothing unless
18
+ something asserts otherwise. That is this script's whole job.
19
+
20
+ Usage:
21
+ python3 scripts/check-gate-liveness.py # check this repo
22
+ python3 scripts/check-gate-liveness.py --json # machine-readable
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import argparse
28
+ import functools
29
+ import json
30
+ import re
31
+ import subprocess
32
+ import sys
33
+ from pathlib import Path
34
+
35
+ import yaml
36
+
37
+ # A verdict — as opposed to a skip, an error, or a dry run.
38
+ GRADED_STATUSES = {"PASS", "FAIL"}
39
+
40
+
41
+ def check_every_test_file_collects(
42
+ test_files: list[str], collected_counts: dict[str, int]
43
+ ) -> list[str]:
44
+ """Every test file must contribute at least one collected test.
45
+
46
+ A file that imports cleanly and yields nothing is the voice_rules failure:
47
+ pytest reports success because there was nothing to fail.
48
+ """
49
+ problems = []
50
+ for path in sorted(test_files):
51
+ if collected_counts.get(path, 0) < 1:
52
+ problems.append(
53
+ f"{path} collected 0 tests — it asserts nothing but reports green "
54
+ "(empty glob or empty parametrize?)"
55
+ )
56
+ return problems
57
+
58
+
59
+ def check_testpaths_cover_suites(
60
+ testpaths: list[str], test_dirs: list[str]
61
+ ) -> list[str]:
62
+ """Every directory holding tests must be reachable from a bare `pytest`."""
63
+ covered = set(testpaths)
64
+ return [
65
+ f"{d} holds tests but is not in pytest.ini testpaths — a bare `pytest` skips it"
66
+ for d in sorted(test_dirs)
67
+ if d not in covered
68
+ ]
69
+
70
+
71
+ def check_graded_suites_graded_something(suites: list[dict]) -> list[str]:
72
+ """A graded fidelity suite that produced no verdict must not read as a pass.
73
+
74
+ Dry runs are exempt: grading nothing is what a dry run is for.
75
+ """
76
+ problems = []
77
+ for suite in suites:
78
+ if suite.get("mode") == "dry_run":
79
+ continue
80
+ verdicts = [
81
+ r for r in suite.get("results", [])
82
+ if str(r.get("status", "")).upper() in GRADED_STATUSES
83
+ ]
84
+ if not verdicts:
85
+ problems.append(
86
+ f"{suite.get('master', '?')}: graded suite produced 0 verdicts "
87
+ "— it graded nothing (missing API key, or every call errored)"
88
+ )
89
+ return problems
90
+
91
+
92
+ def check_catalog_matches_filesystem(
93
+ catalog: dict, prebuilt_dir: Path, root: Path
94
+ ) -> list[str]:
95
+ """The catalog and `prebuilt/` must name the same set of skills."""
96
+ entries = catalog.get("skills", [])
97
+ if not entries:
98
+ problems = ["skill-catalog.json lists no skills — an empty catalog validates vacuously"]
99
+ return problems
100
+
101
+ problems = []
102
+ catalog_sources = set()
103
+ for entry in entries:
104
+ source = entry.get("source", "")
105
+ catalog_sources.add(source)
106
+ if source.startswith("prebuilt/") and not (root / source).is_dir():
107
+ problems.append(
108
+ f"{entry.get('name', '?')}: catalog points at {source}, which does not exist"
109
+ )
110
+
111
+ if prebuilt_dir.is_dir():
112
+ for d in sorted(p for p in prebuilt_dir.iterdir() if p.is_dir()):
113
+ rel = f"prebuilt/{d.name}"
114
+ if rel not in catalog_sources:
115
+ problems.append(
116
+ f"{d.name}: directory exists under prebuilt/ but no catalog entry "
117
+ "claims it — it ships to nobody and no gate examines it"
118
+ )
119
+ return problems
120
+
121
+
122
+ def check_every_skill_has_fixtures(prebuilt_dir: Path) -> list[str]:
123
+ """Every prebuilt skill must carry at least one fidelity fixture."""
124
+ if not prebuilt_dir.is_dir():
125
+ return [f"{prebuilt_dir} does not exist — nothing to examine"]
126
+
127
+ problems = []
128
+ for d in sorted(p for p in prebuilt_dir.iterdir() if p.is_dir()):
129
+ fixtures = d / "tests" / "fidelity.jsonl"
130
+ if not fixtures.exists():
131
+ problems.append(f"{d.name}: no tests/fidelity.jsonl — nothing grades this skill")
132
+ continue
133
+ lines = [ln for ln in fixtures.read_text(encoding="utf-8").splitlines() if ln.strip()]
134
+ if not lines:
135
+ problems.append(f"{d.name}: tests/fidelity.jsonl is empty — it grades 0 cases")
136
+ return problems
137
+
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
+
249
+ # ---------------------------------------------------------------------------
250
+ # Repo-level wiring
251
+ # ---------------------------------------------------------------------------
252
+
253
+
254
+ def discover_test_files(root: Path) -> list[str]:
255
+ return sorted(
256
+ str(p.relative_to(root))
257
+ for d in ("tests", "scripts/tests")
258
+ for p in (root / d).glob("test_*.py")
259
+ if (root / d).is_dir()
260
+ )
261
+
262
+
263
+ def discover_test_dirs(root: Path) -> list[str]:
264
+ return sorted(
265
+ d for d in ("tests", "scripts/tests")
266
+ if (root / d).is_dir() and any((root / d).glob("test_*.py"))
267
+ )
268
+
269
+
270
+ def read_testpaths(root: Path) -> list[str]:
271
+ ini = root / "pytest.ini"
272
+ if not ini.exists():
273
+ return []
274
+ for line in ini.read_text(encoding="utf-8").splitlines():
275
+ if line.strip().startswith("testpaths"):
276
+ return line.split("=", 1)[1].split()
277
+ return []
278
+
279
+
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
+ """
291
+ proc = subprocess.run(
292
+ [sys.executable, "-m", "pytest", "--collect-only", "-q"],
293
+ cwd=root, capture_output=True, text=True,
294
+ )
295
+ counts: dict[str, int] = {}
296
+ for line in proc.stdout.splitlines():
297
+ match = re.match(r"^([\w./-]+\.py)::", line.strip())
298
+ if match:
299
+ counts[match.group(1)] = counts.get(match.group(1), 0) + 1
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
+
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"))
322
+
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]:
340
+ problems: list[str] = []
341
+
342
+ test_files = discover_test_files(root)
343
+ if not test_files:
344
+ return ["no test files found at all — this check would pass vacuously"]
345
+
346
+ problems += check_every_test_file_collects(test_files, collect_counts(root))
347
+ problems += check_testpaths_cover_suites(read_testpaths(root), discover_test_dirs(root))
348
+
349
+ catalog_path = root / "skill-catalog.json"
350
+ if catalog_path.exists():
351
+ catalog = json.loads(catalog_path.read_text(encoding="utf-8"))
352
+ problems += check_catalog_matches_filesystem(catalog, root / "prebuilt", root)
353
+
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)
381
+ return problems
382
+
383
+
384
+ def main() -> int:
385
+ parser = argparse.ArgumentParser(description=__doc__)
386
+ parser.add_argument("--root", type=Path, default=Path(__file__).resolve().parent.parent)
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
+ )
394
+ args = parser.parse_args()
395
+
396
+ problems = run_all(args.root, args.fidelity_report)
397
+
398
+ if args.json:
399
+ print(json.dumps(
400
+ {"problems": problems, "ok": not problems, "advisory_gates": ADVISORY_GATES},
401
+ ensure_ascii=False, indent=2,
402
+ ))
403
+ elif problems:
404
+ print(f"✗ {len(problems)} gate-liveness problem(s):\n")
405
+ for p in problems:
406
+ print(f" - {p}")
407
+ print("\nA gate that examines nothing reports the same green as one that passes.")
408
+ else:
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}")
416
+
417
+ return 1 if problems else 0
418
+
419
+
420
+ if __name__ == "__main__":
421
+ sys.exit(main())
@@ -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))