@uinaf/skillcheck 1.4.0 → 1.5.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/dist/cli.js +36 -7
- package/dist/lint.js +21 -3
- package/dist/scenario.js +3 -2
- package/docs/scenarios.md +1 -0
- package/docs/usage.md +8 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { lintSkills } from "./lint.js";
|
|
3
2
|
import { encodeRunNamePart, generateRun, requiredEvalPackages, resolvePackageDir, runNameFor } from "./scenario.js";
|
|
3
|
+
import { lintSkills } from "./lint.js";
|
|
4
4
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
5
5
|
import { createHash } from "node:crypto";
|
|
6
6
|
import fs from "node:fs";
|
|
@@ -357,6 +357,17 @@ function readJson(file) {
|
|
|
357
357
|
return;
|
|
358
358
|
}
|
|
359
359
|
}
|
|
360
|
+
function liveScenarios(root) {
|
|
361
|
+
const cli = path.join(root, "cli");
|
|
362
|
+
if (!(fs.existsSync(path.join(root, "skills")) || fs.existsSync(cli) && fs.readdirSync(cli).some((d) => fs.existsSync(path.join(cli, d, "skills"))))) return void 0;
|
|
363
|
+
return new Set(discoverScenarios(root).map((dir) => {
|
|
364
|
+
const parts = dir.split(path.sep);
|
|
365
|
+
return `${parts.at(-3)}\0${parts.at(-1)}`;
|
|
366
|
+
}));
|
|
367
|
+
}
|
|
368
|
+
function isLive(live, e) {
|
|
369
|
+
return live === void 0 || live.has(`${e.skill}\0${e.scenario}`);
|
|
370
|
+
}
|
|
360
371
|
function discoverScenarios(root) {
|
|
361
372
|
const roots = [path.join(root, "skills")];
|
|
362
373
|
const cliDir = path.join(root, "cli");
|
|
@@ -464,15 +475,26 @@ function resultIdentity(file, dir) {
|
|
|
464
475
|
variant
|
|
465
476
|
};
|
|
466
477
|
}
|
|
467
|
-
function reduceResults(dir, allowMixed) {
|
|
478
|
+
function reduceResults(dir, allowMixed, live) {
|
|
468
479
|
const entries = [];
|
|
469
480
|
const skipped = [];
|
|
481
|
+
const retired = /* @__PURE__ */ new Set();
|
|
470
482
|
const gradedAt = /* @__PURE__ */ new Map();
|
|
471
483
|
const shas = /* @__PURE__ */ new Set();
|
|
472
484
|
const files = fs.readdirSync(dir);
|
|
473
485
|
const incomplete = new Set(files.filter((f) => f.endsWith(".json.attempt")).map((f) => f.replace(/\.attempt$/, "")));
|
|
474
486
|
const results = files.filter((f) => f.endsWith(".json") && !f.endsWith(".meta.json"));
|
|
475
487
|
for (const f of [.../* @__PURE__ */ new Set([...results, ...incomplete])].sort()) {
|
|
488
|
+
let identity;
|
|
489
|
+
try {
|
|
490
|
+
identity = resultIdentity(f, dir);
|
|
491
|
+
} catch {
|
|
492
|
+
identity = void 0;
|
|
493
|
+
}
|
|
494
|
+
if (identity !== void 0 && identity.scenario !== "" && !isLive(live, identity)) {
|
|
495
|
+
retired.add(entryKey(identity));
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
476
498
|
if (f.endsWith("--cursor.json")) {
|
|
477
499
|
console.error(`skipping ${f}: Cursor harness is retired`);
|
|
478
500
|
skipped.push(f);
|
|
@@ -522,7 +544,8 @@ function reduceResults(dir, allowMixed) {
|
|
|
522
544
|
treeSha: shas.size === 1 ? [...shas][0] : shas.size === 0 ? "none" : "mixed",
|
|
523
545
|
entries,
|
|
524
546
|
skipped,
|
|
525
|
-
gradedAt
|
|
547
|
+
gradedAt,
|
|
548
|
+
retired
|
|
526
549
|
};
|
|
527
550
|
}
|
|
528
551
|
function entryKey(e) {
|
|
@@ -565,12 +588,17 @@ function readExistingScorecard(out) {
|
|
|
565
588
|
function cmdSummarize(argv) {
|
|
566
589
|
const { positional, flags } = parseArgs(argv);
|
|
567
590
|
if (positional.length > 0) fail("usage: skillcheck summarize [--root DIR] [--allow-mixed]");
|
|
568
|
-
const
|
|
591
|
+
const root = resolveRoot(flags);
|
|
592
|
+
const dirs = stateDirs(root);
|
|
569
593
|
if (!fs.existsSync(dirs.results)) fail(`no results directory at ${dirs.results}; run some evals first`);
|
|
570
|
-
const
|
|
594
|
+
const live = liveScenarios(root);
|
|
595
|
+
const { entries, skipped, gradedAt, retired: retiredResults } = reduceResults(dirs.results, flags.get("--allow-mixed") === true, live);
|
|
571
596
|
fs.mkdirSync(dirs.scorecards, { recursive: true });
|
|
572
597
|
const out = path.join(dirs.scorecards, `${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.json`);
|
|
573
|
-
const
|
|
598
|
+
const previous = readExistingScorecard(out);
|
|
599
|
+
const existing = previous.filter((e) => isLive(live, e));
|
|
600
|
+
for (const e of previous) if (!isLive(live, e)) retiredResults.add(entryKey(e));
|
|
601
|
+
const retired = retiredResults.size;
|
|
574
602
|
const skippedKeys = new Set(skipped.map((file) => ({
|
|
575
603
|
key: entryKey(resultIdentity(file, dirs.results)),
|
|
576
604
|
modifiedAt: fs.statSync(fs.existsSync(path.join(dirs.results, `${file}.attempt`)) ? path.join(dirs.results, `${file}.attempt`) : path.join(dirs.results, file)).mtimeMs
|
|
@@ -587,6 +615,7 @@ function cmdSummarize(argv) {
|
|
|
587
615
|
scenarios: merged.entries
|
|
588
616
|
};
|
|
589
617
|
fs.writeFileSync(out, JSON.stringify(scorecard, null, 2) + "\n");
|
|
618
|
+
if (retired > 0) console.log(`dropped ${retired} row(s) for scenarios no longer in the tree`);
|
|
590
619
|
const scenarios = merged.entries.filter((e) => e.variant !== "control");
|
|
591
620
|
console.log(`${out}: ${scenarios.length} scenario(s), ${scenarios.filter((e) => e.pass).length} passing, ${merged.entries.length - scenarios.length} control(s), ${skipped.length} skipped file(s)`);
|
|
592
621
|
if (existing.length > 0) console.log(`merged into today's scorecard: ${entries.length} from this run, ${merged.carried} carried over`);
|
|
@@ -688,4 +717,4 @@ if (isMainModule()) {
|
|
|
688
717
|
}
|
|
689
718
|
}
|
|
690
719
|
//#endregion
|
|
691
|
-
export { NOISY_SPREAD, aggregateTrials, assertUniformConfig, classifyResult, ensurePrivateDir, formatStats, mergeScorecard, parseArgs, parsePositiveInt, reduceResults, resolveRoot, resultRunConfig, runConfigOf, runOptions, stateDirs, summarizeSkills, toolVersion, treeShaOf };
|
|
720
|
+
export { NOISY_SPREAD, aggregateTrials, assertUniformConfig, classifyResult, ensurePrivateDir, formatStats, liveScenarios, mergeScorecard, parseArgs, parsePositiveInt, reduceResults, resolveRoot, resultRunConfig, runConfigOf, runOptions, stateDirs, summarizeSkills, toolVersion, treeShaOf };
|
package/dist/lint.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { FILE_BLOCK } from "./scenario.js";
|
|
1
2
|
import fs from "node:fs";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
//#region src/lint.ts
|
|
@@ -61,19 +62,36 @@ function lintSkill(dir, root, errors) {
|
|
|
61
62
|
function lintSkills(root) {
|
|
62
63
|
const roots = ["skills", ...fs.globSync("cli/*/skills", { cwd: root })].map((r) => path.join(root, r));
|
|
63
64
|
const errors = [];
|
|
64
|
-
|
|
65
|
+
const skills = [];
|
|
65
66
|
for (const dir of roots) {
|
|
66
67
|
if (!fs.existsSync(dir)) continue;
|
|
67
68
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
68
69
|
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
69
70
|
lintSkill(path.join(dir, entry.name), root, errors);
|
|
70
|
-
|
|
71
|
+
skills.push(path.join(dir, entry.name));
|
|
71
72
|
}
|
|
72
73
|
}
|
|
74
|
+
lintTasks(skills, root, errors);
|
|
73
75
|
return {
|
|
74
76
|
errors,
|
|
75
|
-
count
|
|
77
|
+
count: skills.length
|
|
76
78
|
};
|
|
77
79
|
}
|
|
80
|
+
function lintTasks(skills, root, errors) {
|
|
81
|
+
const names = skills.map((dir) => path.basename(dir));
|
|
82
|
+
for (const dir of skills) {
|
|
83
|
+
const evals = path.join(dir, "evals");
|
|
84
|
+
if (!fs.existsSync(evals)) continue;
|
|
85
|
+
for (const scenario of fs.readdirSync(evals, { withFileTypes: true })) {
|
|
86
|
+
const file = path.join(evals, scenario.name, "task.md");
|
|
87
|
+
if (!scenario.isDirectory() || !fs.existsSync(file)) continue;
|
|
88
|
+
const text = fs.readFileSync(file, "utf8").replace(FILE_BLOCK, "");
|
|
89
|
+
for (const name of names) {
|
|
90
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
91
|
+
if (new RegExp(`(?<![\\w-])${escaped}(?![\\w-])`, "i").test(text)) errors.push(`${path.relative(root, file)}: task names skill ${name}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
78
96
|
//#endregion
|
|
79
97
|
export { lintSkills };
|
package/dist/scenario.js
CHANGED
|
@@ -4,6 +4,7 @@ import fs from "node:fs";
|
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
//#region src/scenario.ts
|
|
7
|
+
const FILE_BLOCK = /^=+ FILE: (.+?) =+\n([\s\S]*?)\n=+ END FILE =+$/gm;
|
|
7
8
|
const DEFAULT_CLAUDE_AGENT = "claude-opus-5";
|
|
8
9
|
function loadScenario(scenarioDir) {
|
|
9
10
|
const match = scenarioDir.match(/skills\/([^/]+)\/evals\/([^/]+)$/);
|
|
@@ -15,7 +16,7 @@ function loadScenario(scenarioDir) {
|
|
|
15
16
|
if (criteria.skill_use !== void 0 && criteria.skill_use !== "required" && criteria.skill_use !== "optional") throw new Error(`skill_use must be "required" or "optional" in ${scenarioDir}`);
|
|
16
17
|
for (const item of criteria.checklist) if (!(typeof item?.name === "string" && item.name.trim() !== "" && typeof item?.description === "string" && item.description.trim() !== "" && Number.isFinite(item?.max_score) && item.max_score > 0)) throw new Error(`invalid checklist item in ${scenarioDir}: ${JSON.stringify(item)}`);
|
|
17
18
|
const files = [];
|
|
18
|
-
const task = taskMd.replace(
|
|
19
|
+
const task = taskMd.replace(FILE_BLOCK, (_, name, content) => {
|
|
19
20
|
files.push({
|
|
20
21
|
name: name.trim(),
|
|
21
22
|
content: content + "\n"
|
|
@@ -322,4 +323,4 @@ function generateRun(scenarioDir, opts, paths) {
|
|
|
322
323
|
};
|
|
323
324
|
}
|
|
324
325
|
//#endregion
|
|
325
|
-
export { DEFAULT_CLAUDE_AGENT, buildConfig, encodeRunNamePart, generateRun, isHiddenSkill, loadScenario, materialize, privateCodexHome, requiredEvalPackages, resolvePackageDir, runNameFor, sdkNodeModulesDir, stripHiddenFlag, trialLabel };
|
|
326
|
+
export { DEFAULT_CLAUDE_AGENT, FILE_BLOCK, buildConfig, encodeRunNamePart, generateRun, isHiddenSkill, loadScenario, materialize, privateCodexHome, requiredEvalPackages, resolvePackageDir, runNameFor, sdkNodeModulesDir, stripHiddenFlag, trialLabel };
|
package/docs/scenarios.md
CHANGED
|
@@ -35,6 +35,7 @@ own examiner or embed generated dependencies.
|
|
|
35
35
|
|
|
36
36
|
Write the task the way a user would write it. Do not name the skill, describe
|
|
37
37
|
its steps, or hint at the checklist: routing is part of what is being measured.
|
|
38
|
+
`skillcheck lint` rejects a task whose prose names any skill in the root.
|
|
38
39
|
|
|
39
40
|
## criteria.json
|
|
40
41
|
|
package/docs/usage.md
CHANGED
|
@@ -20,6 +20,9 @@ Checks each `<root>/skills/<skill>/`:
|
|
|
20
20
|
- `disable-model-invocation`, when present, is the bare YAML boolean `true`.
|
|
21
21
|
A quoted `"true"` is an error
|
|
22
22
|
- Relative links in the body resolve on disk
|
|
23
|
+
- No eval `task.md` names a skill from the root, outside its inline input
|
|
24
|
+
files. A named skill gives routing away, and a no-skill control can read
|
|
25
|
+
the operator's installed copy
|
|
23
26
|
|
|
24
27
|
Code spans and fenced blocks are stripped before links are checked, so example
|
|
25
28
|
links never fail. External schemes and `#anchors` pass. Dot-directories under
|
|
@@ -179,6 +182,11 @@ six of twenty-nine scenarios therefore leaves twenty-nine rows in the file, not
|
|
|
179
182
|
six. A same-date file that cannot be parsed stops the write instead of being
|
|
180
183
|
overwritten.
|
|
181
184
|
|
|
185
|
+
Rows for scenarios that no longer exist in the tree, whether carried from
|
|
186
|
+
today's scorecard or left in `results/`, are dropped and counted on stdout.
|
|
187
|
+
A root with no skills tree (`skills/` or `cli/*/skills/`) holds results only
|
|
188
|
+
and keeps every row.
|
|
189
|
+
|
|
182
190
|
Files that are not promptfoo results and ungraded transport errors are skipped
|
|
183
191
|
with a warning rather than failing the reduction. Graded assertion failures
|
|
184
192
|
remain scored results. If a skipped file matches an existing scorecard row,
|