@wix/pathgrade 0.36.0 → 1.0.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.
Files changed (117) hide show
  1. package/LICENSE +24 -0
  2. package/README.md +508 -0
  3. package/dist/adapter-kit/index.d.ts +17 -0
  4. package/dist/adapter-kit/index.js +14 -0
  5. package/dist/adapters/jest/index.d.ts +3 -0
  6. package/dist/adapters/jest/index.js +3 -0
  7. package/dist/adapters/jest/invocation-adapter.d.ts +15 -0
  8. package/dist/adapters/jest/invocation-adapter.js +115 -0
  9. package/dist/adapters/jest/lifecycle.d.ts +21 -0
  10. package/dist/adapters/jest/lifecycle.js +62 -0
  11. package/dist/adapters/jest/metadata.d.ts +6 -0
  12. package/dist/adapters/jest/metadata.js +35 -0
  13. package/dist/adapters/jest/reporter.cjs +10 -0
  14. package/dist/adapters/jest/reporter.d.ts +5 -0
  15. package/dist/adapters/jest/reporter.js +67 -0
  16. package/dist/adapters/jest/results.d.ts +31 -0
  17. package/dist/adapters/jest/results.js +144 -0
  18. package/dist/adapters/jest/runner-adapter.d.ts +9 -0
  19. package/dist/adapters/jest/runner-adapter.js +49 -0
  20. package/dist/adapters/jest/setup.d.ts +1 -0
  21. package/dist/adapters/jest/setup.js +10 -0
  22. package/dist/adapters/node-test/index.d.ts +8 -0
  23. package/dist/adapters/node-test/index.js +106 -0
  24. package/dist/adapters/node-test/invocation-adapter.d.ts +5 -0
  25. package/dist/adapters/node-test/invocation-adapter.js +47 -0
  26. package/dist/adapters/node-test/runner-adapter.d.ts +2 -0
  27. package/dist/adapters/node-test/runner-adapter.js +116 -0
  28. package/dist/adapters/vitest/index.d.ts +9 -0
  29. package/dist/adapters/vitest/index.js +82 -0
  30. package/dist/adapters/vitest/lifecycle.d.ts +24 -0
  31. package/dist/adapters/vitest/lifecycle.js +41 -0
  32. package/dist/adapters/vitest/reporter.d.ts +12 -0
  33. package/dist/adapters/vitest/reporter.js +75 -0
  34. package/dist/adapters/vitest/setup.d.ts +1 -0
  35. package/dist/adapters/vitest/setup.js +3 -0
  36. package/dist/agents/codex-app-server/agent.js +5 -1
  37. package/dist/agents/codex-app-server/protocol/index.js +3 -3
  38. package/dist/agents/codex-app-server/turn-completion.d.ts +1 -0
  39. package/dist/agents/codex-app-server/turn-completion.js +9 -0
  40. package/dist/commands/affected.js +22 -15
  41. package/dist/commands/run-args.d.ts +4 -3
  42. package/dist/commands/run-args.js +12 -6
  43. package/dist/commands/run-changed.d.ts +4 -4
  44. package/dist/commands/run-changed.js +45 -58
  45. package/dist/config/pathgrade.d.ts +48 -0
  46. package/dist/config/pathgrade.js +204 -0
  47. package/dist/pathgrade.d.ts +2 -2
  48. package/dist/pathgrade.js +35 -34
  49. package/dist/plugin/index.d.ts +2 -9
  50. package/dist/plugin/index.js +1 -81
  51. package/dist/plugin/lifecycle.d.ts +1 -45
  52. package/dist/plugin/lifecycle.js +1 -146
  53. package/dist/plugin/reporter.d.ts +1 -37
  54. package/dist/plugin/reporter.js +1 -280
  55. package/dist/plugin/setup.d.ts +1 -1
  56. package/dist/plugin/setup.js +1 -3
  57. package/dist/providers/copy-filter.d.ts +5 -0
  58. package/dist/providers/copy-filter.js +9 -0
  59. package/dist/providers/sandbox.js +2 -2
  60. package/dist/providers/workspace.d.ts +6 -0
  61. package/dist/providers/workspace.js +34 -14
  62. package/dist/reporters/diagnostics.d.ts +2 -40
  63. package/dist/reporters/diagnostics.js +1 -80
  64. package/dist/reporters/report-summary.d.ts +6 -0
  65. package/dist/reporters/report-summary.js +29 -0
  66. package/dist/reporting/artifacts.d.ts +2 -0
  67. package/dist/reporting/artifacts.js +20 -0
  68. package/dist/reporting/core.d.ts +2 -0
  69. package/dist/reporting/core.js +173 -0
  70. package/dist/reporting/types.d.ts +57 -0
  71. package/dist/reporting/types.js +1 -0
  72. package/dist/reporting/vitest-edge.d.ts +1 -0
  73. package/dist/reporting/vitest-edge.js +1 -0
  74. package/dist/runners/adapter-loader.d.ts +17 -0
  75. package/dist/runners/adapter-loader.js +67 -0
  76. package/dist/runners/adapter.d.ts +65 -0
  77. package/dist/runners/adapter.js +1 -0
  78. package/dist/runners/invocation.d.ts +10 -0
  79. package/dist/runners/invocation.js +1 -0
  80. package/dist/runners/lifecycle-hooks.d.ts +2 -0
  81. package/dist/runners/lifecycle-hooks.js +18 -0
  82. package/dist/runners/model-builders.d.ts +4 -0
  83. package/dist/runners/model-builders.js +67 -0
  84. package/dist/runners/model-validation.d.ts +12 -0
  85. package/dist/runners/model-validation.js +213 -0
  86. package/dist/runners/model.d.ts +117 -0
  87. package/dist/runners/model.js +1 -0
  88. package/dist/runners/orchestrator.d.ts +30 -0
  89. package/dist/runners/orchestrator.js +74 -0
  90. package/dist/runners/report-projection.d.ts +6 -0
  91. package/dist/runners/report-projection.js +62 -0
  92. package/dist/runners/selection.d.ts +6 -0
  93. package/dist/runners/selection.js +10 -0
  94. package/dist/runners/vitest-adapter.d.ts +8 -0
  95. package/dist/runners/vitest-adapter.js +108 -0
  96. package/dist/runners/vitest-invocation.d.ts +10 -0
  97. package/dist/runners/vitest-invocation.js +44 -0
  98. package/dist/runners/vitest-lifecycle.d.ts +43 -0
  99. package/dist/runners/vitest-lifecycle.js +150 -0
  100. package/dist/sdk/agent.js +15 -16
  101. package/dist/sdk/case-context.d.ts +20 -0
  102. package/dist/sdk/case-context.js +31 -0
  103. package/dist/sdk/diagnostics.d.ts +40 -0
  104. package/dist/sdk/diagnostics.js +80 -0
  105. package/dist/sdk/eval-runtime.d.ts +4 -0
  106. package/dist/sdk/eval-runtime.js +11 -1
  107. package/dist/sdk/evaluate.js +2 -1
  108. package/dist/sdk/index.d.ts +2 -0
  109. package/dist/sdk/index.js +1 -0
  110. package/dist/sdk/lifecycle.d.ts +40 -0
  111. package/dist/sdk/lifecycle.js +170 -0
  112. package/dist/sdk/result-capture.d.ts +18 -0
  113. package/dist/sdk/result-capture.js +49 -0
  114. package/dist/sdk/types.d.ts +1 -1
  115. package/dist/types.d.ts +2 -2
  116. package/dist/utils/llm-providers/anthropic.js +9 -2
  117. package/package.json +73 -36
@@ -5,6 +5,38 @@ import { createSandbox } from './sandbox.js';
5
5
  import { stageMcpConfig } from './mcp-config.js';
6
6
  import { sandboxExec } from './sandbox-exec.js';
7
7
  import { resolveCredentials } from './credentials.js';
8
+ import { isPortableCopyEntry } from './copy-filter.js';
9
+ const SANDBOX_REMOVE_RETRY_DELAYS_MS = [
10
+ 50,
11
+ 100,
12
+ 250,
13
+ 500,
14
+ 1_000,
15
+ 2_000,
16
+ 3_000,
17
+ 5_000,
18
+ ];
19
+ function isRetryableRemoveError(error) {
20
+ const code = error.code;
21
+ return code === 'ENOTEMPTY' || code === 'EBUSY' || code === 'EPERM';
22
+ }
23
+ export async function removeSandboxRoot(rootDir, opts = {}) {
24
+ const remove = opts.remove ?? ((target) => fs.remove(target));
25
+ const sleep = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
26
+ const retryDelaysMs = opts.retryDelaysMs ?? SANDBOX_REMOVE_RETRY_DELAYS_MS;
27
+ for (let attempt = 0;; attempt++) {
28
+ try {
29
+ await remove(rootDir);
30
+ return;
31
+ }
32
+ catch (error) {
33
+ if (!isRetryableRemoveError(error) || attempt >= retryDelaysMs.length) {
34
+ throw error;
35
+ }
36
+ await sleep(retryDelaysMs[attempt]);
37
+ }
38
+ }
39
+ }
8
40
  async function copyPathsFromHostHome(pathsToCopy, sandboxHomePath) {
9
41
  const realHome = os.homedir();
10
42
  for (const relPath of pathsToCopy) {
@@ -13,7 +45,7 @@ async function copyPathsFromHostHome(pathsToCopy, sandboxHomePath) {
13
45
  continue;
14
46
  const destPath = path.join(sandboxHomePath, relPath);
15
47
  await fs.ensureDir(path.dirname(destPath));
16
- await fs.copy(srcPath, destPath);
48
+ await fs.copy(srcPath, destPath, { filter: isPortableCopyEntry });
17
49
  }
18
50
  }
19
51
  export async function linkPathsFromHostHome(pathsToLink, sandboxHomePath) {
@@ -50,19 +82,7 @@ export async function prepareWorkspace(spec) {
50
82
  if (disposed)
51
83
  return;
52
84
  disposed = true;
53
- for (let attempt = 0; attempt < 4; attempt++) {
54
- try {
55
- await fs.remove(rootDir);
56
- return;
57
- }
58
- catch (error) {
59
- const code = error.code;
60
- const retryable = code === 'ENOTEMPTY' || code === 'EBUSY' || code === 'EPERM';
61
- if (!retryable || attempt === 3)
62
- throw error;
63
- await new Promise((r) => setTimeout(r, 50 * (attempt + 1)));
64
- }
65
- }
85
+ await removeSandboxRoot(rootDir);
66
86
  },
67
87
  };
68
88
  }
@@ -1,46 +1,8 @@
1
- import type { LogEntry } from '../types.js';
2
- import type { ReactionFiredEntry, TurnDetail } from '../sdk/types.js';
3
- import { type RuntimePolicyAuditEntry } from '../sdk/runtime-policy.js';
4
- export interface DiagnosticsScorer {
5
- name: string;
6
- type: string;
7
- score: number;
8
- weight: number;
9
- details?: string;
10
- status?: 'ok' | 'error' | 'skipped';
11
- }
12
- export interface DiagnosticsTurnDetail extends TurnDetail {
13
- apiRetries: number;
14
- }
15
- export type DiagnosticsRuntimePolicyEntry = RuntimePolicyAuditEntry;
16
- export interface DiagnosticsReport {
17
- turns: number;
18
- totalDurationMs: number;
19
- completionReason?: string;
20
- completionDetail?: string;
21
- score?: number;
22
- turnDetails: DiagnosticsTurnDetail[];
23
- reactionsFired: ReactionFiredEntry[];
24
- runtimePoliciesApplied: DiagnosticsRuntimePolicyEntry[];
25
- recommendedTimeoutMs: number;
26
- warnings: string[];
27
- scorers: Array<DiagnosticsScorer & {
28
- status: 'ok' | 'error' | 'skipped';
29
- }>;
30
- }
31
- export interface BuildDiagnosticsReportInput {
32
- completionReason?: string;
33
- completionDetail?: string;
34
- score?: number;
35
- turnDetails?: TurnDetail[];
36
- reactionsFired?: ReactionFiredEntry[];
37
- scorers?: DiagnosticsScorer[];
38
- log: LogEntry[];
39
- }
1
+ import type { DiagnosticsReport } from '../sdk/diagnostics.js';
2
+ export { buildDiagnosticsReport } from '../sdk/diagnostics.js';
40
3
  export interface FormatDiagnosticsOptions {
41
4
  verbose?: boolean;
42
5
  currentTimeoutMs?: number;
43
6
  }
44
- export declare function buildDiagnosticsReport(input: BuildDiagnosticsReportInput): DiagnosticsReport;
45
7
  export declare function formatDiagnosticsSummary(report: DiagnosticsReport): string;
46
8
  export declare function formatDiagnostics(report: DiagnosticsReport, opts?: FormatDiagnosticsOptions): string;
@@ -1,35 +1,4 @@
1
- import { extractRuntimePolicyAuditEntries } from '../sdk/runtime-policy.js';
2
- export function buildDiagnosticsReport(input) {
3
- const baseTurnDetails = (input.turnDetails && input.turnDetails.length > 0)
4
- ? [...input.turnDetails]
5
- : deriveTurnDetailsFromLog(input.log);
6
- const retriesByTurn = countRetriesByTurn(input.log);
7
- const turnDetails = baseTurnDetails
8
- .sort((a, b) => a.turn - b.turn)
9
- .map((detail) => ({
10
- ...detail,
11
- apiRetries: retriesByTurn.get(detail.turn) ?? 0,
12
- }));
13
- const warnings = turnDetails
14
- .filter((detail) => detail.outputLines > 500)
15
- .map((detail) => `Turn ${detail.turn}: output exceeded 500 lines (${detail.outputLines} lines)`);
16
- return {
17
- turns: turnDetails.length,
18
- totalDurationMs: turnDetails.reduce((sum, detail) => sum + detail.durationMs, 0),
19
- completionReason: input.completionReason,
20
- completionDetail: input.completionDetail,
21
- score: input.score,
22
- turnDetails,
23
- reactionsFired: input.reactionsFired ?? [],
24
- runtimePoliciesApplied: extractRuntimePolicyAuditEntries(input.log),
25
- recommendedTimeoutMs: recommendTimeoutMs(turnDetails),
26
- warnings,
27
- scorers: (input.scorers ?? []).map((scorer) => ({
28
- ...scorer,
29
- status: scorer.status ?? 'ok',
30
- })),
31
- };
32
- }
1
+ export { buildDiagnosticsReport } from '../sdk/diagnostics.js';
33
2
  export function formatDiagnosticsSummary(report) {
34
3
  const turnsLabel = report.turns === 1 ? '1 turn' : `${report.turns} turns`;
35
4
  const durationLabel = `${(report.totalDurationMs / 1000).toFixed(1)}s`;
@@ -84,54 +53,6 @@ export function formatDiagnostics(report, opts = {}) {
84
53
  lines.push('', formatTimeoutRecommendation(report.recommendedTimeoutMs, opts.currentTimeoutMs));
85
54
  return lines.join('\n');
86
55
  }
87
- function deriveTurnDetailsFromLog(log) {
88
- return log
89
- .filter((entry) => entry.type === 'agent_result'
90
- && entry.synthetic_blocked_prompt !== true
91
- && typeof entry.turn_number === 'number'
92
- && typeof entry.duration_ms === 'number'
93
- && typeof entry.output_lines === 'number'
94
- && typeof entry.output_chars === 'number')
95
- .map((entry) => ({
96
- turn: entry.turn_number,
97
- durationMs: entry.duration_ms,
98
- outputLines: entry.output_lines,
99
- outputChars: entry.output_chars,
100
- }));
101
- }
102
- function countRetriesByTurn(log) {
103
- const retriesByTurn = new Map();
104
- let pendingRetries = 0;
105
- for (const entry of log) {
106
- if (isRetryEntry(entry)) {
107
- pendingRetries++;
108
- continue;
109
- }
110
- if (entry.type === 'agent_result' && typeof entry.turn_number === 'number') {
111
- if (pendingRetries > 0) {
112
- retriesByTurn.set(entry.turn_number, (retriesByTurn.get(entry.turn_number) ?? 0) + pendingRetries);
113
- pendingRetries = 0;
114
- }
115
- if (!retriesByTurn.has(entry.turn_number)) {
116
- retriesByTurn.set(entry.turn_number, 0);
117
- }
118
- }
119
- }
120
- return retriesByTurn;
121
- }
122
- function isRetryEntry(entry) {
123
- if (entry.type !== 'agent_result')
124
- return false;
125
- const text = `${entry.assistant_message ?? ''} ${entry.output ?? ''}`.toLowerCase();
126
- return text.includes('retry') || text.includes('api_retry');
127
- }
128
- function recommendTimeoutMs(turnDetails) {
129
- if (turnDetails.length <= 1) {
130
- return (turnDetails[0]?.durationMs ?? 0) + 30_000;
131
- }
132
- const totalDurationMs = turnDetails.reduce((sum, detail) => sum + detail.durationMs, 0);
133
- return Math.max(totalDurationMs + 200_000, 30_000);
134
- }
135
56
  function formatTimeoutRecommendation(recommendedTimeoutMs, currentTimeoutMs) {
136
57
  const recommendedSeconds = Math.round(recommendedTimeoutMs / 1000);
137
58
  if (currentTimeoutMs == null) {
@@ -0,0 +1,6 @@
1
+ import type { ReportSummaryGroup } from '../reporting/types.js';
2
+ export interface PrintReportSummaryOptions {
3
+ forceVerbose?: boolean;
4
+ currentTimeoutMs?: number;
5
+ }
6
+ export declare function printReportSummary(groups: ReportSummaryGroup[], opts?: PrintReportSummaryOptions): void;
@@ -0,0 +1,29 @@
1
+ import { fmt } from '../utils/cli.js';
2
+ import { formatDiagnostics, formatDiagnosticsSummary } from './diagnostics.js';
3
+ export function printReportSummary(groups, opts = {}) {
4
+ console.log(`\n${fmt.bold('── pathgrade summary ')}${fmt.dim('─'.repeat(40))}\n`);
5
+ for (const group of groups) {
6
+ const prColor = group.pass_rate >= 0.5 ? fmt.green : fmt.red;
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)}%`);
11
+ 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}`);
13
+ console.log();
14
+ for (const diagnostic of group.diagnostics) {
15
+ const shouldPrintFull = opts.forceVerbose
16
+ || diagnostic.state !== 'passed'
17
+ || diagnostic.report.completionReason === 'timeout'
18
+ || diagnostic.report.completionReason === 'agent_crashed';
19
+ console.log(` ${fmt.bold(diagnostic.caseName)}`);
20
+ const formatted = shouldPrintFull
21
+ ? formatDiagnostics(diagnostic.report, { verbose: true, currentTimeoutMs: opts.currentTimeoutMs })
22
+ : formatDiagnosticsSummary(diagnostic.report);
23
+ for (const line of formatted.split('\n')) {
24
+ console.log(` ${line}`);
25
+ }
26
+ console.log();
27
+ }
28
+ }
29
+ }
@@ -0,0 +1,2 @@
1
+ import type { ArtifactWriteResult, PathgradeReportBuildResult } from './types.js';
2
+ export declare function writePathgradeArtifacts(artifactRoot: string, built: PathgradeReportBuildResult): Promise<ArtifactWriteResult>;
@@ -0,0 +1,20 @@
1
+ import path from 'node:path';
2
+ import fs from 'fs-extra';
3
+ export async function writePathgradeArtifacts(artifactRoot, built) {
4
+ await fs.ensureDir(path.join(artifactRoot, 'traces'));
5
+ const gitignorePath = path.join(artifactRoot, '.gitignore');
6
+ if (!(await fs.pathExists(gitignorePath))) {
7
+ await fs.writeFile(gitignorePath, '*\n');
8
+ }
9
+ const traceFiles = [];
10
+ for (const trace of built.traces) {
11
+ await fs.writeJson(path.join(artifactRoot, trace.traceFile), trace.trials, { spaces: 2 });
12
+ traceFiles.push(trace.traceFile);
13
+ }
14
+ const resultsPath = path.join(artifactRoot, 'results.json');
15
+ await fs.writeJson(resultsPath, built.report, { spaces: 2 });
16
+ return {
17
+ resultsPath,
18
+ traceFiles,
19
+ };
20
+ }
@@ -0,0 +1,2 @@
1
+ import type { PathgradeReportBuildResult, ReportRunInput } from './types.js';
2
+ export declare function buildPathgradeReport(input: ReportRunInput): PathgradeReportBuildResult;
@@ -0,0 +1,173 @@
1
+ import { extractSkillsFromLog } from '../tool-events.js';
2
+ export function buildPathgradeReport(input) {
3
+ const warnings = [];
4
+ const consolidatedGroups = [];
5
+ const traces = [];
6
+ const summaries = [];
7
+ const builtGroups = input.groups.map(group => ({
8
+ groupName: group.groupName,
9
+ cases: group.cases.map(toBuiltCase),
10
+ }));
11
+ for (const group of builtGroups) {
12
+ for (const testCase of group.cases) {
13
+ warnings.push(...testCase.warnings);
14
+ }
15
+ }
16
+ const reportableGroups = builtGroups
17
+ .map(group => ({
18
+ ...group,
19
+ cases: group.cases.filter(reportableBuiltCase),
20
+ }))
21
+ .filter(group => group.cases.length > 0);
22
+ for (const group of reportableGroups) {
23
+ const report = buildEvalReport(group.groupName, group.cases);
24
+ summaries.push(buildSummary(group.groupName, group.cases, report));
25
+ const traceFile = `traces/${slug(group.groupName)}.json`;
26
+ traces.push({ traceFile, trials: report.trials });
27
+ const strippedTrials = report.trials.map(trial => {
28
+ const { session_log, conversation, ...rest } = trial;
29
+ return rest;
30
+ });
31
+ const { trials: _trials, ...rest } = report;
32
+ consolidatedGroups.push({
33
+ ...rest,
34
+ trials: strippedTrials,
35
+ trace_file: traceFile,
36
+ });
37
+ }
38
+ const scores = reportableGroups.flatMap(group => group.cases.map(testCase => testCase.score));
39
+ const overallPassRate = average(scores);
40
+ const status = input.threshold != null
41
+ ? (overallPassRate >= input.threshold ? 'pass' : 'fail')
42
+ : (reportableGroups.every(group => group.cases.every(testCase => testCase.state === 'passed')) ? 'pass' : 'fail');
43
+ return {
44
+ report: {
45
+ version: 1,
46
+ timestamp: new Date().toISOString(),
47
+ ...(input.threshold != null ? { threshold: input.threshold } : {}),
48
+ overall_pass_rate: overallPassRate,
49
+ status,
50
+ groups: consolidatedGroups,
51
+ ...(input.selection ? { selection: input.selection } : {}),
52
+ },
53
+ traces,
54
+ summaries,
55
+ warnings,
56
+ };
57
+ }
58
+ function reportableBuiltCase(testCase) {
59
+ if (testCase.reportable !== undefined)
60
+ return testCase.reportable;
61
+ return testCase.state !== 'skipped' && testCase.state !== 'pending';
62
+ }
63
+ function toBuiltCase(testCase) {
64
+ if (testCase.evaluations === undefined) {
65
+ return fallbackCase(testCase, []);
66
+ }
67
+ if (testCase.evaluations.length === 0) {
68
+ return fallbackCase(testCase, [
69
+ `empty results for "${testCase.name}" — evaluate() may not have been called`,
70
+ ]);
71
+ }
72
+ const evaluation = testCase.evaluations[testCase.evaluations.length - 1];
73
+ const diagnostics = evaluation.diagnostics ?? testCase.diagnostics;
74
+ return {
75
+ name: testCase.name,
76
+ state: testCase.state,
77
+ score: evaluation.score,
78
+ runnerDurationMs: testCase.runnerDurationMs,
79
+ diagnostics,
80
+ reportable: testCase.reportable,
81
+ trial: normalizeTrial({
82
+ name: testCase.name,
83
+ score: evaluation.score,
84
+ runnerDurationMs: testCase.runnerDurationMs,
85
+ trial: evaluation.trial,
86
+ diagnostics,
87
+ }),
88
+ warnings: [],
89
+ };
90
+ }
91
+ function fallbackCase(testCase, warnings) {
92
+ const score = testCase.state === 'passed' ? 1 : 0;
93
+ return {
94
+ name: testCase.name,
95
+ state: testCase.state,
96
+ score,
97
+ runnerDurationMs: testCase.runnerDurationMs,
98
+ diagnostics: testCase.diagnostics,
99
+ reportable: testCase.reportable,
100
+ trial: normalizeTrial({
101
+ name: testCase.name,
102
+ score,
103
+ runnerDurationMs: testCase.runnerDurationMs,
104
+ diagnostics: testCase.diagnostics,
105
+ }),
106
+ warnings,
107
+ };
108
+ }
109
+ function normalizeTrial(input) {
110
+ const base = input.trial ?? {
111
+ trial_id: 1,
112
+ reward: input.score,
113
+ scorer_results: [],
114
+ duration_ms: input.runnerDurationMs,
115
+ n_commands: 0,
116
+ input_tokens: 0,
117
+ output_tokens: 0,
118
+ session_log: [],
119
+ };
120
+ const skills = base.skills_used ?? extractSkillsFromLog(base.session_log);
121
+ return {
122
+ ...base,
123
+ name: input.name,
124
+ duration_ms: base.duration_ms || input.runnerDurationMs,
125
+ diagnostics: input.diagnostics ?? base.diagnostics,
126
+ ...(skills.length > 0 ? { skills_used: skills } : {}),
127
+ };
128
+ }
129
+ function buildEvalReport(groupName, cases) {
130
+ const trials = cases.map((testCase, index) => ({
131
+ ...testCase.trial,
132
+ trial_id: index + 1,
133
+ }));
134
+ const passRate = average(cases.map(testCase => testCase.state === 'passed' ? 1 : 0));
135
+ const skills = new Set();
136
+ for (const trial of trials) {
137
+ for (const skill of trial.skills_used ?? []) {
138
+ skills.add(skill);
139
+ }
140
+ }
141
+ return {
142
+ task: groupName,
143
+ pass_rate: passRate,
144
+ pass_at_k: 1 - Math.pow(1 - passRate, trials.length),
145
+ pass_pow_k: Math.pow(passRate, trials.length),
146
+ trials,
147
+ skills_used: [...skills],
148
+ };
149
+ }
150
+ function buildSummary(groupName, cases, report) {
151
+ return {
152
+ task: groupName,
153
+ pass_rate: report.pass_rate,
154
+ pass_at_k: report.pass_at_k,
155
+ pass_pow_k: report.pass_pow_k,
156
+ average_duration_ms: average(cases.map(testCase => testCase.runnerDurationMs)),
157
+ trial_count: cases.length,
158
+ diagnostics: cases.flatMap(testCase => testCase.diagnostics
159
+ ? [{ caseName: testCase.name, state: testCase.state, report: testCase.diagnostics }]
160
+ : []),
161
+ };
162
+ }
163
+ function average(values) {
164
+ if (values.length === 0)
165
+ return 0;
166
+ return values.reduce((sum, value) => sum + value, 0) / values.length;
167
+ }
168
+ function slug(value) {
169
+ return value
170
+ .toLowerCase()
171
+ .replace(/[^a-z0-9]+/g, '-')
172
+ .replace(/^-+|-+$/g, '') || 'report';
173
+ }
@@ -0,0 +1,57 @@
1
+ import type { DiagnosticsReport } from '../sdk/diagnostics.js';
2
+ import type { PathgradeReport, PathgradeSelectionReport, TrialResult } from '../types.js';
3
+ export type ReportCaseState = 'passed' | 'failed' | 'skipped' | 'pending';
4
+ export interface ReportRunInput {
5
+ threshold?: number;
6
+ selection?: PathgradeSelectionReport;
7
+ groups: ReportGroupInput[];
8
+ }
9
+ export interface ReportGroupInput {
10
+ groupName: string;
11
+ cases: ReportCaseInput[];
12
+ }
13
+ export interface ReportCaseInput {
14
+ caseId?: string;
15
+ name: string;
16
+ state: ReportCaseState;
17
+ runnerDurationMs: number;
18
+ sourceRef?: string;
19
+ filePath?: string;
20
+ groupName?: string;
21
+ runnerCaseId?: string;
22
+ reportable?: boolean;
23
+ evaluations?: readonly ReportEvaluationInput[];
24
+ diagnostics?: DiagnosticsReport;
25
+ }
26
+ export interface ReportEvaluationInput {
27
+ score: number;
28
+ trial?: TrialResult;
29
+ diagnostics?: DiagnosticsReport;
30
+ }
31
+ export interface PathgradeReportBuildResult {
32
+ report: PathgradeReport;
33
+ traces: Array<{
34
+ traceFile: string;
35
+ trials: TrialResult[];
36
+ }>;
37
+ summaries: ReportSummaryGroup[];
38
+ warnings: string[];
39
+ }
40
+ export interface ArtifactWriteResult {
41
+ resultsPath: string;
42
+ traceFiles: string[];
43
+ }
44
+ export interface ReportSummaryGroup {
45
+ task: string;
46
+ pass_rate: number;
47
+ pass_at_k: number;
48
+ pass_pow_k: number;
49
+ average_duration_ms: number;
50
+ trial_count: number;
51
+ diagnostics: ReportSummaryDiagnostics[];
52
+ }
53
+ export interface ReportSummaryDiagnostics {
54
+ caseName: string;
55
+ state: ReportCaseState;
56
+ report: DiagnosticsReport;
57
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export { collectVitestReportGroups } from '../runners/vitest-adapter.js';
@@ -0,0 +1 @@
1
+ export { collectVitestReportGroups } from '../runners/vitest-adapter.js';
@@ -0,0 +1,17 @@
1
+ import type { ResolvedPathgradeConfig } from '../config/pathgrade.js';
2
+ import { type SpawnVitest } from './vitest-invocation.js';
3
+ import type { RunnerAdapter } from './adapter.js';
4
+ import type { RunnerInvocationAdapter } from './invocation.js';
5
+ export interface ExternalAdapterFactoryInput {
6
+ config: ResolvedPathgradeConfig;
7
+ }
8
+ export declare function loadRunnerAdapter(input: {
9
+ adapterName?: string;
10
+ cwd?: string;
11
+ }): Promise<RunnerAdapter>;
12
+ export declare function loadRunnerInvocationAdapter(input: {
13
+ adapterName?: string;
14
+ cwd?: string;
15
+ config: ResolvedPathgradeConfig;
16
+ spawnVitest?: SpawnVitest;
17
+ }): Promise<RunnerInvocationAdapter>;
@@ -0,0 +1,67 @@
1
+ import { createRequire } from 'node:module';
2
+ import path from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
4
+ import { createJestInvocationAdapter } from '../adapters/jest/invocation-adapter.js';
5
+ import { createJestAdapter } from '../adapters/jest/runner-adapter.js';
6
+ import { createNodeTestInvocationAdapter } from '../adapters/node-test/invocation-adapter.js';
7
+ import { createVitestInvocationAdapter } from './vitest-invocation.js';
8
+ import { resolveRunnerAdapter } from './selection.js';
9
+ export async function loadRunnerAdapter(input) {
10
+ const name = input.adapterName ?? 'vitest';
11
+ if (name === 'vitest' || name === 'node-test') {
12
+ return resolveRunnerAdapter({ adapterName: name });
13
+ }
14
+ if (name === 'jest')
15
+ return createJestAdapter();
16
+ const mod = await importExternalAdapter({
17
+ adapterName: name,
18
+ cwd: input.cwd ?? process.cwd(),
19
+ });
20
+ if (typeof mod.createPathgradeAdapter !== 'function') {
21
+ throw new Error(`Pathgrade adapter package "${adapterPackageSpecifier(name)}" must export createPathgradeAdapter().`);
22
+ }
23
+ return mod.createPathgradeAdapter();
24
+ }
25
+ export async function loadRunnerInvocationAdapter(input) {
26
+ const name = input.adapterName ?? 'vitest';
27
+ if (name === 'vitest')
28
+ return createVitestInvocationAdapter({ spawnVitest: input.spawnVitest });
29
+ if (name === 'node-test')
30
+ return createNodeTestInvocationAdapter({ config: input.config });
31
+ if (name === 'jest')
32
+ return createJestInvocationAdapter({ config: input.config });
33
+ const mod = await importExternalAdapter({
34
+ adapterName: name,
35
+ cwd: input.cwd ?? process.cwd(),
36
+ });
37
+ if (typeof mod.createPathgradeInvocationAdapter !== 'function') {
38
+ throw new Error(`Pathgrade adapter package "${adapterPackageSpecifier(name)}" must export createPathgradeInvocationAdapter().`);
39
+ }
40
+ return mod.createPathgradeInvocationAdapter({ config: input.config });
41
+ }
42
+ function adapterPackageSpecifier(adapterName) {
43
+ if (adapterName.startsWith('.') || path.isAbsolute(adapterName) || adapterName.includes('/')) {
44
+ return adapterName;
45
+ }
46
+ return `@wix/pathgrade-adapter-${adapterName}`;
47
+ }
48
+ function resolveAdapterSpecifier(input) {
49
+ if (input.adapterName.startsWith('.') || path.isAbsolute(input.adapterName)) {
50
+ return path.resolve(input.cwd, input.adapterName);
51
+ }
52
+ const specifier = adapterPackageSpecifier(input.adapterName);
53
+ return createRequire(path.join(input.cwd, 'package.json')).resolve(specifier);
54
+ }
55
+ async function importExternalAdapter(input) {
56
+ try {
57
+ const resolved = resolveAdapterSpecifier(input);
58
+ return await import(pathToFileURL(resolved).href);
59
+ }
60
+ catch (err) {
61
+ throw new Error(`Unsupported Pathgrade runner adapter: ${input.adapterName}. ` +
62
+ `Tried to load ${adapterPackageSpecifier(input.adapterName)} from ${input.cwd}: ${errMsg(err)}`);
63
+ }
64
+ }
65
+ function errMsg(err) {
66
+ return err instanceof Error ? err.message : String(err);
67
+ }
@@ -0,0 +1,65 @@
1
+ import type { PathgradeSelectionReport } from '../types.js';
2
+ import type { Agent, PathgradeTestMeta, RecordedEvalResult } from '../sdk/types.js';
3
+ import type { CaseContextScope } from '../sdk/case-context.js';
4
+ import type { NormalizedRunSnapshot } from './model.js';
5
+ export declare const runnerAdapterContractVersion = 1;
6
+ export interface RunnerAdapter {
7
+ readonly name: string;
8
+ discover(input: AdapterDiscoveryInput): Promise<AdapterDiscoveryResult>;
9
+ invoke(input: AdapterInvocationInput): Promise<AdapterRunHandle>;
10
+ collectNormalizedRunSnapshot(run: AdapterRunHandle): Promise<NormalizedRunSnapshot>;
11
+ }
12
+ export interface AdapterDiscoveryInput {
13
+ cwd: string;
14
+ include?: string[];
15
+ exclude?: string[];
16
+ selection?: PathgradeSelectionReport;
17
+ }
18
+ export interface AdapterDiscoveryResult {
19
+ units: AdapterEvalUnit[];
20
+ diagnostics?: AdapterDiagnostic[];
21
+ }
22
+ export interface AdapterEvalUnit {
23
+ id: string;
24
+ displayName: string;
25
+ sourceRef?: string;
26
+ native?: unknown;
27
+ }
28
+ export interface AdapterDiagnostic {
29
+ severity: 'info' | 'warning' | 'error';
30
+ message: string;
31
+ sourceRef?: string;
32
+ }
33
+ export interface AdapterInvocationInput {
34
+ discovered: AdapterDiscoveryResult;
35
+ argv: string[];
36
+ env: NodeJS.ProcessEnv;
37
+ lifecycle: AdapterLifecycleHooks;
38
+ signal?: AbortSignal;
39
+ }
40
+ export interface AdapterRunHandle {
41
+ adapterName: string;
42
+ status: AdapterRunStatus;
43
+ exitCode: number;
44
+ native?: unknown;
45
+ diagnostics?: AdapterDiagnostic[];
46
+ }
47
+ export type AdapterRunStatus = 'completed' | 'failed' | 'cancelled' | 'timed_out' | 'parked';
48
+ export interface AdapterLifecycleHooks {
49
+ onResult(event: EvalResultEvent): void;
50
+ withCaseContext<T>(context: AdapterCaseContext, run: () => T): T;
51
+ flushCase(caseId: string): Promise<PathgradeTestMeta[]>;
52
+ cleanupRun(): Promise<void>;
53
+ }
54
+ export interface AdapterCaseContext {
55
+ caseId: string;
56
+ caseName: string;
57
+ filePath?: string;
58
+ sourceRef?: string;
59
+ runnerNativeId?: string;
60
+ scope: CaseContextScope;
61
+ }
62
+ export interface EvalResultEvent {
63
+ result: RecordedEvalResult;
64
+ agent: Agent;
65
+ }
@@ -0,0 +1 @@
1
+ export const runnerAdapterContractVersion = 1;