@uinaf/skillcheck 1.0.1 → 1.0.2
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/dist/cli.js +44 -16
- package/dist/scenario.js +10 -2
- package/docs/usage.md +9 -2
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { lintSkills } from "./lint.js";
|
|
3
|
-
import { generateRun, requiredEvalPackages, resolvePackageDir, runNameFor } from "./scenario.js";
|
|
3
|
+
import { encodeRunNamePart, generateRun, requiredEvalPackages, resolvePackageDir, runNameFor } from "./scenario.js";
|
|
4
4
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
5
5
|
import fs from "node:fs";
|
|
6
6
|
import path from "node:path";
|
|
@@ -134,14 +134,19 @@ function attemptPath(resultPath) {
|
|
|
134
134
|
}
|
|
135
135
|
function runScenario(scenarioDir, opts, root) {
|
|
136
136
|
const dirs = stateDirs(root);
|
|
137
|
-
const { name, configPath } = generateRun(path.resolve(scenarioDir), opts, {
|
|
137
|
+
const { name, configPath, skill, scenario } = generateRun(path.resolve(scenarioDir), opts, {
|
|
138
138
|
scratchDir: dirs.scratch,
|
|
139
139
|
transformPath: path.join(here, `transform${selfExt}`),
|
|
140
140
|
grokProviderPath: path.join(here, `grok-provider${selfExt}`)
|
|
141
141
|
});
|
|
142
142
|
fs.mkdirSync(dirs.results, { recursive: true });
|
|
143
143
|
const resultPath = path.join(dirs.results, `${name}.json`);
|
|
144
|
-
|
|
144
|
+
const identity = {
|
|
145
|
+
skill,
|
|
146
|
+
scenario,
|
|
147
|
+
harness: opts.harness
|
|
148
|
+
};
|
|
149
|
+
fs.writeFileSync(attemptPath(resultPath), JSON.stringify(identity) + "\n");
|
|
145
150
|
fs.rmSync(resultPath, { force: true });
|
|
146
151
|
fs.rmSync(metaPath(resultPath), { force: true });
|
|
147
152
|
const sha = gitHead(root);
|
|
@@ -182,7 +187,7 @@ function runScenario(scenarioDir, opts, root) {
|
|
|
182
187
|
if (verdict.score !== void 0) {
|
|
183
188
|
fs.writeFileSync(metaPath(resultPath), JSON.stringify({
|
|
184
189
|
skills_tree_sha: sha,
|
|
185
|
-
|
|
190
|
+
...identity,
|
|
186
191
|
ran_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
187
192
|
tool_version: toolVersion()
|
|
188
193
|
}, null, 2) + "\n");
|
|
@@ -235,11 +240,13 @@ function cmdSweep(argv) {
|
|
|
235
240
|
for (const dir of discoverScenarios(root)) {
|
|
236
241
|
const name = runNameFor(dir, opts.harness);
|
|
237
242
|
const resultPath = path.join(resultsDir, `${name}.json`);
|
|
238
|
-
if (!all && fs.existsSync(resultPath) && !fs.existsSync(attemptPath(resultPath))) {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
+
if (!all && fs.existsSync(resultPath) && !fs.existsSync(attemptPath(resultPath))) try {
|
|
244
|
+
if (classifyResult(JSON.parse(fs.readFileSync(resultPath, "utf8"))).score !== void 0) {
|
|
245
|
+
skipped++;
|
|
246
|
+
console.log(`SKIP ${name} (results exist; use --all to rerun)`);
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
} catch {}
|
|
243
250
|
const o = runScenario(dir, opts, root);
|
|
244
251
|
if (o.score === void 0) {
|
|
245
252
|
errored++;
|
|
@@ -255,16 +262,26 @@ function cmdSweep(argv) {
|
|
|
255
262
|
console.log(`\nsweep: ${passed} passed, ${failed} failed, ${errored} errored, ${skipped} skipped`);
|
|
256
263
|
process.exit(errored > 0 ? 2 : failed > 0 ? 1 : 0);
|
|
257
264
|
}
|
|
258
|
-
function resultIdentity(file) {
|
|
265
|
+
function resultIdentity(file, dir) {
|
|
266
|
+
const base = file.replace(/\.json$/, "");
|
|
267
|
+
for (const sidecar of [`${base}.meta.json`, `${file}.attempt`]) try {
|
|
268
|
+
const identity = JSON.parse(fs.readFileSync(path.join(dir, sidecar), "utf8"));
|
|
269
|
+
if (identity !== null && typeof identity === "object" && "skill" in identity && typeof identity.skill === "string" && "scenario" in identity && typeof identity.scenario === "string" && "harness" in identity && (identity.harness === "claude" || identity.harness === "codex" || identity.harness === "grok")) return {
|
|
270
|
+
skill: identity.skill,
|
|
271
|
+
scenario: identity.scenario,
|
|
272
|
+
harness: identity.harness
|
|
273
|
+
};
|
|
274
|
+
} catch {}
|
|
275
|
+
if (/^~v3~[0-9a-f]{64}$/.test(base)) throw new Error(`missing identity metadata for ${file}`);
|
|
259
276
|
const decode = (part) => {
|
|
260
277
|
if (!part.startsWith("~v2~")) return part;
|
|
261
278
|
try {
|
|
262
|
-
|
|
279
|
+
const decoded = decodeURIComponent(part.slice(4));
|
|
280
|
+
return encodeRunNamePart(decoded) === part ? decoded : part;
|
|
263
281
|
} catch {
|
|
264
282
|
return part;
|
|
265
283
|
}
|
|
266
284
|
};
|
|
267
|
-
const base = file.replace(/\.json$/, "");
|
|
268
285
|
const suffix = base.match(/--(codex|grok|cursor)$/);
|
|
269
286
|
const harness = suffix === null ? "claude" : suffix[1];
|
|
270
287
|
const [skill, ...rest] = base.replace(/--(codex|grok|cursor)$/, "").split("--");
|
|
@@ -277,6 +294,7 @@ function resultIdentity(file) {
|
|
|
277
294
|
function reduceResults(dir, allowMixed) {
|
|
278
295
|
const entries = [];
|
|
279
296
|
const skipped = [];
|
|
297
|
+
const gradedAt = /* @__PURE__ */ new Map();
|
|
280
298
|
const shas = /* @__PURE__ */ new Set();
|
|
281
299
|
const files = fs.readdirSync(dir);
|
|
282
300
|
const incomplete = new Set(files.filter((f) => f.endsWith(".json.attempt")).map((f) => f.replace(/\.attempt$/, "")));
|
|
@@ -308,7 +326,13 @@ function reduceResults(dir, allowMixed) {
|
|
|
308
326
|
const provider = raw.config?.providers?.[0];
|
|
309
327
|
const judge = raw.config?.defaultTest?.options?.provider;
|
|
310
328
|
const base = f.replace(/\.json$/, "");
|
|
311
|
-
const { skill, scenario, harness } = resultIdentity(f);
|
|
329
|
+
const { skill, scenario, harness } = resultIdentity(f, dir);
|
|
330
|
+
const key = entryKey({
|
|
331
|
+
skill,
|
|
332
|
+
scenario,
|
|
333
|
+
harness
|
|
334
|
+
});
|
|
335
|
+
gradedAt.set(key, Math.max(gradedAt.get(key) ?? 0, fs.statSync(path.join(dir, f)).mtimeMs));
|
|
312
336
|
let sha = "unattested";
|
|
313
337
|
try {
|
|
314
338
|
sha = JSON.parse(fs.readFileSync(path.join(dir, `${base}.meta.json`), "utf8")).skills_tree_sha ?? "unattested";
|
|
@@ -331,7 +355,8 @@ function reduceResults(dir, allowMixed) {
|
|
|
331
355
|
return {
|
|
332
356
|
treeSha: shas.size === 1 ? [...shas][0] : shas.size === 0 ? "none" : "mixed",
|
|
333
357
|
entries,
|
|
334
|
-
skipped
|
|
358
|
+
skipped,
|
|
359
|
+
gradedAt
|
|
335
360
|
};
|
|
336
361
|
}
|
|
337
362
|
function entryKey(e) {
|
|
@@ -375,11 +400,14 @@ function cmdSummarize(argv) {
|
|
|
375
400
|
if (positional.length > 0) fail("usage: skillcheck summarize [--root DIR] [--allow-mixed]");
|
|
376
401
|
const dirs = stateDirs(resolveRoot(flags));
|
|
377
402
|
if (!fs.existsSync(dirs.results)) fail(`no results directory at ${dirs.results}; run some evals first`);
|
|
378
|
-
const { entries, skipped } = reduceResults(dirs.results, flags.get("--allow-mixed") === true);
|
|
403
|
+
const { entries, skipped, gradedAt } = reduceResults(dirs.results, flags.get("--allow-mixed") === true);
|
|
379
404
|
fs.mkdirSync(dirs.scorecards, { recursive: true });
|
|
380
405
|
const out = path.join(dirs.scorecards, `${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.json`);
|
|
381
406
|
const existing = readExistingScorecard(out);
|
|
382
|
-
const skippedKeys = new Set(skipped.map((file) =>
|
|
407
|
+
const skippedKeys = new Set(skipped.map((file) => ({
|
|
408
|
+
key: entryKey(resultIdentity(file, dirs.results)),
|
|
409
|
+
modifiedAt: fs.statSync(fs.existsSync(path.join(dirs.results, `${file}.attempt`)) ? path.join(dirs.results, `${file}.attempt`) : path.join(dirs.results, file)).mtimeMs
|
|
410
|
+
})).filter(({ key, modifiedAt }) => (gradedAt.get(key) ?? 0) <= modifiedAt).map(({ key }) => key));
|
|
383
411
|
if (existing.some((entry) => skippedKeys.has(entryKey(entry)))) throw new Error("skipped rerun matches an existing score; refusing to carry it or overwrite the scorecard");
|
|
384
412
|
const merged = mergeScorecard(existing, entries);
|
|
385
413
|
const treeSha = treeShaOf(merged.entries);
|
package/dist/scenario.js
CHANGED
|
@@ -40,7 +40,13 @@ function runNameFor(scenarioDir, harness) {
|
|
|
40
40
|
const m = path.resolve(scenarioDir).match(/skills\/([^/]+)\/evals\/([^/]+)$/);
|
|
41
41
|
if (!m) throw new Error(`not a scenario dir (want .../skills/<skill>/evals/<scenario>): ${scenarioDir}`);
|
|
42
42
|
const name = `${encodeRunNamePart(m[1])}--${encodeRunNamePart(m[2])}`;
|
|
43
|
-
|
|
43
|
+
const full = harness === "claude" ? name : `${name}--${harness}`;
|
|
44
|
+
if (Buffer.byteLength(`${full}.json.attempt`) <= 255) return full;
|
|
45
|
+
return `~v3~${createHash("sha256").update(JSON.stringify([
|
|
46
|
+
m[1],
|
|
47
|
+
m[2],
|
|
48
|
+
harness
|
|
49
|
+
])).digest("hex")}`;
|
|
44
50
|
}
|
|
45
51
|
function frontmatterRange(text) {
|
|
46
52
|
const lines = text.split("\n");
|
|
@@ -259,7 +265,9 @@ function generateRun(scenarioDir, opts, paths) {
|
|
|
259
265
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
260
266
|
return {
|
|
261
267
|
name,
|
|
262
|
-
configPath
|
|
268
|
+
configPath,
|
|
269
|
+
skill: s.skill,
|
|
270
|
+
scenario: s.scenario
|
|
263
271
|
};
|
|
264
272
|
}
|
|
265
273
|
//#endregion
|
package/docs/usage.md
CHANGED
|
@@ -115,14 +115,19 @@ Files that are not promptfoo results and ungraded transport errors are skipped
|
|
|
115
115
|
with a warning rather than failing the reduction. Graded assertion failures
|
|
116
116
|
remain scored results. If a skipped file matches an existing scorecard row,
|
|
117
117
|
summary generation fails and leaves the scorecard unchanged, so an errored rerun
|
|
118
|
-
cannot carry forward its old score.
|
|
118
|
+
cannot carry forward its old score. A graded result for the same identity
|
|
119
|
+
supersedes a skipped attempt only when the result file is newer. This also
|
|
120
|
+
applies with `--allow-mixed`.
|
|
119
121
|
Results from the retired Cursor harness are skipped with their original identity,
|
|
120
122
|
so they cannot become Claude scores or silently carry an old Cursor row.
|
|
121
123
|
Runs keep a `<name>.json.attempt` marker until a graded result and its provenance
|
|
122
|
-
are written. An outstanding marker makes `summarize` skip that identity even
|
|
124
|
+
are written. The marker records the original skill, scenario, and harness. An outstanding marker makes `summarize` skip that identity even
|
|
123
125
|
when the child produced no result file or left partial output. The marker does
|
|
124
126
|
not count as a result for the sweep's existence check, so no-output failures
|
|
125
127
|
remain eligible for retry.
|
|
128
|
+
Long escaped names use a short hashed filename; the original identity is kept
|
|
129
|
+
in the attempt marker and result sidecar. `sweep` retries existing results that
|
|
130
|
+
contain no grade, including results written by older versions without a marker.
|
|
126
131
|
|
|
127
132
|
## Provenance
|
|
128
133
|
|
|
@@ -131,6 +136,8 @@ Each successful run writes a `<name>.meta.json` sidecar next to its result:
|
|
|
131
136
|
```json
|
|
132
137
|
{
|
|
133
138
|
"skills_tree_sha": "<root repo HEAD at run time>",
|
|
139
|
+
"skill": "<skill directory name>",
|
|
140
|
+
"scenario": "<scenario directory name>",
|
|
134
141
|
"harness": "claude",
|
|
135
142
|
"ran_at": "<ISO timestamp>",
|
|
136
143
|
"tool_version": "<skillcheck version>"
|