@fede0089/skill-eval 3.1.0 → 3.3.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.
@@ -1,2 +1 @@
1
1
  export { HtmlReporter } from './html-reporter.js';
2
- export { JsonReporter } from './json-reporter.js';
@@ -1,20 +1,36 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
3
  import { ConfigError } from '../core/errors.js';
4
+ /**
5
+ * Normalizes an --eval-file value to the bare file name used inside the evals directory.
6
+ * Accepts 'edge-cases', 'edge-cases.json' and 'path/to/evals/edge-cases.json' alike.
7
+ */
8
+ function normalizeEvalFileName(evalFile) {
9
+ const base = path.basename(evalFile.trim());
10
+ return base.endsWith('.json') ? base : `${base}.json`;
11
+ }
4
12
  /**
5
13
  * Loads and merges all JSON evaluation files from a skill's evals directory.
6
14
  * Aligned with Anthropic's recommendation to split evals by capability/regression.
7
15
  * Supports legacy 'tasks' and 'assertions' internally while maintaining 'evals' and 'expectations' in files.
16
+ * When 'evalFile' is given, only that file is loaded instead of the whole suite.
8
17
  */
9
- export function loadEvalSuite(skillPath) {
18
+ export function loadEvalSuite(skillPath, evalFile) {
10
19
  const evalsDir = path.resolve(skillPath, 'evals');
11
20
  if (!fs.existsSync(evalsDir)) {
12
21
  throw new ConfigError(`Could not find evals directory at ${evalsDir}`);
13
22
  }
14
- const files = fs.readdirSync(evalsDir).filter(file => file.endsWith('.json'));
23
+ let files = fs.readdirSync(evalsDir).filter(file => file.endsWith('.json'));
15
24
  if (files.length === 0) {
16
25
  throw new ConfigError(`No JSON evaluation files found in ${evalsDir}`);
17
26
  }
27
+ if (evalFile !== undefined) {
28
+ const wanted = normalizeEvalFileName(evalFile);
29
+ if (!files.includes(wanted)) {
30
+ throw new ConfigError(`Eval file '${wanted}' not found in ${evalsDir}. Available: ${[...files].sort().join(', ')}.`);
31
+ }
32
+ files = [wanted];
33
+ }
18
34
  let mergedSkillName = '';
19
35
  const mergedTasks = [];
20
36
  for (const file of files) {
@@ -59,7 +75,8 @@ export function loadEvalSuite(skillPath) {
59
75
  mergedTasks.push(...mappedTasks);
60
76
  }
61
77
  if (mergedTasks.length === 0) {
62
- throw new ConfigError(`No evaluations found in any of the JSON files in ${evalsDir}`);
78
+ const scope = evalFile !== undefined ? `${files[0]} in ${evalsDir}` : `any of the JSON files in ${evalsDir}`;
79
+ throw new ConfigError(`No evaluations found in ${scope}`);
63
80
  }
64
81
  return {
65
82
  skill_name: mergedSkillName,
@@ -62,6 +62,25 @@ export function parseStreamResult(output) {
62
62
  (typeof resultEvent.response === 'string' ? resultEvent.response : '');
63
63
  return { response: text };
64
64
  }
65
+ /**
66
+ * Counts tool invocations and reads the terminal status from a stream-json blob.
67
+ * Both are needed to tell a legitimate short answer apart from an agent that
68
+ * stopped early or degenerated: a trial with tool calls and a 'success' status
69
+ * that still produced two characters of text is a very different failure from
70
+ * one that never started.
71
+ * Returns toolCalls: 0 and an undefined status when the blob carries no events.
72
+ */
73
+ export function parseStreamStats(output) {
74
+ let toolCalls = 0;
75
+ let status;
76
+ for (const event of parseNdjsonEvents(output)) {
77
+ if (event.type === 'tool_use')
78
+ toolCalls += 1;
79
+ else if (event.type === 'result')
80
+ status = event.status;
81
+ }
82
+ return { toolCalls, status };
83
+ }
65
84
  /**
66
85
  * Extracts token consumption stats from a Gemini CLI stream-json stdout blob.
67
86
  * Looks for a result event with a stats.total_tokens field.
@@ -98,7 +98,7 @@ function boxLabel(key, value) {
98
98
  * Always shown regardless of debug mode.
99
99
  */
100
100
  export function renderRunHeader(config) {
101
- const { command, skillName, agent, workspace, tasks, trials, maxAgents, timeoutMs, runDir, evalId } = config;
101
+ const { command, skillName, agent, workspace, tasks, trials, maxAgents, timeoutMs, runDir, evalId, evalFile } = config;
102
102
  let timeoutStr = 'None';
103
103
  if (timeoutMs && timeoutMs > 0) {
104
104
  const timeoutSec = timeoutMs / 1000;
@@ -111,7 +111,12 @@ export function renderRunHeader(config) {
111
111
  const dashes = '─'.repeat(BOX_INNER - titleLabel.length);
112
112
  const top = chalk.gray('┌─ ') + chalk.bold(titleLabel) + ' ' + chalk.gray(dashes + '┐');
113
113
  const bottom = chalk.gray('└' + '─'.repeat(BOX_INNER + 2) + '┘');
114
- const commandPart = evalId !== undefined ? `${command} · eval #${evalId}` : command;
114
+ const filterParts = [];
115
+ if (evalFile !== undefined)
116
+ filterParts.push(evalFile);
117
+ if (evalId !== undefined)
118
+ filterParts.push(`eval #${evalId}`);
119
+ const commandPart = [command, ...filterParts].join(' · ');
115
120
  const title = chalk.bold.cyan(skillName) + chalk.gray(` · ${commandPart}`);
116
121
  const runLine = `${tasks} task${tasks !== 1 ? 's' : ''} · ${trials} trial${trials !== 1 ? 's' : ''} · agents ${maxAgents}`;
117
122
  process.stdout.write('\n');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fede0089/skill-eval",
3
- "version": "3.1.0",
3
+ "version": "3.3.0",
4
4
  "description": "CLI to evaluate agent skills triggering and functionality",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -1,10 +0,0 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import { Logger } from '../utils/logger.js';
4
- export class JsonReporter {
5
- generate(report, runDir) {
6
- const jsonPath = path.join(runDir, 'summary.json');
7
- fs.writeFileSync(jsonPath, JSON.stringify(report, null, 2), 'utf-8');
8
- Logger.write(`\n Report: file://${jsonPath}\n`);
9
- }
10
- }