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,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))
|