@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
|
@@ -26,11 +26,24 @@ export function formatNoAffectedEvalsMarkdown(selection) {
|
|
|
26
26
|
return lines.join('\n');
|
|
27
27
|
}
|
|
28
28
|
export function commentMarker(commentId) {
|
|
29
|
+
if (commentId.length === 0
|
|
30
|
+
|| commentId.length > 128
|
|
31
|
+
|| /[<>\r\n]/.test(commentId)
|
|
32
|
+
|| commentId.includes('--')) {
|
|
33
|
+
throw new Error('Pathgrade comment id must be 1-128 characters and cannot contain HTML comment delimiters');
|
|
34
|
+
}
|
|
29
35
|
return `<!-- pathgrade:${commentId} -->`;
|
|
30
36
|
}
|
|
31
37
|
function pct(n) {
|
|
32
38
|
return `${(n * 100).toFixed(1)}%`;
|
|
33
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
|
+
}
|
|
34
47
|
function durationSeconds(ms) {
|
|
35
48
|
return `${(ms / 1000).toFixed(1)}s`;
|
|
36
49
|
}
|
|
@@ -49,30 +62,36 @@ function escapeTableCell(value) {
|
|
|
49
62
|
* posted as a GitHub issue comment body.
|
|
50
63
|
*/
|
|
51
64
|
export function formatReportMarkdown(report, opts) {
|
|
52
|
-
const
|
|
53
|
-
const p = report.overall_pass_rate;
|
|
54
|
-
const overallPassAtK = totalTrials > 0 ? 1 - Math.pow(1 - p, totalTrials) : 0;
|
|
55
|
-
const overallPassPowK = totalTrials > 0 ? Math.pow(p, totalTrials) : 0;
|
|
65
|
+
const meanReward = report.overall_mean_reward ?? report.overall_pass_rate;
|
|
56
66
|
const icon = report.status === 'pass' ? '✅' : '❌';
|
|
57
67
|
const lines = [];
|
|
58
68
|
lines.push(commentMarker(opts.commentId));
|
|
59
69
|
lines.push('');
|
|
60
70
|
lines.push(`### ${icon} Pathgrade report`);
|
|
61
71
|
lines.push('');
|
|
62
|
-
lines.push(`**
|
|
63
|
-
|
|
64
|
-
|
|
72
|
+
lines.push(`**Last run:** \`${report.timestamp}\``);
|
|
73
|
+
lines.push('');
|
|
74
|
+
lines.push(`**Mean reward:** ${pct(meanReward)}` +
|
|
75
|
+
(report.attempts_requested ? ` | **Attempts:** ${report.attempts_completed ?? 0}/${report.attempts_requested}` : ''));
|
|
76
|
+
if (opts.detailsUrl) {
|
|
77
|
+
lines.push('');
|
|
78
|
+
lines.push(`**Details:** [Open the full report](${formatLinkDestination(opts.detailsUrl)})`);
|
|
79
|
+
}
|
|
80
|
+
if (opts.notice) {
|
|
81
|
+
lines.push('');
|
|
82
|
+
lines.push(`> ${opts.notice.replace(/[\r\n]+/g, ' ').trim()}`);
|
|
83
|
+
}
|
|
65
84
|
if (report.threshold != null) {
|
|
66
85
|
lines.push('');
|
|
67
86
|
lines.push(`Threshold: ${pct(report.threshold)} — ${report.status.toUpperCase()}`);
|
|
68
87
|
}
|
|
69
88
|
lines.push('');
|
|
70
|
-
lines.push('| Group |
|
|
89
|
+
lines.push('| Group | Mean reward | Success rate | pass@k | Skills | Avg duration |');
|
|
71
90
|
lines.push('|---|---|---|---|---|---|');
|
|
72
91
|
for (const group of report.groups) {
|
|
73
92
|
const skills = group.skills_used.length > 0 ? group.skills_used.join(', ') : '—';
|
|
74
93
|
const avg = computeAvgDuration(group.trials);
|
|
75
|
-
lines.push(`| ${escapeTableCell(group.task)} | ${pct(group.pass_rate)} | ${pct(group.
|
|
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)} |`);
|
|
76
95
|
}
|
|
77
96
|
if (report.selection) {
|
|
78
97
|
lines.push('');
|
|
@@ -100,14 +119,14 @@ export function formatSelectionSection(selection) {
|
|
|
100
119
|
out.push('### Selection');
|
|
101
120
|
const total = selection.selected.length + selection.skipped.length;
|
|
102
121
|
if (selection.global_match) {
|
|
103
|
-
out.push(`
|
|
122
|
+
out.push(`Selected **all ${total}** eval files — global trigger \`${selection.global_match}\` matched.`);
|
|
104
123
|
return out.join('\n');
|
|
105
124
|
}
|
|
106
125
|
if (selection.skipped.length === 0 && selection.selected.length > 0) {
|
|
107
|
-
out.push(`
|
|
126
|
+
out.push(`Selected **all ${total}** eval files — every eval had a matching change.`);
|
|
108
127
|
return out.join('\n');
|
|
109
128
|
}
|
|
110
|
-
out.push(`
|
|
129
|
+
out.push(`Selected **${selection.selected.length} of ${total}** eval files based on changes vs \`${selection.base_ref}\`.`);
|
|
111
130
|
if (selection.skipped.length > 0) {
|
|
112
131
|
out.push('');
|
|
113
132
|
out.push(`<details><summary>${selection.skipped.length} skipped (unaffected)</summary>`);
|
|
@@ -171,13 +190,36 @@ function apiHeaders(token) {
|
|
|
171
190
|
};
|
|
172
191
|
}
|
|
173
192
|
async function listPrComments(ctx) {
|
|
174
|
-
const
|
|
193
|
+
const comments = [];
|
|
194
|
+
for (let page = 1; page <= 100; page += 1) {
|
|
195
|
+
const url = `https://api.github.com/repos/${ctx.owner}/${ctx.repo}/issues/${ctx.prNumber}/comments?per_page=100&page=${page}`;
|
|
196
|
+
const res = await fetch(url, { headers: apiHeaders(ctx.token) });
|
|
197
|
+
if (!res.ok) {
|
|
198
|
+
throw new Error(`GET ${url} returned ${res.status}`);
|
|
199
|
+
}
|
|
200
|
+
const data = await res.json();
|
|
201
|
+
if (!Array.isArray(data)) {
|
|
202
|
+
throw new Error(`GET ${url} returned a non-array response`);
|
|
203
|
+
}
|
|
204
|
+
for (const value of data) {
|
|
205
|
+
if (isGithubComment(value))
|
|
206
|
+
comments.push(value);
|
|
207
|
+
}
|
|
208
|
+
if (data.length < 100)
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
return comments;
|
|
212
|
+
}
|
|
213
|
+
async function currentPrHeadSha(ctx) {
|
|
214
|
+
const url = `https://api.github.com/repos/${ctx.owner}/${ctx.repo}/pulls/${ctx.prNumber}`;
|
|
175
215
|
const res = await fetch(url, { headers: apiHeaders(ctx.token) });
|
|
176
|
-
if (!res.ok)
|
|
216
|
+
if (!res.ok)
|
|
177
217
|
throw new Error(`GET ${url} returned ${res.status}`);
|
|
218
|
+
const data = await res.json();
|
|
219
|
+
if (!isRecord(data) || !isRecord(data.head) || typeof data.head.sha !== 'string') {
|
|
220
|
+
throw new Error(`GET ${url} returned no pull-request head SHA`);
|
|
178
221
|
}
|
|
179
|
-
|
|
180
|
-
return Array.isArray(data) ? data : [];
|
|
222
|
+
return data.head.sha;
|
|
181
223
|
}
|
|
182
224
|
async function createPrComment(ctx, body) {
|
|
183
225
|
const url = `https://api.github.com/repos/${ctx.owner}/${ctx.repo}/issues/${ctx.prNumber}/comments`;
|
|
@@ -206,8 +248,8 @@ const GITHUB_COMMENT_MAX = 65536;
|
|
|
206
248
|
const TRUNCATION_SENTINEL = '\n\n_…output truncated to fit GitHub\'s comment size limit — see the workflow artifacts for the full report._';
|
|
207
249
|
/**
|
|
208
250
|
* Find an existing PR comment carrying `<!-- pathgrade:${commentId} -->`
|
|
209
|
-
* and update it; otherwise create a new one.
|
|
210
|
-
*
|
|
251
|
+
* and update it; otherwise create a new one. Default mode logs and swallows
|
|
252
|
+
* transport errors; strict mode rethrows them for an owning orchestrator.
|
|
211
253
|
*
|
|
212
254
|
* Bodies longer than GitHub's 65,536-character limit are truncated with a
|
|
213
255
|
* sentinel pointing to workflow artifacts, preserving the leading dedup
|
|
@@ -221,19 +263,51 @@ export async function postOrUpdateComment(ctx, opts) {
|
|
|
221
263
|
: withMarker.slice(0, GITHUB_COMMENT_MAX - TRUNCATION_SENTINEL.length) +
|
|
222
264
|
TRUNCATION_SENTINEL;
|
|
223
265
|
try {
|
|
266
|
+
if (opts.expectedHeadSha) {
|
|
267
|
+
const currentHead = await currentPrHeadSha(ctx);
|
|
268
|
+
if (currentHead !== opts.expectedHeadSha)
|
|
269
|
+
return 'stale';
|
|
270
|
+
}
|
|
224
271
|
const existing = await listPrComments(ctx);
|
|
225
272
|
const match = existing.find((c) => typeof c.body === 'string' && c.body.includes(marker));
|
|
226
273
|
if (match) {
|
|
227
274
|
await updatePrComment(ctx, match.id, body);
|
|
275
|
+
return 'updated';
|
|
228
276
|
}
|
|
229
277
|
else {
|
|
230
278
|
await createPrComment(ctx, body);
|
|
279
|
+
return 'created';
|
|
231
280
|
}
|
|
232
281
|
}
|
|
233
282
|
catch (err) {
|
|
234
283
|
const message = err instanceof Error ? err.message : String(err);
|
|
235
284
|
console.error(`pathgrade report: failed to post PR comment — ${message}`);
|
|
285
|
+
if (opts.strict)
|
|
286
|
+
throw err;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
function formatLinkDestination(value) {
|
|
290
|
+
let url;
|
|
291
|
+
try {
|
|
292
|
+
url = new URL(value);
|
|
236
293
|
}
|
|
294
|
+
catch {
|
|
295
|
+
throw new Error('Pathgrade details URL must be an absolute HTTP(S) URL');
|
|
296
|
+
}
|
|
297
|
+
if (url.protocol !== 'https:' && url.protocol !== 'http:') {
|
|
298
|
+
throw new Error('Pathgrade details URL must be an absolute HTTP(S) URL');
|
|
299
|
+
}
|
|
300
|
+
return url.toString().replace(/\)/g, '%29');
|
|
301
|
+
}
|
|
302
|
+
function isRecord(value) {
|
|
303
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
304
|
+
}
|
|
305
|
+
function isGithubComment(value) {
|
|
306
|
+
return isRecord(value)
|
|
307
|
+
&& typeof value.id === 'number'
|
|
308
|
+
&& Number.isSafeInteger(value.id)
|
|
309
|
+
&& value.id > 0
|
|
310
|
+
&& typeof value.body === 'string';
|
|
237
311
|
}
|
|
238
312
|
function formatGroupDetails(group) {
|
|
239
313
|
const out = [];
|
package/dist/reporters/loader.js
CHANGED
|
@@ -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
|
-
|
|
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 = {
|
|
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
|
|
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('
|
|
9
|
-
console.log(` ${fmt.dim(
|
|
10
|
-
|
|
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('
|
|
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,18 +1,21 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import fs from 'fs-extra';
|
|
3
|
+
import { collectSensitiveEnvValues, sanitizePersistenceValue, } from '../tool-event-results.js';
|
|
3
4
|
export async function writePathgradeArtifacts(artifactRoot, built) {
|
|
4
5
|
await fs.ensureDir(path.join(artifactRoot, 'traces'));
|
|
6
|
+
const sensitiveValues = collectSensitiveEnvValues(process.env);
|
|
7
|
+
const report = sanitizePersistenceValue(built.report, sensitiveValues);
|
|
5
8
|
const gitignorePath = path.join(artifactRoot, '.gitignore');
|
|
6
9
|
if (!(await fs.pathExists(gitignorePath))) {
|
|
7
10
|
await fs.writeFile(gitignorePath, '*\n');
|
|
8
11
|
}
|
|
9
12
|
const traceFiles = [];
|
|
10
13
|
for (const trace of built.traces) {
|
|
11
|
-
await fs.writeJson(path.join(artifactRoot, trace.traceFile), trace.trials, { spaces: 2 });
|
|
14
|
+
await fs.writeJson(path.join(artifactRoot, trace.traceFile), sanitizePersistenceValue(trace.trials, sensitiveValues), { spaces: 2 });
|
|
12
15
|
traceFiles.push(trace.traceFile);
|
|
13
16
|
}
|
|
14
17
|
const resultsPath = path.join(artifactRoot, 'results.json');
|
|
15
|
-
await fs.writeJson(resultsPath,
|
|
18
|
+
await fs.writeJson(resultsPath, report, { spaces: 2 });
|
|
16
19
|
return {
|
|
17
20
|
resultsPath,
|
|
18
21
|
traceFiles,
|
package/dist/reporting/core.d.ts
CHANGED