@wix/pathgrade 1.0.27 → 1.0.28

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.
Files changed (58) hide show
  1. package/README.md +12 -19
  2. package/dist/adapters/jest/invocation-adapter.js +5 -1
  3. package/dist/adapters/jest/reporter.js +4 -1
  4. package/dist/adapters/jest/results.js +8 -0
  5. package/dist/adapters/node-test/index.d.ts +5 -0
  6. package/dist/adapters/node-test/index.js +35 -7
  7. package/dist/adapters/node-test/invocation-adapter.js +3 -1
  8. package/dist/adapters/node-test/runner-adapter.js +22 -15
  9. package/dist/adapters/vitest/reporter.js +4 -1
  10. package/dist/agents/codex-app-server/agent.js +1 -51
  11. package/dist/agents/codex-app-server/turn-notifications.d.ts +6 -0
  12. package/dist/agents/codex-app-server/turn-notifications.js +51 -0
  13. package/dist/agents/codex-app-server/turn-state.d.ts +19 -0
  14. package/dist/agents/codex-app-server/turn-state.js +1 -0
  15. package/dist/agents/opencode/protocol.d.ts +7 -0
  16. package/dist/agents/opencode/protocol.js +47 -0
  17. package/dist/agents/opencode.js +1 -47
  18. package/dist/analytics/engine.js +5 -2
  19. package/dist/commands/report.js +16 -2
  20. package/dist/commands/run-args.d.ts +1 -0
  21. package/dist/commands/run-args.js +13 -0
  22. package/dist/commands/run-changed.js +5 -15
  23. package/dist/config/pathgrade.d.ts +3 -0
  24. package/dist/config/pathgrade.js +33 -1
  25. package/dist/pathgrade.js +10 -2
  26. package/dist/reporters/cli.js +13 -6
  27. package/dist/reporters/github-comment.js +12 -9
  28. package/dist/reporters/loader.d.ts +1 -0
  29. package/dist/reporters/loader.js +27 -2
  30. package/dist/reporters/report-summary.js +13 -5
  31. package/dist/reporting/core.d.ts +1 -0
  32. package/dist/reporting/core.js +183 -105
  33. package/dist/reporting/types.d.ts +19 -3
  34. package/dist/runners/adapter-loader.js +17 -12
  35. package/dist/runners/direct-reporter-attempts.d.ts +1 -0
  36. package/dist/runners/direct-reporter-attempts.js +7 -0
  37. package/dist/runners/invocation.d.ts +2 -0
  38. package/dist/runners/model-builders.js +1 -0
  39. package/dist/runners/model-validation.js +31 -0
  40. package/dist/runners/model.d.ts +4 -0
  41. package/dist/runners/orchestrator.d.ts +2 -0
  42. package/dist/runners/orchestrator.js +11 -1
  43. package/dist/runners/repeated-attempts.d.ts +7 -0
  44. package/dist/runners/repeated-attempts.js +149 -0
  45. package/dist/runners/repeated-invocation.d.ts +7 -0
  46. package/dist/runners/repeated-invocation.js +129 -0
  47. package/dist/runners/report-projection.js +16 -6
  48. package/dist/runners/vitest-adapter.js +10 -0
  49. package/dist/runners/vitest-invocation.js +2 -0
  50. package/dist/sdk/agent-runtime-options.d.ts +12 -0
  51. package/dist/sdk/agent-runtime-options.js +67 -0
  52. package/dist/sdk/agent.js +4 -57
  53. package/dist/sdk/case-context.js +7 -2
  54. package/dist/sdk/lifecycle.js +8 -3
  55. package/dist/sdk/result-capture.js +4 -1
  56. package/dist/types.d.ts +32 -5
  57. package/dist/viewer.html +19 -19
  58. package/package.json +2 -2
@@ -43,10 +43,24 @@ function isPathgradeReport(value) {
43
43
  if (!value || typeof value !== 'object')
44
44
  return false;
45
45
  const v = value;
46
- return (v.version === 1 &&
46
+ return ((v.version === 1 || v.version === 2) &&
47
47
  typeof v.overall_pass_rate === 'number' &&
48
48
  (v.status === 'pass' || v.status === 'fail') &&
49
- Array.isArray(v.groups));
49
+ Array.isArray(v.groups) &&
50
+ (v.version === 1 || (typeof v.overall_mean_reward === 'number'
51
+ && hasValidAttemptCounts(v)
52
+ && v.groups.every(group => typeof group.mean_reward === 'number'))));
53
+ }
54
+ function hasValidAttemptCounts(report) {
55
+ const requested = report.attempts_requested;
56
+ const completed = report.attempts_completed;
57
+ return typeof requested === 'number'
58
+ && Number.isSafeInteger(requested)
59
+ && requested >= 1
60
+ && typeof completed === 'number'
61
+ && Number.isSafeInteger(completed)
62
+ && completed >= 0
63
+ && completed <= requested;
50
64
  }
51
65
  async function loadReport(resolvedPath) {
52
66
  if (!(await fs.pathExists(resolvedPath))) {
@@ -8,6 +8,7 @@
8
8
  export interface PathgradeRunArgs {
9
9
  runnerArgs: string[];
10
10
  adapterName?: string;
11
+ attempts?: number;
11
12
  forceDiagnostics: boolean;
12
13
  forceVerbose: boolean;
13
14
  changed: boolean;
@@ -14,6 +14,7 @@ export function parsePathgradeRunArgs(args) {
14
14
  let since;
15
15
  let changedFilesPath;
16
16
  let adapterName;
17
+ let attempts;
17
18
  let passthrough = false;
18
19
  for (const arg of args) {
19
20
  if (passthrough) {
@@ -44,6 +45,10 @@ export function parsePathgradeRunArgs(args) {
44
45
  adapterName = arg.slice('--adapter='.length);
45
46
  continue;
46
47
  }
48
+ if (arg.startsWith('--attempts=')) {
49
+ attempts = parseAttempts(arg.slice('--attempts='.length));
50
+ continue;
51
+ }
47
52
  if (arg.startsWith('--since=')) {
48
53
  since = arg.slice('--since='.length);
49
54
  continue;
@@ -68,8 +73,16 @@ export function parsePathgradeRunArgs(args) {
68
73
  changed,
69
74
  quiet,
70
75
  adapterName,
76
+ attempts,
71
77
  since,
72
78
  changedFilesPath,
73
79
  };
74
80
  return warnings.length > 0 ? { ...base, warnings } : base;
75
81
  }
82
+ function parseAttempts(value) {
83
+ const attempts = Number(value);
84
+ if (!Number.isSafeInteger(attempts) || attempts < 1) {
85
+ throw new Error('pathgrade: --attempts must be an integer greater than or equal to 1');
86
+ }
87
+ return attempts;
88
+ }
@@ -27,13 +27,13 @@ export async function runChanged(opts) {
27
27
  const runnerEnv = buildRunnerEnv(parsed, {
28
28
  PATHGRADE_SELECTION_INVOCATION_ID: selectionInvocationId,
29
29
  });
30
- const configPath = findVitestConfigArg(parsed.runnerArgs);
31
30
  let config;
32
31
  let runnerInvocation;
33
32
  try {
34
33
  config = await resolvePathgradeConfig({
35
34
  cwd,
36
- legacyVitestConfigPath: configPath,
35
+ cli: parsed.attempts === undefined ? undefined : { attempts: parsed.attempts },
36
+ runnerArgs: parsed.runnerArgs,
37
37
  warn: w => {
38
38
  if (!parsed.quiet)
39
39
  process.stderr.write(`${w}\n`);
@@ -107,6 +107,7 @@ export async function runChanged(opts) {
107
107
  totalEvals: evalFiles.length,
108
108
  changedCount: changedFiles.length,
109
109
  result,
110
+ attempts: config.attempts,
110
111
  });
111
112
  }
112
113
  // Persist the sidecar immediately — even on empty selection, so the
@@ -128,7 +129,7 @@ export async function runChanged(opts) {
128
129
  cwd,
129
130
  runnerArgs,
130
131
  selectedFiles,
131
- env: runnerEnv,
132
+ env: { ...runnerEnv, PATHGRADE_ATTEMPT_COUNT: String(config.attempts) },
132
133
  });
133
134
  }
134
135
  function printRunStartSummary(input) {
@@ -140,6 +141,7 @@ function printRunStartSummary(input) {
140
141
  const globalLabel = result.globalMatch ? `\`${result.globalMatch}\`` : 'none';
141
142
  process.stderr.write(` global matches: ${globalLabel}\n`);
142
143
  process.stderr.write(` selected: ${result.selected.length} / ${totalEvals} evals\n`);
144
+ process.stderr.write(` attempts: ${input.attempts} per selected case\n`);
143
145
  for (const entry of result.selected) {
144
146
  process.stderr.write(` ${entry.file}\n`);
145
147
  }
@@ -158,15 +160,3 @@ function readChangedFilesList(filePath) {
158
160
  function errMsg(err) {
159
161
  return err instanceof Error ? err.message : String(err);
160
162
  }
161
- function findVitestConfigArg(args) {
162
- for (let i = 0; i < args.length; i++) {
163
- const arg = args[i];
164
- if (arg === '--config' || arg === '-c')
165
- return args[i + 1];
166
- if (arg.startsWith('--config='))
167
- return arg.slice('--config='.length);
168
- if (arg.startsWith('-c='))
169
- return arg.slice('-c='.length);
170
- }
171
- return undefined;
172
- }
@@ -1,4 +1,5 @@
1
1
  export interface PathgradeConfig {
2
+ attempts?: number;
2
3
  runner?: {
3
4
  adapter?: string;
4
5
  args?: string[];
@@ -18,6 +19,7 @@ export interface PathgradeConfig {
18
19
  };
19
20
  }
20
21
  export interface ResolvedPathgradeConfig {
22
+ attempts: number;
21
23
  runner: {
22
24
  adapter: string;
23
25
  args: string[];
@@ -44,5 +46,6 @@ export declare function resolvePathgradeConfig(input: {
44
46
  cli?: PathgradeConfig;
45
47
  configPath?: string;
46
48
  legacyVitestConfigPath?: string;
49
+ runnerArgs?: readonly string[];
47
50
  warn?: (message: string) => void;
48
51
  }): Promise<ResolvedPathgradeConfig>;
@@ -17,6 +17,7 @@ const PATHGRADE_CONFIG_CANDIDATES = [
17
17
  ];
18
18
  export function defaultPathgradeConfig() {
19
19
  return {
20
+ attempts: 1,
20
21
  runner: {
21
22
  adapter: 'vitest',
22
23
  args: [],
@@ -35,13 +36,30 @@ export function defaultPathgradeConfig() {
35
36
  }
36
37
  export async function resolvePathgradeConfig(input) {
37
38
  const fileConfig = await loadPathgradeConfigFile(input.cwd, input.configPath);
38
- const legacyConfig = await loadLegacyVitestConfig(input.cwd, input.legacyVitestConfigPath, input.warn);
39
+ const legacyConfig = await loadLegacyVitestConfig(input.cwd, input.legacyVitestConfigPath ?? findVitestConfigArg([
40
+ ...(fileConfig?.runner?.args ?? []),
41
+ ...(input.runnerArgs ?? []),
42
+ ]), input.warn);
39
43
  return mergePathgradeConfig(mergePathgradeConfig(mergePathgradeConfig(defaultPathgradeConfig(), legacyConfig), fileConfig), input.cli);
40
44
  }
45
+ function findVitestConfigArg(args) {
46
+ let configPath;
47
+ for (let i = 0; i < args.length; i++) {
48
+ const arg = args[i];
49
+ if (arg === '--config' || arg === '-c')
50
+ configPath = args[i + 1];
51
+ else if (arg.startsWith('--config='))
52
+ configPath = arg.slice('--config='.length);
53
+ else if (arg.startsWith('-c='))
54
+ configPath = arg.slice('-c='.length);
55
+ }
56
+ return configPath;
57
+ }
41
58
  function mergePathgradeConfig(base, override) {
42
59
  if (!override)
43
60
  return base;
44
61
  return {
62
+ attempts: override.attempts ?? base.attempts,
45
63
  runner: {
46
64
  adapter: override.runner?.adapter ?? base.runner.adapter,
47
65
  args: override.runner?.args ?? base.runner.args,
@@ -91,6 +109,7 @@ function validatePathgradeConfig(value, label) {
91
109
  throw invalidConfig(label, 'default export must be an object');
92
110
  }
93
111
  validateOptionalObject(value.runner, label, 'runner');
112
+ validateOptionalPositiveInteger(value.attempts, label, 'attempts');
94
113
  const runner = asOptionalObject(value.runner);
95
114
  validateOptionalString(runner?.adapter, label, 'runner.adapter');
96
115
  validateOptionalStringArray(runner?.args, label, 'runner.args');
@@ -139,6 +158,11 @@ function validateOptionalNumber(value, label, field) {
139
158
  throw invalidConfig(label, `${field} must be a number`);
140
159
  }
141
160
  }
161
+ function validateOptionalPositiveInteger(value, label, field) {
162
+ if (value !== undefined && (!Number.isSafeInteger(value) || Number(value) < 1)) {
163
+ throw invalidConfig(label, `${field} must be an integer greater than or equal to 1`);
164
+ }
165
+ }
142
166
  function invalidConfig(label, reason) {
143
167
  return new InvalidPathgradeConfigError(`pathgrade: invalid ${label}: ${reason}`);
144
168
  }
@@ -179,6 +203,14 @@ async function loadLegacyVitestConfig(cwd, configPath, warn = () => { }) {
179
203
  if (!isObject(opts))
180
204
  return undefined;
181
205
  return {
206
+ ...(opts.reporter === 'cli' || opts.reporter === 'browser' || opts.reporter === 'json'
207
+ ? { reporter: opts.reporter }
208
+ : {}),
209
+ ...(typeof opts.diagnostics === 'boolean' ? { diagnostics: opts.diagnostics } : {}),
210
+ ...(typeof opts.verbose === 'boolean' ? { verbose: opts.verbose } : {}),
211
+ ...(isObject(opts.ci) && typeof opts.ci.threshold === 'number'
212
+ ? { ci: { threshold: opts.ci.threshold } }
213
+ : {}),
182
214
  evals: {
183
215
  ...(Array.isArray(opts.include) ? { include: opts.include } : {}),
184
216
  ...(Array.isArray(opts.exclude) ? { exclude: opts.exclude } : {}),
package/dist/pathgrade.js CHANGED
@@ -201,7 +201,14 @@ export async function runPathgradeCli(options = {}) {
201
201
  await clearSidecar(process.cwd());
202
202
  const env = buildRunnerEnv(parsed);
203
203
  try {
204
- const config = await resolvePathgradeConfig({ cwd: process.cwd() });
204
+ const config = await resolvePathgradeConfig({
205
+ cwd: process.cwd(),
206
+ cli: parsed.attempts === undefined ? undefined : { attempts: parsed.attempts },
207
+ runnerArgs: parsed.runnerArgs,
208
+ });
209
+ if (!parsed.quiet && config.attempts > 1) {
210
+ console.error(`pathgrade: ${config.attempts} sequential attempts per selected case`);
211
+ }
205
212
  const runner = await loadRunnerInvocationAdapter({
206
213
  adapterName: parsed.adapterName ?? config.runner.adapter,
207
214
  cwd: process.cwd(),
@@ -210,7 +217,7 @@ export async function runPathgradeCli(options = {}) {
210
217
  process.exitCode = await runner.run({
211
218
  cwd: process.cwd(),
212
219
  runnerArgs: [...config.runner.args, ...parsed.runnerArgs],
213
- env,
220
+ env: { ...env, PATHGRADE_ATTEMPT_COUNT: String(config.attempts) },
214
221
  });
215
222
  }
216
223
  catch (err) {
@@ -233,6 +240,7 @@ function printHelp(cliName) {
233
240
  [--since=<ref>] Override base ref (implies git mode)
234
241
  [--changed-files=<path>] Use an explicit newline-delimited file list
235
242
  [--adapter=<name|path>] Select built-in or third-party runner adapter
243
+ [--attempts=N] Run every selected case N times sequentially
236
244
  [--diagnostics] Print full diagnostics for passing evals too
237
245
  [--quiet] Suppress the run-start summary
238
246
  [--verbose|-v] Stream live per-turn events to stderr during the run
@@ -18,8 +18,8 @@ export async function runCliPreview(resultsDir, opts) {
18
18
  }
19
19
  console.log(`\n${fmt.bold('pathgrade preview')} ${fmt.dim(`${entries.length} reports from ${resolved}`)}\n`);
20
20
  for (const { file, ...report } of entries) {
21
- const passRate = report.pass_rate ?? 0;
22
- const isPass = passRate >= 0.5;
21
+ const meanReward = report.mean_reward ?? report.pass_rate ?? 0;
22
+ const isPass = report.status === undefined ? meanReward >= 0.5 : report.status === 'pass';
23
23
  const trials = report.trials || [];
24
24
  const avgDur = trials.reduce((s, t) => s + (t.duration_ms || 0), 0) / (trials.length || 1);
25
25
  const totalTokens = trials.reduce((s, t) => s + (t.input_tokens || 0) + (t.output_tokens || 0) + (t.conversation_input_tokens || 0) + (t.conversation_output_tokens || 0), 0);
@@ -33,9 +33,9 @@ export async function runCliPreview(resultsDir, opts) {
33
33
  console.log();
34
34
  // ── Summary metrics
35
35
  const metrics = [
36
- ['Pass Rate', `${(passRate * 100).toFixed(1)}%`],
37
- ['pass@k', report.pass_at_k != null ? `${(report.pass_at_k * 100).toFixed(1)}%` : '—'],
38
- ['pass^k', report.pass_pow_k != null ? `${(report.pass_pow_k * 100).toFixed(1)}%` : '—'],
36
+ ['Mean Reward', `${(meanReward * 100).toFixed(1)}%`],
37
+ ['Success Rate', report.success_rate != null ? `${(report.success_rate * 100).toFixed(1)}%` : '—'],
38
+ ['pass@k', formatPassAtK(report.pass_at_k, report.pass_at_k_unavailable_reason)],
39
39
  ['Avg Duration', `${(avgDur / 1000).toFixed(1)}s`],
40
40
  ['Total Tokens', `~${totalTokens}`],
41
41
  ['Skills', report.skills_used?.join(', ') || 'none'],
@@ -47,7 +47,7 @@ export async function runCliPreview(resultsDir, opts) {
47
47
  // ── Trials
48
48
  for (const trial of trials) {
49
49
  const evaluated = trial.reward !== undefined;
50
- const tp = evaluated && trial.reward >= 0.5;
50
+ const tp = evaluated && (trial.runner_outcome === undefined ? trial.reward >= 0.5 : trial.runner_outcome === 'passed' && trial.reward === 1);
51
51
  const trialStatus = !evaluated ? fmt.dim('N/A') : tp ? fmt.pass('PASS') : fmt.fail('FAIL');
52
52
  const reward = fmt.bold(evaluated ? trial.reward.toFixed(2) : 'n/a');
53
53
  const dur = `${((trial.duration_ms || 0) / 1000).toFixed(1)}s`;
@@ -120,6 +120,13 @@ export async function runCliPreview(resultsDir, opts) {
120
120
  console.log();
121
121
  }
122
122
  }
123
+ function formatPassAtK(value, reason) {
124
+ if (typeof value === 'number')
125
+ return `${(value * 100).toFixed(1)}% (legacy v1)`;
126
+ if (value)
127
+ return Object.entries(value).map(([k, metric]) => `@${k} ${(metric * 100).toFixed(1)}%`).join(', ');
128
+ return reason ? `— (${reason.replaceAll('_', ' ')})` : '—';
129
+ }
123
130
  function formatScorerStatus(status) {
124
131
  switch (status) {
125
132
  case 'error':
@@ -37,6 +37,13 @@ export function commentMarker(commentId) {
37
37
  function pct(n) {
38
38
  return `${(n * 100).toFixed(1)}%`;
39
39
  }
40
+ function formatPassAtK(value, reason) {
41
+ if (typeof value === 'number')
42
+ return `${pct(value)} (legacy v1)`;
43
+ if (value)
44
+ return Object.entries(value).map(([k, metric]) => `pass@${k} ${pct(metric)}`).join(', ');
45
+ return reason ? `Unavailable (${reason.replaceAll('_', ' ')})` : 'Unavailable';
46
+ }
40
47
  function durationSeconds(ms) {
41
48
  return `${(ms / 1000).toFixed(1)}s`;
42
49
  }
@@ -55,10 +62,7 @@ function escapeTableCell(value) {
55
62
  * posted as a GitHub issue comment body.
56
63
  */
57
64
  export function formatReportMarkdown(report, opts) {
58
- const totalTrials = report.groups.reduce((n, g) => n + g.trials.length, 0);
59
- const p = report.overall_pass_rate;
60
- const overallPassAtK = totalTrials > 0 ? 1 - Math.pow(1 - p, totalTrials) : 0;
61
- const overallPassPowK = totalTrials > 0 ? Math.pow(p, totalTrials) : 0;
65
+ const meanReward = report.overall_mean_reward ?? report.overall_pass_rate;
62
66
  const icon = report.status === 'pass' ? '✅' : '❌';
63
67
  const lines = [];
64
68
  lines.push(commentMarker(opts.commentId));
@@ -67,9 +71,8 @@ export function formatReportMarkdown(report, opts) {
67
71
  lines.push('');
68
72
  lines.push(`**Last run:** \`${report.timestamp}\``);
69
73
  lines.push('');
70
- lines.push(`**Pass rate:** ${pct(p)} | ` +
71
- `**pass@${totalTrials}:** ${pct(overallPassAtK)} | ` +
72
- `**pass^${totalTrials}:** ${pct(overallPassPowK)}`);
74
+ lines.push(`**Mean reward:** ${pct(meanReward)}` +
75
+ (report.attempts_requested ? ` | **Attempts:** ${report.attempts_completed ?? 0}/${report.attempts_requested}` : ''));
73
76
  if (opts.detailsUrl) {
74
77
  lines.push('');
75
78
  lines.push(`**Details:** [Open the full report](${formatLinkDestination(opts.detailsUrl)})`);
@@ -83,12 +86,12 @@ export function formatReportMarkdown(report, opts) {
83
86
  lines.push(`Threshold: ${pct(report.threshold)} — ${report.status.toUpperCase()}`);
84
87
  }
85
88
  lines.push('');
86
- lines.push('| Group | Pass rate | pass@k | pass^k | Skills | Avg duration |');
89
+ lines.push('| Group | Mean reward | Success rate | pass@k | Skills | Avg duration |');
87
90
  lines.push('|---|---|---|---|---|---|');
88
91
  for (const group of report.groups) {
89
92
  const skills = group.skills_used.length > 0 ? group.skills_used.join(', ') : '—';
90
93
  const avg = computeAvgDuration(group.trials);
91
- lines.push(`| ${escapeTableCell(group.task)} | ${pct(group.pass_rate)} | ${pct(group.pass_at_k)} | ${pct(group.pass_pow_k)} | ${escapeTableCell(skills)} | ${durationSeconds(avg)} |`);
94
+ lines.push(`| ${escapeTableCell(group.task)} | ${pct(group.mean_reward ?? group.pass_rate ?? 0)} | ${group.success_rate === undefined ? '—' : pct(group.success_rate)} | ${escapeTableCell(formatPassAtK(group.pass_at_k, group.pass_at_k_unavailable_reason))} | ${escapeTableCell(skills)} | ${durationSeconds(avg)} |`);
92
95
  }
93
96
  if (report.selection) {
94
97
  lines.push('');
@@ -2,6 +2,7 @@ import type { EvalReport } from '../types.js';
2
2
  export interface LoadedReport extends EvalReport {
3
3
  file: string;
4
4
  timestamp?: string;
5
+ status?: 'pass' | 'fail';
5
6
  }
6
7
  export declare function loadReports(resultsDir: string, opts?: {
7
8
  skipTraces?: boolean;
@@ -7,7 +7,13 @@ async function hydrateTraces(report, traceFile, resolved) {
7
7
  const traceTrials = await fs.readJSON(tracePath);
8
8
  if (!Array.isArray(traceTrials) || !Array.isArray(report.trials))
9
9
  return;
10
- report.trials = report.trials.map((t, i) => ({ ...t, ...traceTrials[i] }));
10
+ const tracesByAttemptId = new Map(traceTrials
11
+ .filter((trial) => typeof trial?.attempt_id === 'string')
12
+ .map((trial) => [trial.attempt_id, trial]));
13
+ report.trials = report.trials.map((trial, index) => ({
14
+ ...trial,
15
+ ...(trial.attempt_id ? tracesByAttemptId.get(trial.attempt_id) : traceTrials[index]),
16
+ }));
11
17
  }
12
18
  export async function loadReports(resultsDir, opts) {
13
19
  const resolved = path.resolve(resultsDir);
@@ -25,8 +31,15 @@ export async function loadReports(resultsDir, opts) {
25
31
  results.push({ file, ...raw });
26
32
  continue;
27
33
  }
34
+ if (raw.version !== 1 && raw.version !== 2)
35
+ continue;
28
36
  for (const group of raw.groups) {
29
- const report = { file, timestamp: raw.timestamp, ...group };
37
+ const report = {
38
+ file,
39
+ timestamp: raw.timestamp,
40
+ ...group,
41
+ status: groupStatus(raw, group),
42
+ };
30
43
  if (!opts?.skipTraces && group.trace_file) {
31
44
  await hydrateTraces(report, group.trace_file, resolved);
32
45
  }
@@ -37,3 +50,15 @@ export async function loadReports(resultsDir, opts) {
37
50
  }
38
51
  return results;
39
52
  }
53
+ function groupStatus(raw, group) {
54
+ if (group.status === 'pass' || group.status === 'fail')
55
+ return group.status;
56
+ const meanReward = group.mean_reward ?? group.pass_rate ?? 0;
57
+ if (typeof raw.threshold === 'number')
58
+ return meanReward >= raw.threshold ? 'pass' : 'fail';
59
+ const outcomes = Array.isArray(group.trials) ? group.trials.map((trial) => trial.runner_outcome) : [];
60
+ if (outcomes.length > 0 && outcomes.every((outcome) => (outcome === 'passed' || outcome === 'failed' || outcome === 'not-run'))) {
61
+ return outcomes.every((outcome) => outcome === 'passed') ? 'pass' : 'fail';
62
+ }
63
+ return meanReward >= 0.5 ? 'pass' : 'fail';
64
+ }
@@ -3,13 +3,21 @@ import { formatDiagnostics, formatDiagnosticsSummary } from './diagnostics.js';
3
3
  export function printReportSummary(groups, opts = {}) {
4
4
  console.log(`\n${fmt.bold('── pathgrade summary ')}${fmt.dim('─'.repeat(40))}\n`);
5
5
  for (const group of groups) {
6
- const prColor = group.pass_rate >= 0.5 ? fmt.green : fmt.red;
6
+ const rewardColor = group.mean_reward >= 0.5 ? fmt.green : fmt.red;
7
7
  console.log(` ${fmt.bold(group.task)}`);
8
- console.log(` ${fmt.dim('pass rate'.padEnd(12))} ${prColor((group.pass_rate * 100).toFixed(1) + '%')}`);
9
- console.log(` ${fmt.dim(`pass@${group.trial_count}`.padEnd(12))} ${(group.pass_at_k * 100).toFixed(1)}%`);
10
- console.log(` ${fmt.dim(`pass^${group.trial_count}`.padEnd(12))} ${(group.pass_pow_k * 100).toFixed(1)}%`);
8
+ console.log(` ${fmt.dim('mean reward'.padEnd(12))} ${rewardColor((group.mean_reward * 100).toFixed(1) + '%')}`);
9
+ console.log(` ${fmt.dim('success'.padEnd(12))} ${group.success_rate === undefined ? '—' : `${(group.success_rate * 100).toFixed(1)}%`}`);
10
+ if (group.pass_at_k) {
11
+ for (const [k, value] of Object.entries(group.pass_at_k)) {
12
+ console.log(` ${fmt.dim(`pass@${k}`.padEnd(12))} ${(value * 100).toFixed(1)}%`);
13
+ }
14
+ }
15
+ else {
16
+ const reason = group.pass_at_k_unavailable_reason?.replaceAll('_', ' ') ?? 'repeated binary attempts required';
17
+ console.log(` ${fmt.dim('pass@k'.padEnd(12))} — (${reason})`);
18
+ }
11
19
  console.log(` ${fmt.dim('avg time'.padEnd(12))} ${(group.average_duration_ms / 1000).toFixed(1)}s`);
12
- console.log(` ${fmt.dim('trials'.padEnd(12))} ${group.trial_count}`);
20
+ console.log(` ${fmt.dim('attempts'.padEnd(12))} ${group.trial_count}`);
13
21
  console.log();
14
22
  for (const diagnostic of group.diagnostics) {
15
23
  const shouldPrintFull = opts.forceVerbose
@@ -1,2 +1,3 @@
1
1
  import type { PathgradeReportBuildResult, ReportRunInput } from './types.js';
2
2
  export declare function buildPathgradeReport(input: ReportRunInput): PathgradeReportBuildResult;
3
+ export declare function finiteSamplePassAtK(n: number, successes: number, k: number): number;