@icntswm/skillcheck 0.1.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/report.js ADDED
@@ -0,0 +1,93 @@
1
+ import { DIAGNOSIS_LIMIT } from "./judge.js";
2
+ const RESET = "\u001b[0m";
3
+ const MARK_WIDTH = 6; // "ok" / "FAIL" padded, keeps the columns aligned
4
+ const QUERY_WIDTH = 48;
5
+ /** One line per finished case plus a summary; plain text when not a TTY. */
6
+ export class Reporter {
7
+ out;
8
+ useColor;
9
+ constructor(out) {
10
+ this.out = out;
11
+ this.useColor = out.isTTY === true && !process.env.NO_COLOR;
12
+ }
13
+ header(nCases, repeat, nAgents, totalRuns) {
14
+ const runs = totalRuns ?? nCases * repeat * nAgents;
15
+ const agents = nAgents === 1 ? "agent" : "agents";
16
+ this.write(`${nCases} cases × ${repeat} repeat × ${nAgents} ${agents} = ${runs} runs\n`);
17
+ }
18
+ batchHeader(nCases, repeat, calls) {
19
+ this.write(`${nCases} cases × ${repeat} repeat, batch mode = ${calls} call${calls === 1 ? "" : "s"}\n`);
20
+ }
21
+ /** A dim advisory line under the summary. */
22
+ note(text) {
23
+ this.write(this.paint("2", text) + "\n");
24
+ }
25
+ caseDone(res) {
26
+ const label = `#${res.case.id ?? res.case.index}`;
27
+ const mark = res.ok ? this.paint("32", "ok".padEnd(MARK_WIDTH)) : this.paint("31", "FAIL".padEnd(MARK_WIDTH));
28
+ const runs = res.runs.length;
29
+ const score = runs > 1 ? ` ${res.passed}/${runs}` : "";
30
+ const loaded = [...new Set(res.runs.flatMap((r) => r.loaded))];
31
+ const failed = res.runs.find((r) => !r.ok);
32
+ const reason = !res.ok && failed ? ` · ${failed.reason}` : "";
33
+ const arrow = this.paint("2", "→");
34
+ this.write(`${mark}${label} ${oneLine(res.case.query)}${score} ${arrow} ${loaded.length > 0 ? loaded.join(", ") : this.paint("2", "—")}${reason}\n`);
35
+ const diagnosed = res.runs.find((r) => r.diagnosis);
36
+ if (diagnosed?.diagnosis) {
37
+ this.write(` ${this.paint("33", `diagnosis ${label}: ${diagnosed.diagnosis}`)}\n`);
38
+ }
39
+ }
40
+ /** A case the budget never let start; printed after all real runs settle. */
41
+ caseSkipped(c) {
42
+ const label = `#${c.id ?? c.index}`;
43
+ const mark = this.paint("33", "skip".padEnd(MARK_WIDTH));
44
+ const arrow = this.paint("2", "→");
45
+ this.write(`${mark}${label} ${oneLine(c.query)} ${arrow} budget reached\n`);
46
+ }
47
+ summary(results, extra) {
48
+ const verdicts = results.flatMap((r) => r.runs);
49
+ this.write("\n");
50
+ const diagnoses = verdicts.filter((v) => v.diagnosis).length;
51
+ if (diagnoses >= 2)
52
+ this.write(this.paint("33", `warning: ${diagnoses} diagnoses. ${DIAGNOSIS_LIMIT}`) + "\n");
53
+ const unavailable = extra?.unavailable ?? [];
54
+ if (unavailable.length > 0) {
55
+ this.write(this.paint("33", `warning: expected skills not available to the agent: ${unavailable.join(", ")}`) + "\n");
56
+ }
57
+ const pairs = extra?.confusion ?? [];
58
+ if (pairs.length > 0) {
59
+ this.write("confusion:\n");
60
+ for (const p of pairs)
61
+ this.write(` expected ${p.expected} → got ${p.got} (${p.count})\n`);
62
+ }
63
+ if (extra?.budget) {
64
+ const b = extra.budget;
65
+ const text = `budget $${b.limitUsd.toFixed(2)} reached (spent ~$${b.spent.toFixed(2)}), ${b.notStartedRuns} runs not started`;
66
+ this.write(this.paint("33", text) + "\n");
67
+ }
68
+ const failed = results.filter((r) => !r.ok).length;
69
+ const skipped = extra?.skipped ?? 0;
70
+ const skippedPart = skipped > 0 ? `, ${skipped} skipped` : "";
71
+ this.write(`${failed} failed${skippedPart} of ${results.length + skipped} · runs ${verdicts.length} · ${costLine(verdicts.map((v) => v.costUsd))}\n`);
72
+ }
73
+ paint(code, text) {
74
+ return this.useColor ? `\u001b[${code}m${text}${RESET}` : text;
75
+ }
76
+ write(text) {
77
+ this.out.write(text);
78
+ }
79
+ }
80
+ function costLine(costs) {
81
+ const known = costs.filter((c) => c !== null);
82
+ if (known.length === 0)
83
+ return "cost ?";
84
+ const sum = known.reduce((a, b) => a + b, 0);
85
+ const unknown = costs.length - known.length;
86
+ const tail = unknown > 0 ? ` (+? for ${unknown} runs)` : "";
87
+ return `cost $${sum.toFixed(2)}${tail}`;
88
+ }
89
+ /** Collapse to one line and truncate; long queries must not wrap the table. */
90
+ export function oneLine(text) {
91
+ const flat = text.replace(/\s+/g, " ").trim();
92
+ return flat.length > QUERY_WIDTH ? `${flat.slice(0, QUERY_WIDTH)}…` : flat;
93
+ }
@@ -0,0 +1,48 @@
1
+ import { readVersion } from "./version.js";
2
+ /** The single source of truth for --json and --junit; computed once per run. */
3
+ export function buildReport(input) {
4
+ const verdicts = input.cases.flatMap((e) => e.result?.runs ?? []);
5
+ const costs = verdicts.map((v) => v.costUsd);
6
+ const known = costs.filter((c) => c !== null);
7
+ return {
8
+ tool: "skillcheck",
9
+ version: readVersion(),
10
+ file: input.file,
11
+ agent: input.agent,
12
+ model: input.model,
13
+ startedAt: new Date(input.startedAtMs).toISOString(),
14
+ durationMs: input.durationMs,
15
+ summary: {
16
+ cases: input.cases.length,
17
+ failed: input.cases.filter((e) => e.result && !e.result.ok).length,
18
+ skipped: input.cases.filter((e) => !e.result).length,
19
+ runs: verdicts.length,
20
+ costUsd: known.reduce((a, b) => a + b, 0),
21
+ unknownCostRuns: costs.length - known.length,
22
+ diagnoses: verdicts.filter((v) => v.diagnosis).length,
23
+ budgetUsd: input.budgetUsd,
24
+ budgetReached: input.budgetReached,
25
+ },
26
+ unavailable: input.unavailable,
27
+ confusion: input.confusion,
28
+ cases: input.cases.map(toCaseReport),
29
+ };
30
+ }
31
+ function toCaseReport(e) {
32
+ const c = e.c;
33
+ return {
34
+ index: c.index,
35
+ id: c.id ?? null,
36
+ query: c.query,
37
+ note: c.note ?? null,
38
+ expect: c.expect,
39
+ expect_any: c.expect_any,
40
+ forbid: c.forbid,
41
+ first: c.first ?? null,
42
+ none: c.none,
43
+ status: e.result ? (e.result.ok ? "passed" : "failed") : "skipped",
44
+ passed: e.result?.passed ?? 0,
45
+ threshold: e.result?.threshold ?? e.threshold,
46
+ runs: e.result?.runs ?? [],
47
+ };
48
+ }
@@ -0,0 +1,7 @@
1
+ import * as fs from "node:fs";
2
+ /** Package version, read from the package.json next to dist/ or src/. */
3
+ export function readVersion() {
4
+ const url = new URL("../package.json", import.meta.url);
5
+ const pkg = JSON.parse(fs.readFileSync(url, "utf8"));
6
+ return pkg.version ?? "0.0.0";
7
+ }
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@icntswm/skillcheck",
3
+ "version": "0.1.0",
4
+ "description": "Regression tests for Claude Code skills: check that every request still loads the right skill",
5
+ "type": "module",
6
+ "bin": {
7
+ "skillcheck": "dist/cli.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "engines": {
13
+ "node": ">=20"
14
+ },
15
+ "scripts": {
16
+ "build": "tsc -p tsconfig.build.json",
17
+ "lint": "tsc --noEmit",
18
+ "test": "vitest run",
19
+ "prepublishOnly": "npm run lint && npm test && npm run build"
20
+ },
21
+ "keywords": [
22
+ "claude-code",
23
+ "skills",
24
+ "agents",
25
+ "routing",
26
+ "eval",
27
+ "regression-testing"
28
+ ],
29
+ "license": "MIT",
30
+ "dependencies": {
31
+ "yaml": "^2.9.1"
32
+ },
33
+ "devDependencies": {
34
+ "@types/node": "^26.6.3",
35
+ "tsx": "^4.23.15",
36
+ "typescript": "^7.0.2",
37
+ "vitest": "^5.0.2"
38
+ },
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "git+https://github.com/icntswm/skillcheck.git"
42
+ },
43
+ "homepage": "https://github.com/icntswm/skillcheck#readme",
44
+ "publishConfig": {
45
+ "access": "public"
46
+ }
47
+ }