@wix/pathgrade 1.0.26 → 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.
- package/README.md +14 -21
- package/dist/adapters/jest/invocation-adapter.js +5 -1
- package/dist/adapters/jest/reporter.js +4 -1
- package/dist/adapters/jest/results.js +8 -0
- package/dist/adapters/node-test/index.d.ts +5 -0
- package/dist/adapters/node-test/index.js +35 -7
- package/dist/adapters/node-test/invocation-adapter.js +3 -1
- package/dist/adapters/node-test/runner-adapter.js +22 -15
- package/dist/adapters/vitest/reporter.js +4 -1
- package/dist/agents/claude/sdk-message-projector.js +5 -0
- package/dist/agents/codex-app-server/agent.js +7 -57
- package/dist/agents/codex-app-server/turn-notifications.d.ts +6 -0
- package/dist/agents/codex-app-server/turn-notifications.js +51 -0
- package/dist/agents/codex-app-server/turn-state.d.ts +19 -0
- package/dist/agents/codex-app-server/turn-state.js +1 -0
- package/dist/agents/codex.js +1 -0
- package/dist/agents/cursor.js +1 -0
- package/dist/agents/opencode/protocol.d.ts +7 -0
- package/dist/agents/opencode/protocol.js +47 -0
- package/dist/agents/opencode.js +5 -49
- package/dist/analytics/engine.js +5 -2
- package/dist/commands/report.d.ts +10 -2
- package/dist/commands/report.js +41 -6
- package/dist/commands/run-args.d.ts +1 -0
- package/dist/commands/run-args.js +13 -0
- package/dist/commands/run-changed.js +5 -15
- package/dist/config/pathgrade.d.ts +3 -0
- package/dist/config/pathgrade.js +33 -1
- package/dist/pathgrade.js +32 -3
- package/dist/reporters/cli.js +13 -6
- package/dist/reporters/github-comment.d.ts +12 -3
- package/dist/reporters/github-comment.js +92 -18
- package/dist/reporters/loader.d.ts +1 -0
- package/dist/reporters/loader.js +27 -2
- package/dist/reporters/report-summary.js +13 -5
- package/dist/reporting/artifacts.js +5 -2
- package/dist/reporting/core.d.ts +1 -0
- package/dist/reporting/core.js +183 -105
- package/dist/reporting/types.d.ts +19 -3
- package/dist/runners/adapter-loader.js +17 -12
- package/dist/runners/direct-reporter-attempts.d.ts +1 -0
- package/dist/runners/direct-reporter-attempts.js +7 -0
- package/dist/runners/invocation.d.ts +2 -0
- package/dist/runners/model-builders.js +1 -0
- package/dist/runners/model-validation.js +31 -0
- package/dist/runners/model.d.ts +4 -0
- package/dist/runners/orchestrator.d.ts +2 -0
- package/dist/runners/orchestrator.js +11 -1
- package/dist/runners/repeated-attempts.d.ts +7 -0
- package/dist/runners/repeated-attempts.js +149 -0
- package/dist/runners/repeated-invocation.d.ts +7 -0
- package/dist/runners/repeated-invocation.js +129 -0
- package/dist/runners/report-projection.js +16 -6
- package/dist/runners/vitest-adapter.js +10 -0
- package/dist/runners/vitest-invocation.js +2 -0
- package/dist/sdk/agent-runtime-options.d.ts +12 -0
- package/dist/sdk/agent-runtime-options.js +67 -0
- package/dist/sdk/agent.js +11 -59
- package/dist/sdk/case-context.js +7 -2
- package/dist/sdk/evaluate.d.ts +2 -0
- package/dist/sdk/evaluate.js +14 -9
- package/dist/sdk/index.d.ts +2 -0
- package/dist/sdk/index.js +2 -0
- package/dist/sdk/lifecycle.js +16 -6
- package/dist/sdk/result-capture.js +4 -1
- package/dist/sdk/types.d.ts +2 -0
- package/dist/tool-event-results.d.ts +1 -1
- package/dist/tool-event-results.js +2 -1
- package/dist/tool-events.d.ts +5 -0
- package/dist/tool-events.js +5 -0
- package/dist/types.d.ts +34 -5
- package/dist/viewer.html +19 -19
- package/package.json +2 -2
package/dist/reporting/core.js
CHANGED
|
@@ -5,23 +5,25 @@ export function buildPathgradeReport(input) {
|
|
|
5
5
|
const consolidatedGroups = [];
|
|
6
6
|
const traces = [];
|
|
7
7
|
const summaries = [];
|
|
8
|
+
const attemptsRequested = input.attemptsRequested ?? 1;
|
|
9
|
+
const attemptsCompleted = input.attemptsCompleted ?? attemptsRequested;
|
|
8
10
|
const builtGroups = input.groups.map(group => ({
|
|
9
11
|
groupName: group.groupName,
|
|
10
12
|
cases: group.cases.map(toBuiltCase),
|
|
11
13
|
}));
|
|
12
14
|
for (const group of builtGroups) {
|
|
13
|
-
for (const testCase of group.cases)
|
|
15
|
+
for (const testCase of group.cases)
|
|
14
16
|
warnings.push(...testCase.warnings);
|
|
15
|
-
}
|
|
16
17
|
}
|
|
17
18
|
const reportableGroups = builtGroups
|
|
18
|
-
.map(group => ({
|
|
19
|
-
...group,
|
|
20
|
-
cases: group.cases.filter(reportableBuiltCase),
|
|
21
|
-
}))
|
|
19
|
+
.map(group => ({ ...group, cases: group.cases.filter(reportableBuiltCase) }))
|
|
22
20
|
.filter(group => group.cases.length > 0);
|
|
23
21
|
for (const group of reportableGroups) {
|
|
24
|
-
const report = buildEvalReport(group.groupName, group.cases
|
|
22
|
+
const report = buildEvalReport(group.groupName, group.cases, {
|
|
23
|
+
attemptsRequested,
|
|
24
|
+
attemptsCompleted,
|
|
25
|
+
threshold: input.threshold,
|
|
26
|
+
});
|
|
25
27
|
summaries.push(buildSummary(group.groupName, group.cases, report));
|
|
26
28
|
const traceFile = `traces/${slug(group.groupName)}.json`;
|
|
27
29
|
traces.push({ traceFile, trials: report.trials });
|
|
@@ -30,26 +32,25 @@ export function buildPathgradeReport(input) {
|
|
|
30
32
|
return rest;
|
|
31
33
|
});
|
|
32
34
|
const { trials: _trials, ...rest } = report;
|
|
33
|
-
consolidatedGroups.push({
|
|
34
|
-
...rest,
|
|
35
|
-
trials: strippedTrials,
|
|
36
|
-
trace_file: traceFile,
|
|
37
|
-
});
|
|
35
|
+
consolidatedGroups.push({ ...rest, trials: strippedTrials, trace_file: traceFile });
|
|
38
36
|
}
|
|
39
|
-
const scores = reportableGroups.flatMap(group => group.cases
|
|
40
|
-
.filter(
|
|
41
|
-
.map(
|
|
42
|
-
.filter((score) => score !== undefined));
|
|
43
|
-
const
|
|
37
|
+
const scores = reportableGroups.flatMap(group => group.cases.flatMap(testCase => (testCase.attempts
|
|
38
|
+
.filter(attempt => attempt.resultKind !== 'synthetic_no_evaluation')
|
|
39
|
+
.map(attempt => attempt.score)
|
|
40
|
+
.filter((score) => score !== undefined))));
|
|
41
|
+
const overallMeanReward = average(scores);
|
|
44
42
|
const status = input.threshold != null
|
|
45
|
-
? (
|
|
43
|
+
? (overallMeanReward >= input.threshold ? 'pass' : 'fail')
|
|
46
44
|
: (reportableGroups.every(group => group.cases.every(testCase => testCase.state === 'passed')) ? 'pass' : 'fail');
|
|
47
45
|
return {
|
|
48
46
|
report: {
|
|
49
|
-
version:
|
|
47
|
+
version: 2,
|
|
50
48
|
timestamp: new Date().toISOString(),
|
|
51
49
|
...(input.threshold != null ? { threshold: input.threshold } : {}),
|
|
52
|
-
|
|
50
|
+
attempts_requested: attemptsRequested,
|
|
51
|
+
attempts_completed: attemptsCompleted,
|
|
52
|
+
overall_mean_reward: overallMeanReward,
|
|
53
|
+
overall_pass_rate: overallMeanReward,
|
|
53
54
|
status,
|
|
54
55
|
groups: consolidatedGroups,
|
|
55
56
|
...(input.selection ? { selection: input.selection } : {}),
|
|
@@ -65,139 +66,216 @@ function reportableBuiltCase(testCase) {
|
|
|
65
66
|
return testCase.state !== 'skipped' && testCase.state !== 'pending';
|
|
66
67
|
}
|
|
67
68
|
function toBuiltCase(testCase) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
69
|
+
const caseId = testCase.caseId ?? `case:${slug(testCase.name)}`;
|
|
70
|
+
const warnings = [];
|
|
71
|
+
const attempts = testCase.attempts?.map(attempt => buildAttempt({ testCase, caseId, attempt, warnings }))
|
|
72
|
+
?? [buildAttempt({
|
|
73
|
+
testCase,
|
|
74
|
+
caseId,
|
|
75
|
+
attempt: {
|
|
76
|
+
attemptId: `${caseId}:attempt-1`,
|
|
77
|
+
attemptIndex: 1,
|
|
78
|
+
outcome: outcomeForState(testCase.state),
|
|
79
|
+
runnerDurationMs: testCase.runnerDurationMs,
|
|
80
|
+
...(testCase.evaluations !== undefined ? { evaluations: testCase.evaluations } : {}),
|
|
81
|
+
...(testCase.diagnostics ? { diagnostics: testCase.diagnostics } : {}),
|
|
82
|
+
},
|
|
83
|
+
warnings,
|
|
84
|
+
})];
|
|
85
|
+
return { caseId, name: testCase.name, state: testCase.state, attempts, reportable: testCase.reportable, warnings };
|
|
86
|
+
}
|
|
87
|
+
function buildAttempt(input) {
|
|
88
|
+
const { testCase, caseId, attempt } = input;
|
|
89
|
+
const evaluations = attempt.evaluations;
|
|
90
|
+
if (evaluations?.length === 0) {
|
|
91
|
+
input.warnings.push(`empty results for "${testCase.name}" — evaluate() may not have been called`);
|
|
75
92
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const evaluation = testCase.evaluations.findLast(candidate => candidate.resultKind !== 'synthetic_no_evaluation') ?? testCase.evaluations[testCase.evaluations.length - 1];
|
|
80
|
-
const diagnostics = evaluation.diagnostics
|
|
93
|
+
const evaluation = terminalEvaluation(evaluations);
|
|
94
|
+
const diagnostics = evaluation?.diagnostics
|
|
95
|
+
?? attempt.diagnostics
|
|
81
96
|
?? testCase.diagnostics
|
|
82
|
-
?? (evaluation
|
|
83
|
-
? buildDiagnosticsReport({
|
|
84
|
-
score: undefined,
|
|
85
|
-
warnings: ['evaluate() was not called; no evaluation score is available'],
|
|
86
|
-
log: [],
|
|
87
|
-
})
|
|
97
|
+
?? (evaluation?.resultKind === 'synthetic_no_evaluation'
|
|
98
|
+
? buildDiagnosticsReport({ score: undefined, warnings: ['evaluate() was not called; no evaluation score is available'], log: [] })
|
|
88
99
|
: undefined);
|
|
100
|
+
const fallbackScore = attempt.outcome.kind === 'not-run'
|
|
101
|
+
? undefined
|
|
102
|
+
: attempt.outcome.kind === 'passed' ? 1 : 0;
|
|
103
|
+
const score = evaluation?.score ?? fallbackScore;
|
|
89
104
|
return {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
105
|
+
caseId,
|
|
106
|
+
attemptId: attempt.attemptId,
|
|
107
|
+
attemptIndex: attempt.attemptIndex,
|
|
108
|
+
outcome: attempt.outcome,
|
|
109
|
+
score: evaluation?.resultKind === 'synthetic_no_evaluation' ? undefined : score,
|
|
110
|
+
runnerDurationMs: attempt.runnerDurationMs,
|
|
94
111
|
diagnostics,
|
|
95
|
-
|
|
96
|
-
resultKind: evaluation
|
|
112
|
+
hasTerminalEvaluation: evaluation !== undefined && evaluation.resultKind !== 'synthetic_no_evaluation',
|
|
113
|
+
resultKind: evaluation?.resultKind,
|
|
97
114
|
trial: normalizeTrial({
|
|
115
|
+
caseId,
|
|
116
|
+
attemptId: attempt.attemptId,
|
|
117
|
+
attemptIndex: attempt.attemptIndex,
|
|
98
118
|
name: testCase.name,
|
|
99
|
-
score
|
|
100
|
-
runnerDurationMs:
|
|
101
|
-
trial: evaluation
|
|
119
|
+
score,
|
|
120
|
+
runnerDurationMs: attempt.runnerDurationMs,
|
|
121
|
+
trial: evaluation?.trial,
|
|
102
122
|
diagnostics,
|
|
103
|
-
resultKind: evaluation
|
|
104
|
-
scoringDurationMs: evaluation
|
|
105
|
-
recordedAt: evaluation
|
|
106
|
-
agent: evaluation
|
|
123
|
+
resultKind: evaluation?.resultKind,
|
|
124
|
+
scoringDurationMs: evaluation?.scoringDurationMs,
|
|
125
|
+
recordedAt: evaluation?.recordedAt,
|
|
126
|
+
agent: evaluation?.agent,
|
|
127
|
+
runnerOutcome: attempt.outcome.kind,
|
|
107
128
|
}),
|
|
108
|
-
warnings: [],
|
|
109
129
|
};
|
|
110
130
|
}
|
|
111
|
-
function
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
score,
|
|
117
|
-
runnerDurationMs: testCase.runnerDurationMs,
|
|
118
|
-
diagnostics: testCase.diagnostics,
|
|
119
|
-
reportable: testCase.reportable,
|
|
120
|
-
trial: normalizeTrial({
|
|
121
|
-
name: testCase.name,
|
|
122
|
-
score,
|
|
123
|
-
runnerDurationMs: testCase.runnerDurationMs,
|
|
124
|
-
diagnostics: testCase.diagnostics,
|
|
125
|
-
}),
|
|
126
|
-
warnings,
|
|
127
|
-
};
|
|
131
|
+
function terminalEvaluation(evaluations) {
|
|
132
|
+
if (!evaluations || evaluations.length === 0)
|
|
133
|
+
return undefined;
|
|
134
|
+
return evaluations.findLast(candidate => candidate.resultKind !== 'synthetic_no_evaluation')
|
|
135
|
+
?? evaluations[evaluations.length - 1];
|
|
128
136
|
}
|
|
129
137
|
function normalizeTrial(input) {
|
|
130
138
|
const base = input.trial ?? {
|
|
131
|
-
trial_id:
|
|
139
|
+
trial_id: input.attemptIndex,
|
|
132
140
|
...(input.score !== undefined ? { reward: input.score } : {}),
|
|
133
|
-
scorer_results: [],
|
|
134
|
-
|
|
135
|
-
n_commands: 0,
|
|
136
|
-
input_tokens: 0,
|
|
137
|
-
output_tokens: 0,
|
|
138
|
-
session_log: [],
|
|
141
|
+
scorer_results: [], duration_ms: input.runnerDurationMs, n_commands: 0,
|
|
142
|
+
input_tokens: 0, output_tokens: 0, session_log: [],
|
|
139
143
|
};
|
|
140
144
|
const skills = base.skills_used ?? extractSkillsFromLog(base.session_log);
|
|
141
145
|
const { reward: existingReward, ...baseWithoutReward } = base;
|
|
142
146
|
return {
|
|
143
147
|
...baseWithoutReward,
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
148
|
+
trial_id: input.attemptIndex,
|
|
149
|
+
case_id: input.caseId,
|
|
150
|
+
attempt_id: input.attemptId,
|
|
151
|
+
attempt_index: input.attemptIndex,
|
|
152
|
+
runner_outcome: input.runnerOutcome,
|
|
153
|
+
...(input.resultKind === 'synthetic_no_evaluation' ? {} : { reward: existingReward ?? input.score }),
|
|
147
154
|
name: input.name,
|
|
148
155
|
duration_ms: base.duration_ms || input.runnerDurationMs,
|
|
149
156
|
diagnostics: input.diagnostics ?? base.diagnostics,
|
|
150
157
|
...(input.resultKind ? { result_kind: input.resultKind } : {}),
|
|
151
|
-
...(input.scoringDurationMs !== undefined
|
|
152
|
-
? { scoring_duration_ms: input.scoringDurationMs }
|
|
153
|
-
: {}),
|
|
158
|
+
...(input.scoringDurationMs !== undefined ? { scoring_duration_ms: input.scoringDurationMs } : {}),
|
|
154
159
|
...(input.recordedAt ? { recorded_at: input.recordedAt } : {}),
|
|
155
160
|
...(input.agent ? { agent: input.agent } : {}),
|
|
156
161
|
...(skills.length > 0 ? { skills_used: skills } : {}),
|
|
157
162
|
};
|
|
158
163
|
}
|
|
159
|
-
function buildEvalReport(groupName, cases) {
|
|
160
|
-
const trials = cases.map((
|
|
161
|
-
...
|
|
164
|
+
function buildEvalReport(groupName, cases, run) {
|
|
165
|
+
const trials = cases.flatMap(testCase => testCase.attempts).map((attempt, index) => ({
|
|
166
|
+
...attempt.trial,
|
|
162
167
|
trial_id: index + 1,
|
|
163
168
|
}));
|
|
164
|
-
const
|
|
169
|
+
const rewards = cases.flatMap(testCase => testCase.attempts)
|
|
170
|
+
.filter(attempt => attempt.resultKind !== 'synthetic_no_evaluation')
|
|
171
|
+
.map(attempt => attempt.score)
|
|
172
|
+
.filter((score) => score !== undefined);
|
|
173
|
+
const meanReward = average(rewards);
|
|
174
|
+
const metrics = binaryMetrics(cases, run);
|
|
165
175
|
const skills = new Set();
|
|
166
|
-
for (const trial of trials)
|
|
167
|
-
for (const skill of trial.skills_used ?? [])
|
|
176
|
+
for (const trial of trials)
|
|
177
|
+
for (const skill of trial.skills_used ?? [])
|
|
168
178
|
skills.add(skill);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
179
|
return {
|
|
172
180
|
task: groupName,
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
181
|
+
status: run.threshold != null
|
|
182
|
+
? (meanReward >= run.threshold ? 'pass' : 'fail')
|
|
183
|
+
: (cases.every(testCase => testCase.state === 'passed') ? 'pass' : 'fail'),
|
|
184
|
+
mean_reward: meanReward,
|
|
185
|
+
...(metrics.successRate !== undefined ? { success_rate: metrics.successRate } : {}),
|
|
186
|
+
...(metrics.passAtK ? {
|
|
187
|
+
pass_at_k: metrics.passAtK,
|
|
188
|
+
pass_at_k_method: 'finite_sample_unbiased',
|
|
189
|
+
eligible_case_count: metrics.eligibleCaseCount,
|
|
190
|
+
attempts_per_case: metrics.attemptsPerCase,
|
|
191
|
+
} : {}),
|
|
192
|
+
...(metrics.unavailableReason ? { pass_at_k_unavailable_reason: metrics.unavailableReason } : {}),
|
|
176
193
|
trials,
|
|
177
194
|
skills_used: [...skills],
|
|
178
195
|
};
|
|
179
196
|
}
|
|
197
|
+
function binaryMetrics(cases, run) {
|
|
198
|
+
if (run.attemptsCompleted !== run.attemptsRequested)
|
|
199
|
+
return { unavailableReason: 'incomplete_attempts' };
|
|
200
|
+
const successesPerCase = [];
|
|
201
|
+
for (const testCase of cases) {
|
|
202
|
+
if (testCase.attempts.length !== run.attemptsRequested)
|
|
203
|
+
return { unavailableReason: 'incomplete_attempts' };
|
|
204
|
+
let successes = 0;
|
|
205
|
+
for (const attempt of testCase.attempts) {
|
|
206
|
+
if (attempt.outcome.kind === 'not-run')
|
|
207
|
+
return { unavailableReason: 'incomplete_attempts' };
|
|
208
|
+
if (!attempt.hasTerminalEvaluation)
|
|
209
|
+
return { unavailableReason: 'incomplete_attempts' };
|
|
210
|
+
if (attempt.score !== undefined && attempt.score !== 0 && attempt.score !== 1) {
|
|
211
|
+
return { unavailableReason: 'non_binary_reward' };
|
|
212
|
+
}
|
|
213
|
+
if (attempt.outcome.kind === 'passed') {
|
|
214
|
+
if (attempt.score === undefined)
|
|
215
|
+
return { unavailableReason: 'incomplete_attempts' };
|
|
216
|
+
if (attempt.score === 1)
|
|
217
|
+
successes++;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
successesPerCase.push(successes);
|
|
221
|
+
}
|
|
222
|
+
const n = run.attemptsRequested;
|
|
223
|
+
const passAtK = {};
|
|
224
|
+
for (let k = 1; k <= n; k++) {
|
|
225
|
+
passAtK[String(k)] = average(successesPerCase.map(successes => finiteSamplePassAtK(n, successes, k)));
|
|
226
|
+
}
|
|
227
|
+
const totalSuccesses = successesPerCase.reduce((sum, value) => sum + value, 0);
|
|
228
|
+
return {
|
|
229
|
+
successRate: cases.length === 0 ? 0 : totalSuccesses / (cases.length * n),
|
|
230
|
+
passAtK,
|
|
231
|
+
eligibleCaseCount: cases.length,
|
|
232
|
+
attemptsPerCase: n,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
export function finiteSamplePassAtK(n, successes, k) {
|
|
236
|
+
if (k < 1 || k > n)
|
|
237
|
+
throw new RangeError(`k must be in [1, ${n}]`);
|
|
238
|
+
const failures = n - successes;
|
|
239
|
+
if (failures < k)
|
|
240
|
+
return 1;
|
|
241
|
+
let allFailures = 1;
|
|
242
|
+
for (let offset = 0; offset < k; offset++)
|
|
243
|
+
allFailures *= (failures - offset) / (n - offset);
|
|
244
|
+
return 1 - allFailures;
|
|
245
|
+
}
|
|
180
246
|
function buildSummary(groupName, cases, report) {
|
|
247
|
+
const attempts = cases.flatMap(testCase => testCase.attempts);
|
|
248
|
+
const casesById = new Map(cases.map(testCase => [testCase.caseId, testCase]));
|
|
181
249
|
return {
|
|
182
250
|
task: groupName,
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
251
|
+
mean_reward: report.mean_reward ?? 0,
|
|
252
|
+
...(report.success_rate !== undefined ? { success_rate: report.success_rate } : {}),
|
|
253
|
+
...(typeof report.pass_at_k === 'object' ? { pass_at_k: report.pass_at_k } : {}),
|
|
254
|
+
...(report.pass_at_k_unavailable_reason ? { pass_at_k_unavailable_reason: report.pass_at_k_unavailable_reason } : {}),
|
|
255
|
+
...(report.attempts_per_case !== undefined ? { attempts_per_case: report.attempts_per_case } : {}),
|
|
256
|
+
...(report.eligible_case_count !== undefined ? { eligible_case_count: report.eligible_case_count } : {}),
|
|
257
|
+
average_duration_ms: average(attempts.map(attempt => attempt.runnerDurationMs)),
|
|
258
|
+
trial_count: attempts.length,
|
|
259
|
+
diagnostics: attempts.flatMap(attempt => attempt.diagnostics ? [{
|
|
260
|
+
caseName: casesById.get(attempt.caseId)?.name ?? attempt.caseId,
|
|
261
|
+
state: casesById.get(attempt.caseId)?.state ?? 'failed',
|
|
262
|
+
resultKind: attempt.resultKind,
|
|
263
|
+
report: attempt.diagnostics,
|
|
264
|
+
}] : []),
|
|
191
265
|
};
|
|
192
266
|
}
|
|
267
|
+
function outcomeForState(state) {
|
|
268
|
+
if (state === 'passed')
|
|
269
|
+
return { kind: 'passed' };
|
|
270
|
+
if (state === 'failed')
|
|
271
|
+
return { kind: 'failed' };
|
|
272
|
+
return { kind: 'not-run', reason: state };
|
|
273
|
+
}
|
|
193
274
|
function average(values) {
|
|
194
275
|
if (values.length === 0)
|
|
195
276
|
return 0;
|
|
196
277
|
return values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
197
278
|
}
|
|
198
279
|
function slug(value) {
|
|
199
|
-
return value
|
|
200
|
-
.toLowerCase()
|
|
201
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
202
|
-
.replace(/^-+|-+$/g, '') || 'report';
|
|
280
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'report';
|
|
203
281
|
}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import type { DiagnosticsReport } from '../sdk/diagnostics.js';
|
|
2
2
|
import type { AgentExecutionMetadata, EvaluationResultKind } from '../sdk/types.js';
|
|
3
|
+
import type { AttemptOutcome } from '../runners/model.js';
|
|
3
4
|
import type { PathgradeReport, PathgradeSelectionReport, TrialResult } from '../types.js';
|
|
4
5
|
export type ReportCaseState = 'passed' | 'failed' | 'skipped' | 'pending';
|
|
5
6
|
export interface ReportRunInput {
|
|
6
7
|
threshold?: number;
|
|
7
8
|
selection?: PathgradeSelectionReport;
|
|
9
|
+
attemptsRequested?: number;
|
|
10
|
+
attemptsCompleted?: number;
|
|
8
11
|
groups: ReportGroupInput[];
|
|
9
12
|
}
|
|
10
13
|
export interface ReportGroupInput {
|
|
@@ -13,6 +16,7 @@ export interface ReportGroupInput {
|
|
|
13
16
|
}
|
|
14
17
|
export interface ReportCaseInput {
|
|
15
18
|
caseId?: string;
|
|
19
|
+
repeatKey?: string;
|
|
16
20
|
name: string;
|
|
17
21
|
state: ReportCaseState;
|
|
18
22
|
runnerDurationMs: number;
|
|
@@ -22,6 +26,15 @@ export interface ReportCaseInput {
|
|
|
22
26
|
runnerCaseId?: string;
|
|
23
27
|
reportable?: boolean;
|
|
24
28
|
evaluations?: readonly ReportEvaluationInput[];
|
|
29
|
+
attempts?: readonly ReportAttemptInput[];
|
|
30
|
+
diagnostics?: DiagnosticsReport;
|
|
31
|
+
}
|
|
32
|
+
export interface ReportAttemptInput {
|
|
33
|
+
attemptId: string;
|
|
34
|
+
attemptIndex: number;
|
|
35
|
+
outcome: AttemptOutcome;
|
|
36
|
+
runnerDurationMs: number;
|
|
37
|
+
evaluations?: readonly ReportEvaluationInput[];
|
|
25
38
|
diagnostics?: DiagnosticsReport;
|
|
26
39
|
}
|
|
27
40
|
export interface ReportEvaluationInput {
|
|
@@ -48,9 +61,12 @@ export interface ArtifactWriteResult {
|
|
|
48
61
|
}
|
|
49
62
|
export interface ReportSummaryGroup {
|
|
50
63
|
task: string;
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
64
|
+
mean_reward: number;
|
|
65
|
+
success_rate?: number;
|
|
66
|
+
pass_at_k?: Record<string, number>;
|
|
67
|
+
pass_at_k_unavailable_reason?: import('../types.js').PassAtKUnavailableReason;
|
|
68
|
+
attempts_per_case?: number;
|
|
69
|
+
eligible_case_count?: number;
|
|
54
70
|
average_duration_ms: number;
|
|
55
71
|
trial_count: number;
|
|
56
72
|
diagnostics: ReportSummaryDiagnostics[];
|
|
@@ -6,6 +6,7 @@ import { createJestAdapter } from '../adapters/jest/runner-adapter.js';
|
|
|
6
6
|
import { createNodeTestInvocationAdapter } from '../adapters/node-test/invocation-adapter.js';
|
|
7
7
|
import { createVitestInvocationAdapter } from './vitest-invocation.js';
|
|
8
8
|
import { resolveRunnerAdapter } from './selection.js';
|
|
9
|
+
import { withRepeatedAttempts } from './repeated-invocation.js';
|
|
9
10
|
export async function loadRunnerAdapter(input) {
|
|
10
11
|
const name = input.adapterName ?? 'vitest';
|
|
11
12
|
if (name === 'vitest' || name === 'node-test') {
|
|
@@ -24,20 +25,24 @@ export async function loadRunnerAdapter(input) {
|
|
|
24
25
|
}
|
|
25
26
|
export async function loadRunnerInvocationAdapter(input) {
|
|
26
27
|
const name = input.adapterName ?? 'vitest';
|
|
28
|
+
let adapter;
|
|
27
29
|
if (name === 'vitest')
|
|
28
|
-
|
|
29
|
-
if (name === 'node-test')
|
|
30
|
-
|
|
31
|
-
if (name === 'jest')
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
30
|
+
adapter = createVitestInvocationAdapter({ spawnVitest: input.spawnVitest });
|
|
31
|
+
else if (name === 'node-test')
|
|
32
|
+
adapter = createNodeTestInvocationAdapter({ config: input.config });
|
|
33
|
+
else if (name === 'jest')
|
|
34
|
+
adapter = createJestInvocationAdapter({ config: input.config });
|
|
35
|
+
else {
|
|
36
|
+
const mod = await importExternalAdapter({
|
|
37
|
+
adapterName: name,
|
|
38
|
+
cwd: input.cwd ?? process.cwd(),
|
|
39
|
+
});
|
|
40
|
+
if (typeof mod.createPathgradeInvocationAdapter !== 'function') {
|
|
41
|
+
throw new Error(`Pathgrade adapter package "${adapterPackageSpecifier(name)}" must export createPathgradeInvocationAdapter().`);
|
|
42
|
+
}
|
|
43
|
+
adapter = mod.createPathgradeInvocationAdapter({ config: input.config });
|
|
39
44
|
}
|
|
40
|
-
return
|
|
45
|
+
return withRepeatedAttempts({ adapter, config: input.config });
|
|
41
46
|
}
|
|
42
47
|
function adapterPackageSpecifier(adapterName) {
|
|
43
48
|
if (adapterName.startsWith('.') || path.isAbsolute(adapterName) || adapterName.includes('/')) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function assertDirectReporterAttemptsSupported(attempts: number, env: Readonly<NodeJS.ProcessEnv>): void;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export function assertDirectReporterAttemptsSupported(attempts, env) {
|
|
2
|
+
const effectiveAttempts = Number(env.PATHGRADE_ATTEMPT_COUNT);
|
|
3
|
+
if (attempts <= 1 || effectiveAttempts === 1 || env.PATHGRADE_ATTEMPT_INDEX !== undefined)
|
|
4
|
+
return;
|
|
5
|
+
throw new Error(`Pathgrade attempts=${attempts} requires "pathgrade run"; `
|
|
6
|
+
+ 'a direct test-runner reporter cannot restart Jest or Vitest.');
|
|
7
|
+
}
|
|
@@ -36,6 +36,7 @@ export function buildNormalizedRunSnapshotFromReportGroups(run, groups) {
|
|
|
36
36
|
}));
|
|
37
37
|
return {
|
|
38
38
|
id: caseId,
|
|
39
|
+
...(testCase.repeatKey ? { repeatKey: testCase.repeatKey } : {}),
|
|
39
40
|
runId: `${run.adapterName}:run`,
|
|
40
41
|
unitId: unitId(group.groupName, groupIndex),
|
|
41
42
|
name: testCase.name,
|
|
@@ -18,6 +18,37 @@ function validateRun(snapshot, completeness, errors) {
|
|
|
18
18
|
if (!snapshot.model.run.adapterName) {
|
|
19
19
|
errors.push({ path: 'model.run.adapterName', message: 'Run adapterName is required.' });
|
|
20
20
|
}
|
|
21
|
+
const { attemptsRequested, attemptsCompleted } = snapshot.model.run;
|
|
22
|
+
if (attemptsRequested === undefined && attemptsCompleted !== undefined) {
|
|
23
|
+
errors.push({
|
|
24
|
+
path: 'model.run.attemptsRequested',
|
|
25
|
+
message: 'attemptsRequested is required when attemptsCompleted is present.',
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
if (attemptsCompleted === undefined && attemptsRequested !== undefined) {
|
|
29
|
+
errors.push({
|
|
30
|
+
path: 'model.run.attemptsCompleted',
|
|
31
|
+
message: 'attemptsCompleted is required when attemptsRequested is present.',
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
if (attemptsRequested !== undefined && (!Number.isSafeInteger(attemptsRequested) || attemptsRequested < 1)) {
|
|
35
|
+
errors.push({
|
|
36
|
+
path: 'model.run.attemptsRequested',
|
|
37
|
+
message: 'attemptsRequested must be a positive integer.',
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
if (attemptsCompleted !== undefined && (!Number.isSafeInteger(attemptsCompleted) || attemptsCompleted < 0)) {
|
|
41
|
+
errors.push({
|
|
42
|
+
path: 'model.run.attemptsCompleted',
|
|
43
|
+
message: 'attemptsCompleted must be a non-negative integer.',
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
if (attemptsRequested !== undefined && attemptsCompleted !== undefined && attemptsCompleted > attemptsRequested) {
|
|
47
|
+
errors.push({
|
|
48
|
+
path: 'model.run.attemptsCompleted',
|
|
49
|
+
message: 'attemptsCompleted cannot exceed attemptsRequested.',
|
|
50
|
+
});
|
|
51
|
+
}
|
|
21
52
|
if (completeness === 'final' && snapshot.model.run.status === 'parked' && (snapshot.model.run.diagnostics ?? []).length === 0) {
|
|
22
53
|
errors.push({ path: 'model.run.diagnostics', message: 'Final parked snapshots require diagnostics explaining why parked is terminal.' });
|
|
23
54
|
}
|
package/dist/runners/model.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ export interface RunRecord {
|
|
|
16
16
|
id: string;
|
|
17
17
|
adapterName: string;
|
|
18
18
|
status: RunStatus;
|
|
19
|
+
attemptsRequested?: number;
|
|
20
|
+
attemptsCompleted?: number;
|
|
19
21
|
diagnostics?: Diagnostic[];
|
|
20
22
|
nativeReferences?: NativeReference[];
|
|
21
23
|
}
|
|
@@ -39,6 +41,8 @@ export type ScoringPolicy = {
|
|
|
39
41
|
};
|
|
40
42
|
export interface RunCaseRecord {
|
|
41
43
|
id: string;
|
|
44
|
+
/** Adapter-owned semantic identity used only to match this case across runner invocations. */
|
|
45
|
+
repeatKey?: string;
|
|
42
46
|
runId: string;
|
|
43
47
|
unitId?: string;
|
|
44
48
|
name: string;
|
|
@@ -8,6 +8,8 @@ export interface PathgradeRunOptions {
|
|
|
8
8
|
runnerArgs: string[];
|
|
9
9
|
env: NodeJS.ProcessEnv;
|
|
10
10
|
artifactRoot: string;
|
|
11
|
+
/** Child-run handoff path. When set, write only the normalized snapshot. */
|
|
12
|
+
normalizedSnapshotPath?: string;
|
|
11
13
|
reporterMode?: AdapterReporterMode;
|
|
12
14
|
threshold?: number;
|
|
13
15
|
selection?: PathgradeSelectionReport;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { buildPathgradeReport } from '../reporting/core.js';
|
|
2
2
|
import { writePathgradeArtifacts } from '../reporting/artifacts.js';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import fs from 'fs-extra';
|
|
3
5
|
import { projectNormalizedRunSnapshotToReportInput } from './report-projection.js';
|
|
4
6
|
export async function runWithAdapter(input) {
|
|
5
7
|
const { adapter, options } = input;
|
|
@@ -18,7 +20,15 @@ export async function runWithAdapter(input) {
|
|
|
18
20
|
signal: options.signal,
|
|
19
21
|
});
|
|
20
22
|
const runExitCode = normalizeRunExitCode(run.status, run.exitCode);
|
|
21
|
-
const
|
|
23
|
+
const snapshot = await adapter.collectNormalizedRunSnapshot(run);
|
|
24
|
+
if (options.normalizedSnapshotPath) {
|
|
25
|
+
// Projection performs the strict final-snapshot validation before handoff.
|
|
26
|
+
projectNormalizedRunSnapshotToReportInput(snapshot, { selection });
|
|
27
|
+
await fs.ensureDir(path.dirname(options.normalizedSnapshotPath));
|
|
28
|
+
await fs.writeJson(options.normalizedSnapshotPath, snapshot, { spaces: 2 });
|
|
29
|
+
return runExitCode;
|
|
30
|
+
}
|
|
31
|
+
const reportInput = projectNormalizedRunSnapshotToReportInput(snapshot, { selection });
|
|
22
32
|
let built = buildPathgradeReport({
|
|
23
33
|
threshold: options.threshold,
|
|
24
34
|
...reportInput,
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { NormalizedRunSnapshot } from './model.js';
|
|
2
|
+
export type RepeatAggregationFailure = 'incomplete_attempts' | 'case_identity_mismatch';
|
|
3
|
+
export declare class RepeatAggregationError extends Error {
|
|
4
|
+
readonly reason: RepeatAggregationFailure;
|
|
5
|
+
constructor(reason: RepeatAggregationFailure, message: string);
|
|
6
|
+
}
|
|
7
|
+
export declare function mergeRepeatedRunSnapshots(snapshots: readonly NormalizedRunSnapshot[]): NormalizedRunSnapshot;
|