@wix/pathgrade 1.0.38 → 1.0.40
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 +5 -5
- package/dist/adapters/jest/results.d.ts +1 -0
- package/dist/adapters/jest/results.js +36 -14
- package/dist/adapters/jest/runner-adapter.d.ts +1 -0
- package/dist/adapters/jest/runner-adapter.js +5 -1
- package/dist/adapters/node-test/runner-adapter.js +6 -5
- package/dist/affected/meta.d.ts +4 -1
- package/dist/affected/meta.js +9 -5
- package/dist/affected/select.js +1 -1
- package/dist/agents/claude/sdk-message-projector.js +4 -5
- package/dist/agents/claude/tool-permission-bridge.js +2 -1
- package/dist/agents/codex-app-server/item-projection.js +2 -2
- package/dist/agents/codex-app-server/mcp-approval-correlator.js +3 -2
- package/dist/agents/opencode.js +4 -5
- package/dist/commands/report.js +5 -26
- package/dist/internal/direct-mcp-v2/acp-author-projector.js +20 -7
- package/dist/reporters/cli.js +20 -1
- package/dist/reporters/github-comment.js +18 -7
- package/dist/reporters/loader.d.ts +4 -0
- package/dist/reporters/loader.js +26 -9
- package/dist/reporting/comparison-contract.d.ts +3 -2
- package/dist/reporting/comparison-contract.js +66 -33
- package/dist/reporting/core.js +35 -8
- package/dist/reporting/reliability-contract.d.ts +1 -0
- package/dist/reporting/report-parser.js +125 -5
- package/dist/reporting/source-metadata.d.ts +3 -0
- package/dist/reporting/source-metadata.js +67 -2
- package/dist/reporting/types.d.ts +5 -1
- package/dist/runners/model-builders.d.ts +1 -0
- package/dist/runners/model-builders.js +36 -12
- package/dist/runners/model.d.ts +2 -0
- package/dist/runners/orchestrator.js +3 -4
- package/dist/runners/repeated-invocation.js +2 -5
- package/dist/runners/report-projection.js +84 -0
- package/dist/runners/vitest-adapter.js +6 -2
- package/dist/sdk/agent.js +10 -1
- package/dist/sdk/evaluate.js +58 -3
- package/dist/sdk/judge-prompt-builder.js +11 -7
- package/dist/sdk/mcp-event-input.d.ts +1 -0
- package/dist/sdk/mcp-event-input.js +3 -0
- package/dist/sdk/mcp-evidence.js +16 -3
- package/dist/sdk/mcp-safety.js +2 -2
- package/dist/sdk/scorers.d.ts +5 -0
- package/dist/sdk/scorers.js +4 -0
- package/dist/sdk/scripted-mcp-events.js +3 -2
- package/dist/sdk/tool-event-log.js +18 -3
- package/dist/sdk/tool-event-secrets.d.ts +4 -0
- package/dist/sdk/tool-event-secrets.js +33 -2
- package/dist/sdk/types.d.ts +2 -0
- package/dist/tool-event-results.d.ts +3 -0
- package/dist/tool-event-results.js +153 -23
- package/dist/types.d.ts +14 -7
- package/package.json +2 -2
package/dist/reporters/loader.js
CHANGED
|
@@ -34,11 +34,18 @@ export async function loadReports(resultsDir, opts) {
|
|
|
34
34
|
if (raw.version !== 1 && raw.version !== 2)
|
|
35
35
|
continue;
|
|
36
36
|
for (const group of raw.groups) {
|
|
37
|
+
const runnerStatus = groupRunnerStatus(group);
|
|
38
|
+
const thresholdStatus = groupThresholdStatus(raw, group);
|
|
37
39
|
const report = {
|
|
38
40
|
file,
|
|
39
41
|
timestamp: raw.timestamp,
|
|
42
|
+
threshold: raw.threshold,
|
|
40
43
|
...group,
|
|
41
|
-
|
|
44
|
+
runner_status: runnerStatus,
|
|
45
|
+
...(raw.runner_status === 'pass' || raw.runner_status === 'fail'
|
|
46
|
+
? { run_runner_status: raw.runner_status } : {}),
|
|
47
|
+
threshold_status: thresholdStatus,
|
|
48
|
+
status: runnerStatus === 'fail' || thresholdStatus === 'fail' ? 'fail' : 'pass',
|
|
42
49
|
};
|
|
43
50
|
if (!opts?.skipTraces && group.trace_file) {
|
|
44
51
|
await hydrateTraces(report, group.trace_file, resolved);
|
|
@@ -50,15 +57,25 @@ export async function loadReports(resultsDir, opts) {
|
|
|
50
57
|
}
|
|
51
58
|
return results;
|
|
52
59
|
}
|
|
53
|
-
function
|
|
60
|
+
function groupRunnerStatus(group) {
|
|
61
|
+
if (group.runner_status === 'pass' || group.runner_status === 'fail')
|
|
62
|
+
return group.runner_status;
|
|
63
|
+
const outcomes = Array.isArray(group.trials)
|
|
64
|
+
? group.trials.map((trial) => trial.runner_outcome).filter((outcome) => outcome !== undefined)
|
|
65
|
+
: [];
|
|
66
|
+
if (outcomes.length > 0) {
|
|
67
|
+
return outcomes.every((outcome) => outcome === 'passed') ? 'pass' : 'fail';
|
|
68
|
+
}
|
|
54
69
|
if (group.status === 'pass' || group.status === 'fail')
|
|
55
70
|
return group.status;
|
|
56
71
|
const meanReward = group.mean_reward ?? group.pass_rate ?? 0;
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
72
|
+
return meanReward === 1 ? 'pass' : 'fail';
|
|
73
|
+
}
|
|
74
|
+
function groupThresholdStatus(raw, group) {
|
|
75
|
+
if (group.threshold_status === 'pass' || group.threshold_status === 'fail'
|
|
76
|
+
|| group.threshold_status === 'not_configured')
|
|
77
|
+
return group.threshold_status;
|
|
78
|
+
if (typeof raw.threshold !== 'number')
|
|
79
|
+
return 'not_configured';
|
|
80
|
+
return (group.mean_reward ?? group.pass_rate ?? 0) >= raw.threshold ? 'pass' : 'fail';
|
|
64
81
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { EvalReport } from '../types.js';
|
|
2
2
|
import type { Scorer } from '../sdk/types.js';
|
|
3
3
|
import type { ReportGroupInput } from './types.js';
|
|
4
|
+
import type { ComparisonContractV2 } from './reliability-contract.js';
|
|
4
5
|
export declare function buildComparisonContract(input: {
|
|
5
6
|
group: ReportGroupInput;
|
|
6
7
|
report: EvalReport;
|
|
7
8
|
attemptsRequested: number;
|
|
8
9
|
attemptsCompleted: number;
|
|
9
|
-
}):
|
|
10
|
+
}): ComparisonContractV2;
|
|
10
11
|
export declare function createScorerRevision(scorers: readonly Scorer[]): string | undefined;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { canonicalizeJson } from '../core/canonical-json.js';
|
|
3
3
|
export function buildComparisonContract(input) {
|
|
4
|
-
const unavailable = new Set();
|
|
5
4
|
const evaluations = input.group.cases.flatMap(testCase => {
|
|
6
5
|
const caseIdentity = testCase.repeatKey ?? testCase.caseId ?? testCase.name;
|
|
7
6
|
return (testCase.attempts?.flatMap(attempt => attempt.evaluations ?? [])
|
|
@@ -15,16 +14,16 @@ export function buildComparisonContract(input) {
|
|
|
15
14
|
runtime: runtimeIdentity(evaluation),
|
|
16
15
|
}));
|
|
17
16
|
});
|
|
18
|
-
const
|
|
17
|
+
const comparisonInputs = input.group.comparisonInputs ?? { state: 'missing' };
|
|
18
|
+
const definitionRevision = input.group.sourceRevision && comparisonInputs.state === 'resolved'
|
|
19
19
|
? revision('definition', {
|
|
20
20
|
source: input.group.sourceRevision,
|
|
21
|
+
comparison_inputs: comparisonInputs.revision,
|
|
21
22
|
evaluations: uniqueSorted(evaluations.map(evaluation => ({
|
|
22
23
|
case: evaluation.case, definition: evaluation.definition,
|
|
23
24
|
}))),
|
|
24
25
|
})
|
|
25
26
|
: undefined;
|
|
26
|
-
if (!definitionRevision)
|
|
27
|
-
unavailable.add('definition_metadata_missing');
|
|
28
27
|
const scorerRevision = evaluations.length > 0 && evaluations.every(evaluation => evaluation.scorer)
|
|
29
28
|
? revision('scorers', uniqueSorted(evaluations.map(evaluation => ({
|
|
30
29
|
case: evaluation.case,
|
|
@@ -32,20 +31,10 @@ export function buildComparisonContract(input) {
|
|
|
32
31
|
scorer: evaluation.scorer,
|
|
33
32
|
}))))
|
|
34
33
|
: undefined;
|
|
35
|
-
if (!scorerRevision)
|
|
36
|
-
unavailable.add('scorer_metadata_missing');
|
|
37
34
|
const runtimeComplete = evaluations.length > 0
|
|
38
|
-
&& evaluations.every(evaluation =>
|
|
39
|
-
|
|
40
|
-
const runtimeRevision =
|
|
41
|
-
? revision('runtime', uniqueSorted(evaluations.map(evaluation => ({
|
|
42
|
-
case: evaluation.case,
|
|
43
|
-
definition: evaluation.definition,
|
|
44
|
-
runtime: evaluation.runtime,
|
|
45
|
-
}))))
|
|
46
|
-
: undefined;
|
|
47
|
-
if (!runtimeRevision)
|
|
48
|
-
unavailable.add('runtime_metadata_missing');
|
|
35
|
+
&& evaluations.every(evaluation => runtimeIdentityComplete(evaluation.runtime));
|
|
36
|
+
const runtimeComponents = runtimeComplete ? runtimeComponentRevisions(evaluations) : undefined;
|
|
37
|
+
const runtimeRevision = runtimeComponents ? revision('runtime', runtimeComponents) : undefined;
|
|
49
38
|
const reportableCases = input.group.cases.filter(testCase => testCase.reportable !== false
|
|
50
39
|
&& testCase.state !== 'skipped' && testCase.state !== 'pending');
|
|
51
40
|
const samplingComplete = input.attemptsCompleted === input.attemptsRequested
|
|
@@ -59,16 +48,41 @@ export function buildComparisonContract(input) {
|
|
|
59
48
|
cases: reportableCases.map(testCase => testCase.repeatKey ?? testCase.caseId ?? testCase.name).toSorted(),
|
|
60
49
|
})
|
|
61
50
|
: undefined;
|
|
62
|
-
if (!samplingRevision)
|
|
63
|
-
unavailable.add('sampling_incomplete');
|
|
64
51
|
return {
|
|
65
|
-
version:
|
|
66
|
-
...(definitionRevision
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
...(
|
|
70
|
-
|
|
71
|
-
|
|
52
|
+
version: 2,
|
|
53
|
+
...(definitionRevision
|
|
54
|
+
? { comparison_inputs: comparisonInputs, definition_revision: definitionRevision }
|
|
55
|
+
: { comparison_inputs: comparisonInputs }),
|
|
56
|
+
...(scorerRevision
|
|
57
|
+
? { scorer_revision_state: { state: 'complete' }, scorer_revision: scorerRevision }
|
|
58
|
+
: { scorer_revision_state: { state: 'incomplete', reason: 'explicit-revision-required' } }),
|
|
59
|
+
...(runtimeRevision && runtimeComponents
|
|
60
|
+
? {
|
|
61
|
+
runtime_revision_state: { state: 'complete' },
|
|
62
|
+
runtime_revision: runtimeRevision,
|
|
63
|
+
runtime_components: runtimeComponents,
|
|
64
|
+
}
|
|
65
|
+
: { runtime_revision_state: { state: 'incomplete', reason: 'metadata-missing' } }),
|
|
66
|
+
...(samplingRevision
|
|
67
|
+
? { sampling_revision_state: { state: 'complete' }, sampling_revision: samplingRevision }
|
|
68
|
+
: { sampling_revision_state: { state: 'incomplete', reason: 'sampling-incomplete' } }),
|
|
69
|
+
report_schema_revision: 'pathgrade-results-v3',
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function runtimeComponentRevisions(evaluations) {
|
|
73
|
+
const component = (name) => revision(`runtime-${name}`, uniqueSorted(evaluations.map(evaluation => ({
|
|
74
|
+
case: evaluation.case,
|
|
75
|
+
definition: evaluation.definition,
|
|
76
|
+
value: evaluation.runtime[name] ?? null,
|
|
77
|
+
}))));
|
|
78
|
+
return {
|
|
79
|
+
agent_name: component('agent_name'),
|
|
80
|
+
model: component('model'),
|
|
81
|
+
transport: component('transport'),
|
|
82
|
+
interaction_mode: component('interaction_mode'),
|
|
83
|
+
...(evaluations.some(evaluation => evaluation.runtime.flow)
|
|
84
|
+
? { flow: component('flow') }
|
|
85
|
+
: {}),
|
|
72
86
|
};
|
|
73
87
|
}
|
|
74
88
|
function hasTerminalEvaluation(evaluations) {
|
|
@@ -85,16 +99,20 @@ export function createScorerRevision(scorers) {
|
|
|
85
99
|
}
|
|
86
100
|
function runtimeIdentity(evaluation) {
|
|
87
101
|
const agent = evaluation.agent ?? evaluation.trial?.agent;
|
|
88
|
-
if (agent)
|
|
102
|
+
if (agent)
|
|
89
103
|
return {
|
|
90
|
-
|
|
104
|
+
agent_name: agent.name,
|
|
91
105
|
model: agent.resolvedModel,
|
|
92
106
|
transport: agent.transport,
|
|
93
|
-
|
|
107
|
+
interaction_mode: agent.interactionMode,
|
|
108
|
+
flow: undefined,
|
|
94
109
|
};
|
|
95
|
-
}
|
|
96
110
|
const flow = evaluation.trial?.flow_trace;
|
|
97
111
|
return {
|
|
112
|
+
agent_name: undefined,
|
|
113
|
+
model: undefined,
|
|
114
|
+
transport: undefined,
|
|
115
|
+
interaction_mode: undefined,
|
|
98
116
|
flow: flow?.completeness.runtimeIdentity === 'complete'
|
|
99
117
|
&& flow.participants.every(participant => participant.runtime) ? {
|
|
100
118
|
...(flow.protocol ? { protocol: flow.protocol } : {}),
|
|
@@ -105,10 +123,22 @@ function runtimeIdentity(evaluation) {
|
|
|
105
123
|
} : undefined,
|
|
106
124
|
};
|
|
107
125
|
}
|
|
126
|
+
function runtimeIdentityComplete(identity) {
|
|
127
|
+
return identity.flow !== undefined || [
|
|
128
|
+
identity.agent_name, identity.model, identity.transport, identity.interaction_mode,
|
|
129
|
+
].every(value => value !== undefined);
|
|
130
|
+
}
|
|
108
131
|
function scorerDeclaration(scorer) {
|
|
109
|
-
const common = {
|
|
132
|
+
const common = {
|
|
133
|
+
type: scorer.type,
|
|
134
|
+
name: scorer.name.normalize('NFC'),
|
|
135
|
+
weight: scorer.weight,
|
|
136
|
+
...(scorer.revision === undefined ? {} : { revision: scorer.revision }),
|
|
137
|
+
};
|
|
110
138
|
if (scorer.type === 'check' || scorer.type === 'score') {
|
|
111
|
-
|
|
139
|
+
if (scorer.revision === undefined)
|
|
140
|
+
throw new Error('explicit scorer revision required');
|
|
141
|
+
return common;
|
|
112
142
|
}
|
|
113
143
|
if (scorer.type === 'tool_usage') {
|
|
114
144
|
return {
|
|
@@ -117,13 +147,16 @@ function scorerDeclaration(scorer) {
|
|
|
117
147
|
.toSorted((left, right) => canonicalizeJson(left).localeCompare(canonicalizeJson(right))),
|
|
118
148
|
};
|
|
119
149
|
}
|
|
150
|
+
if (typeof scorer.input === 'function' && scorer.revision === undefined) {
|
|
151
|
+
throw new Error('explicit scorer revision required');
|
|
152
|
+
}
|
|
120
153
|
return {
|
|
121
154
|
...common,
|
|
122
155
|
rubric: scorer.rubric,
|
|
123
156
|
model: scorer.model ?? null,
|
|
124
157
|
retry: scorer.retry ?? null,
|
|
125
158
|
includeToolEvents: scorer.includeToolEvents ?? null,
|
|
126
|
-
input: typeof scorer.input === 'function' ?
|
|
159
|
+
input: typeof scorer.input === 'function' ? null : scorer.input ?? null,
|
|
127
160
|
tools: scorer.tools?.toSorted() ?? null,
|
|
128
161
|
maxRounds: scorer.maxRounds ?? null,
|
|
129
162
|
cacheControl: scorer.cacheControl ?? null,
|
package/dist/reporting/core.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { buildDiagnosticsReport } from '../sdk/diagnostics.js';
|
|
2
2
|
import { extractSkillsFromLog } from '../tool-events.js';
|
|
3
3
|
import { buildComparisonContract } from './comparison-contract.js';
|
|
4
|
+
import { TASK_INVENTORY_VERSION } from './reliability-contract.js';
|
|
4
5
|
export function buildPathgradeReport(input) {
|
|
5
6
|
const warnings = [];
|
|
6
7
|
const consolidatedGroups = [];
|
|
@@ -52,20 +53,34 @@ export function buildPathgradeReport(input) {
|
|
|
52
53
|
.map(attempt => attempt.score)
|
|
53
54
|
.filter((score) => score !== undefined))));
|
|
54
55
|
const overallMeanReward = average(scores);
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
const runnerStatus = (input.runStatus === undefined || input.runStatus === 'completed')
|
|
57
|
+
&& reportableGroups.every(group => group.cases.every(testCase => (testCase.state !== 'failed'
|
|
58
|
+
&& testCase.attempts.every(attempt => attempt.outcome.kind === 'passed')))) ? 'pass' : 'fail';
|
|
59
|
+
const thresholdStatus = input.threshold == null
|
|
60
|
+
? 'not_configured'
|
|
61
|
+
: overallMeanReward >= input.threshold ? 'pass' : 'fail';
|
|
58
62
|
return {
|
|
59
63
|
report: {
|
|
60
|
-
version:
|
|
64
|
+
version: 3,
|
|
61
65
|
timestamp: new Date().toISOString(),
|
|
62
66
|
...(input.threshold != null ? { threshold: input.threshold } : {}),
|
|
63
67
|
attempts_requested: attemptsRequested,
|
|
64
68
|
attempts_completed: attemptsCompleted,
|
|
65
69
|
overall_mean_reward: overallMeanReward,
|
|
66
70
|
overall_pass_rate: overallMeanReward,
|
|
67
|
-
|
|
71
|
+
runner_status: runnerStatus,
|
|
72
|
+
threshold_status: thresholdStatus,
|
|
73
|
+
status: runnerStatus === 'fail' || thresholdStatus === 'fail' ? 'fail' : 'pass',
|
|
68
74
|
groups: consolidatedGroups,
|
|
75
|
+
task_inventory: input.taskInventory ?? {
|
|
76
|
+
version: TASK_INVENTORY_VERSION,
|
|
77
|
+
files: input.groups.flatMap(group => group.sourceFile ? [{
|
|
78
|
+
eval_file: group.sourceFile,
|
|
79
|
+
completeness: 'incomplete',
|
|
80
|
+
reason: 'adapter-cannot-prove-completeness',
|
|
81
|
+
tasks: [taskInventoryEntry(group)],
|
|
82
|
+
}] : []),
|
|
83
|
+
},
|
|
69
84
|
...(input.selection ? { selection: input.selection } : {}),
|
|
70
85
|
},
|
|
71
86
|
traces,
|
|
@@ -73,6 +88,13 @@ export function buildPathgradeReport(input) {
|
|
|
73
88
|
warnings,
|
|
74
89
|
};
|
|
75
90
|
}
|
|
91
|
+
function taskInventoryEntry(group) {
|
|
92
|
+
const scored = group.cases.some(testCase => testCase.state !== 'skipped' && testCase.state !== 'pending');
|
|
93
|
+
if (scored)
|
|
94
|
+
return { task_key: group.groupName, state: 'scored' };
|
|
95
|
+
const reason = group.cases.every(testCase => testCase.state === 'skipped') ? 'skipped' : 'pending';
|
|
96
|
+
return { task_key: group.groupName, state: 'not-scored', reason };
|
|
97
|
+
}
|
|
76
98
|
function reportableBuiltCase(testCase) {
|
|
77
99
|
if (testCase.reportable !== undefined)
|
|
78
100
|
return testCase.reportable;
|
|
@@ -185,15 +207,20 @@ function buildEvalReport(groupName, cases, run) {
|
|
|
185
207
|
.filter((score) => score !== undefined);
|
|
186
208
|
const meanReward = average(rewards);
|
|
187
209
|
const metrics = binaryMetrics(cases, run);
|
|
210
|
+
const runnerStatus = cases.every(testCase => (testCase.state !== 'failed'
|
|
211
|
+
&& testCase.attempts.every(attempt => attempt.outcome.kind === 'passed'))) ? 'pass' : 'fail';
|
|
212
|
+
const thresholdStatus = run.threshold == null
|
|
213
|
+
? 'not_configured'
|
|
214
|
+
: meanReward >= run.threshold ? 'pass' : 'fail';
|
|
188
215
|
const skills = new Set();
|
|
189
216
|
for (const trial of trials)
|
|
190
217
|
for (const skill of trial.skills_used ?? [])
|
|
191
218
|
skills.add(skill);
|
|
192
219
|
return {
|
|
193
220
|
task: groupName,
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
221
|
+
runner_status: runnerStatus,
|
|
222
|
+
threshold_status: thresholdStatus,
|
|
223
|
+
status: runnerStatus === 'fail' || thresholdStatus === 'fail' ? 'fail' : 'pass',
|
|
197
224
|
mean_reward: meanReward,
|
|
198
225
|
...(metrics.successRate !== undefined ? { success_rate: metrics.successRate } : {}),
|
|
199
226
|
...(metrics.passAtK ? {
|
|
@@ -1,30 +1,87 @@
|
|
|
1
1
|
export function parsePathgradeReport(value) {
|
|
2
2
|
if (!isRecord(value)
|
|
3
|
-
|| (value.version !== 1 && value.version !== 2)
|
|
3
|
+
|| (value.version !== 1 && value.version !== 2 && value.version !== 3)
|
|
4
4
|
|| typeof value.timestamp !== 'string'
|
|
5
5
|
|| typeof value.overall_pass_rate !== 'number'
|
|
6
|
-
|| (value.version
|
|
6
|
+
|| (value.version !== 1 && (typeof value.overall_mean_reward !== 'number' || !validAttemptCounts(value)))
|
|
7
7
|
|| !optionalNumber(value.threshold)
|
|
8
8
|
|| (value.status !== 'pass' && value.status !== 'fail')
|
|
9
|
+
|| !optionalGateStatus(value.runner_status)
|
|
10
|
+
|| !optionalThresholdStatus(value.threshold_status)
|
|
9
11
|
|| (value.run_kind !== undefined && value.run_kind !== 'evaluation' && value.run_kind !== 'no-affected')
|
|
10
12
|
|| !Array.isArray(value.groups)
|
|
11
13
|
|| !value.groups.every(group => isReportGroup(group, value.version))
|
|
14
|
+
|| (value.version === 3 && !isTaskInventory(value.task_inventory))
|
|
12
15
|
|| !isSelection(value.selection)) {
|
|
13
16
|
throw new Error('PathGrade results.json is missing or has an unsupported schema');
|
|
14
17
|
}
|
|
15
|
-
|
|
18
|
+
const threshold = value.threshold;
|
|
19
|
+
const groups = value.groups.map(group => {
|
|
20
|
+
const runnerStatus = deriveRunnerStatus(group);
|
|
21
|
+
const thresholdStatus = deriveThresholdStatus(group.mean_reward ?? group.pass_rate, threshold);
|
|
22
|
+
return {
|
|
23
|
+
...group,
|
|
24
|
+
runner_status: runnerStatus,
|
|
25
|
+
threshold_status: thresholdStatus,
|
|
26
|
+
status: composeStatus(runnerStatus, thresholdStatus),
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
const legacyFailure = value.runner_status === undefined && value.status === 'fail' && groups.length === 0;
|
|
30
|
+
const runnerStatus = value.runner_status !== 'fail' && !legacyFailure
|
|
31
|
+
&& (groups.length > 0 || value.runner_status === 'pass' || value.status === 'pass')
|
|
32
|
+
&& groups.every(group => group.runner_status === 'pass') ? 'pass' : 'fail';
|
|
33
|
+
const thresholdStatus = deriveThresholdStatus(value.overall_mean_reward ?? value.overall_pass_rate, threshold);
|
|
34
|
+
return {
|
|
35
|
+
...value,
|
|
36
|
+
groups,
|
|
37
|
+
runner_status: runnerStatus,
|
|
38
|
+
threshold_status: thresholdStatus,
|
|
39
|
+
status: composeStatus(runnerStatus, thresholdStatus),
|
|
40
|
+
};
|
|
16
41
|
}
|
|
17
42
|
function isReportGroup(value, version) {
|
|
18
43
|
return isRecord(value)
|
|
19
44
|
&& typeof value.task === 'string'
|
|
20
45
|
&& (version === 1 ? legacyMetrics(value) : schemaV2Metrics(value))
|
|
46
|
+
&& optionalGateStatus(value.runner_status)
|
|
47
|
+
&& optionalThresholdStatus(value.threshold_status)
|
|
21
48
|
&& optionalString(value.source_file)
|
|
22
49
|
&& typeof value.trace_file === 'string'
|
|
23
50
|
&& Array.isArray(value.skills_used)
|
|
24
51
|
&& value.skills_used.every(item => typeof item === 'string')
|
|
25
52
|
&& Array.isArray(value.trials)
|
|
26
53
|
&& value.trials.every(isTrial)
|
|
27
|
-
&& (
|
|
54
|
+
&& (version === 3
|
|
55
|
+
? isComparisonContractV2(value.comparison_contract)
|
|
56
|
+
: value.comparison_contract === undefined || isComparisonContract(value.comparison_contract));
|
|
57
|
+
}
|
|
58
|
+
function deriveRunnerStatus(value) {
|
|
59
|
+
if (value.runner_status === 'fail')
|
|
60
|
+
return 'fail';
|
|
61
|
+
const outcomes = value.trials
|
|
62
|
+
.map(trial => trial.runner_outcome)
|
|
63
|
+
.filter(outcome => outcome !== undefined);
|
|
64
|
+
if (outcomes.length > 0)
|
|
65
|
+
return outcomes.every(outcome => outcome === 'passed') ? 'pass' : 'fail';
|
|
66
|
+
if (value.runner_status === 'pass')
|
|
67
|
+
return value.runner_status;
|
|
68
|
+
if (value.status === 'pass' || value.status === 'fail')
|
|
69
|
+
return value.status;
|
|
70
|
+
return value.pass_rate === 1 ? 'pass' : 'fail';
|
|
71
|
+
}
|
|
72
|
+
function deriveThresholdStatus(reward, threshold) {
|
|
73
|
+
if (threshold === undefined)
|
|
74
|
+
return 'not_configured';
|
|
75
|
+
return typeof reward === 'number' && reward >= threshold ? 'pass' : 'fail';
|
|
76
|
+
}
|
|
77
|
+
function composeStatus(runnerStatus, thresholdStatus) {
|
|
78
|
+
return runnerStatus === 'fail' || thresholdStatus === 'fail' ? 'fail' : 'pass';
|
|
79
|
+
}
|
|
80
|
+
function optionalGateStatus(value) {
|
|
81
|
+
return value === undefined || value === 'pass' || value === 'fail';
|
|
82
|
+
}
|
|
83
|
+
function optionalThresholdStatus(value) {
|
|
84
|
+
return optionalGateStatus(value) || value === 'not_configured';
|
|
28
85
|
}
|
|
29
86
|
function isTrial(value) {
|
|
30
87
|
return isRecord(value)
|
|
@@ -40,7 +97,8 @@ function isTrial(value) {
|
|
|
40
97
|
&& value.scorer_results.every(scorer => isRecord(scorer)
|
|
41
98
|
&& typeof scorer.scorer_type === 'string'
|
|
42
99
|
&& typeof scorer.score === 'number'
|
|
43
|
-
&& typeof scorer.weight === 'number'
|
|
100
|
+
&& typeof scorer.weight === 'number'
|
|
101
|
+
&& (scorer.status === undefined || scorer.status === 'ok' || scorer.status === 'error' || scorer.status === 'skipped'));
|
|
44
102
|
}
|
|
45
103
|
function legacyMetrics(value) {
|
|
46
104
|
return typeof value.pass_rate === 'number'
|
|
@@ -82,6 +140,9 @@ function isSelection(value) {
|
|
|
82
140
|
&& item.reason === 'no-matching-deps'));
|
|
83
141
|
}
|
|
84
142
|
function isComparisonContract(value) {
|
|
143
|
+
return isComparisonContractV1(value) || isComparisonContractV2(value);
|
|
144
|
+
}
|
|
145
|
+
function isComparisonContractV1(value) {
|
|
85
146
|
return isRecord(value)
|
|
86
147
|
&& value.version === 1
|
|
87
148
|
&& optionalString(value.definition_revision)
|
|
@@ -95,6 +156,65 @@ function isComparisonContract(value) {
|
|
|
95
156
|
|| reason === 'runtime_metadata_missing'
|
|
96
157
|
|| reason === 'sampling_incomplete')));
|
|
97
158
|
}
|
|
159
|
+
function isComparisonContractV2(value) {
|
|
160
|
+
if (!isRecord(value) || value.version !== 2 || value.report_schema_revision !== 'pathgrade-results-v3')
|
|
161
|
+
return false;
|
|
162
|
+
const comparisonInputs = value.comparison_inputs;
|
|
163
|
+
const definitionComplete = isRecord(comparisonInputs) && comparisonInputs.state === 'resolved';
|
|
164
|
+
const scorerComplete = isState(value.scorer_revision_state, 'complete');
|
|
165
|
+
const runtimeComplete = isState(value.runtime_revision_state, 'complete');
|
|
166
|
+
const samplingComplete = isState(value.sampling_revision_state, 'complete');
|
|
167
|
+
return isComparisonInputs(comparisonInputs)
|
|
168
|
+
&& (definitionComplete ? typeof value.definition_revision === 'string' : value.definition_revision === undefined)
|
|
169
|
+
&& isRevisionState(value.scorer_revision_state, ['explicit-revision-required', 'metadata-missing'])
|
|
170
|
+
&& (scorerComplete ? typeof value.scorer_revision === 'string' : value.scorer_revision === undefined)
|
|
171
|
+
&& isRevisionState(value.runtime_revision_state, ['metadata-missing'])
|
|
172
|
+
&& (runtimeComplete
|
|
173
|
+
? typeof value.runtime_revision === 'string' && isRuntimeComponents(value.runtime_components)
|
|
174
|
+
: value.runtime_revision === undefined && value.runtime_components === undefined)
|
|
175
|
+
&& isRevisionState(value.sampling_revision_state, ['sampling-incomplete'])
|
|
176
|
+
&& (samplingComplete ? typeof value.sampling_revision === 'string' : value.sampling_revision === undefined);
|
|
177
|
+
}
|
|
178
|
+
function isComparisonInputs(value) {
|
|
179
|
+
if (!isRecord(value))
|
|
180
|
+
return false;
|
|
181
|
+
if (value.state === 'missing')
|
|
182
|
+
return true;
|
|
183
|
+
if (value.state === 'invalid') {
|
|
184
|
+
return value.reason === 'malformed-declaration' || value.reason === 'no-matches'
|
|
185
|
+
|| value.reason === 'outside-repository' || value.reason === 'unreadable-input';
|
|
186
|
+
}
|
|
187
|
+
return value.state === 'resolved'
|
|
188
|
+
&& Array.isArray(value.declarations) && value.declarations.every(item => typeof item === 'string')
|
|
189
|
+
&& Array.isArray(value.files) && value.files.every(file => isRecord(file)
|
|
190
|
+
&& typeof file.path === 'string' && typeof file.revision === 'string')
|
|
191
|
+
&& typeof value.revision === 'string';
|
|
192
|
+
}
|
|
193
|
+
function isRevisionState(value, reasons) {
|
|
194
|
+
return isState(value, 'complete') || (isRecord(value) && value.state === 'incomplete'
|
|
195
|
+
&& typeof value.reason === 'string' && reasons.includes(value.reason));
|
|
196
|
+
}
|
|
197
|
+
function isState(value, state) {
|
|
198
|
+
return isRecord(value) && value.state === state;
|
|
199
|
+
}
|
|
200
|
+
function isRuntimeComponents(value) {
|
|
201
|
+
return isRecord(value) && ['agent_name', 'model', 'transport', 'interaction_mode']
|
|
202
|
+
.every(key => typeof value[key] === 'string')
|
|
203
|
+
&& optionalString(value.flow);
|
|
204
|
+
}
|
|
205
|
+
function isTaskInventory(value) {
|
|
206
|
+
return isRecord(value) && value.version === 1 && Array.isArray(value.files)
|
|
207
|
+
&& value.files.every(file => isRecord(file)
|
|
208
|
+
&& typeof file.eval_file === 'string'
|
|
209
|
+
&& (file.completeness === 'complete' || (file.completeness === 'incomplete'
|
|
210
|
+
&& (file.reason === 'adapter-cannot-prove-completeness'
|
|
211
|
+
|| file.reason === 'collection-failed' || file.reason === 'run-incomplete')))
|
|
212
|
+
&& Array.isArray(file.tasks)
|
|
213
|
+
&& file.tasks.every(task => isRecord(task) && typeof task.task_key === 'string'
|
|
214
|
+
&& (task.state === 'scored' || (task.state === 'not-scored'
|
|
215
|
+
&& (task.reason === 'skipped' || task.reason === 'pending'
|
|
216
|
+
|| task.reason === 'timed-out' || task.reason === 'failed-before-evaluation')))));
|
|
217
|
+
}
|
|
98
218
|
function optionalString(value) {
|
|
99
219
|
return value === undefined || typeof value === 'string';
|
|
100
220
|
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import type { ComparisonInputsState } from './reliability-contract.js';
|
|
1
2
|
export interface SourceMetadata {
|
|
2
3
|
sourceFile?: string;
|
|
3
4
|
sourceRevision?: string;
|
|
5
|
+
comparisonInputs?: ComparisonInputsState;
|
|
4
6
|
}
|
|
5
7
|
export declare function resolveSourceMetadata(filePath: string | undefined, cwd: string): SourceMetadata;
|
|
8
|
+
export declare function createSourceMetadataResolver(cwd: string): (filePath: string | undefined) => SourceMetadata;
|
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { readdirSync } from 'node:fs';
|
|
3
4
|
import * as path from 'node:path';
|
|
5
|
+
import picomatch from 'picomatch';
|
|
6
|
+
import { canonicalizeJson } from '../core/canonical-json.js';
|
|
7
|
+
import { parsePathgradeMeta } from '../affected/meta.js';
|
|
4
8
|
export function resolveSourceMetadata(filePath, cwd) {
|
|
9
|
+
return resolveSourceMetadataWithFiles(filePath, cwd, () => listRepositoryFiles(cwd));
|
|
10
|
+
}
|
|
11
|
+
export function createSourceMetadataResolver(cwd) {
|
|
12
|
+
let repositoryFiles;
|
|
13
|
+
return filePath => resolveSourceMetadataWithFiles(filePath, cwd, () => repositoryFiles ??= listRepositoryFiles(cwd));
|
|
14
|
+
}
|
|
15
|
+
function resolveSourceMetadataWithFiles(filePath, cwd, repositoryFiles) {
|
|
5
16
|
if (!filePath)
|
|
6
17
|
return {};
|
|
7
18
|
const absolute = path.resolve(cwd, filePath);
|
|
@@ -11,10 +22,64 @@ export function resolveSourceMetadata(filePath, cwd) {
|
|
|
11
22
|
try {
|
|
12
23
|
return {
|
|
13
24
|
sourceFile: relative,
|
|
14
|
-
sourceRevision:
|
|
25
|
+
sourceRevision: digest(readFileSync(absolute)),
|
|
26
|
+
comparisonInputs: resolveComparisonInputs(absolute, cwd, repositoryFiles),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return { sourceFile: relative, comparisonInputs: { state: 'invalid', reason: 'unreadable-input' } };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function resolveComparisonInputs(evalFile, cwd, repositoryFiles) {
|
|
34
|
+
let declarations;
|
|
35
|
+
try {
|
|
36
|
+
declarations = parsePathgradeMeta(evalFile)?.comparisonInputs;
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
const message = error instanceof Error ? error.message : '';
|
|
40
|
+
return {
|
|
41
|
+
state: 'invalid',
|
|
42
|
+
reason: message.includes('escapes the repo root') ? 'outside-repository' : 'malformed-declaration',
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
if (declarations === undefined)
|
|
46
|
+
return { state: 'missing' };
|
|
47
|
+
const normalized = declarations.map(value => value.replaceAll('\\', '/').normalize('NFC'));
|
|
48
|
+
const matched = [...new Set(normalized.flatMap(declaration => {
|
|
49
|
+
const matches = picomatch(declaration, { dot: true });
|
|
50
|
+
return repositoryFiles().filter(file => matches(file));
|
|
51
|
+
}))].toSorted();
|
|
52
|
+
if (normalized.length > 0 && matched.length === 0)
|
|
53
|
+
return { state: 'invalid', reason: 'no-matches' };
|
|
54
|
+
try {
|
|
55
|
+
const files = matched.map(file => ({ path: file, revision: digest(readFileSync(path.join(cwd, file))) }));
|
|
56
|
+
return {
|
|
57
|
+
state: 'resolved',
|
|
58
|
+
declarations: normalized,
|
|
59
|
+
files,
|
|
60
|
+
revision: digest(canonicalizeJson({ declarations: normalized, files })),
|
|
15
61
|
};
|
|
16
62
|
}
|
|
17
63
|
catch {
|
|
18
|
-
return {
|
|
64
|
+
return { state: 'invalid', reason: 'unreadable-input' };
|
|
19
65
|
}
|
|
20
66
|
}
|
|
67
|
+
function listRepositoryFiles(cwd) {
|
|
68
|
+
const files = [];
|
|
69
|
+
const walk = (directory, relative) => {
|
|
70
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
71
|
+
const childRelative = relative ? `${relative}/${entry.name}` : entry.name;
|
|
72
|
+
if (!relative && (entry.name === '.git' || entry.name === '.pathgrade'))
|
|
73
|
+
continue;
|
|
74
|
+
if (entry.isDirectory())
|
|
75
|
+
walk(path.join(directory, entry.name), childRelative);
|
|
76
|
+
else if (entry.isFile())
|
|
77
|
+
files.push(childRelative);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
walk(path.resolve(cwd), '');
|
|
81
|
+
return files.toSorted();
|
|
82
|
+
}
|
|
83
|
+
function digest(value) {
|
|
84
|
+
return `sha256:${createHash('sha256').update(value).digest('hex')}`;
|
|
85
|
+
}
|
|
@@ -1,19 +1,23 @@
|
|
|
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
|
+
import type { AttemptOutcome, RunStatus } from '../runners/model.js';
|
|
4
4
|
import type { PathgradeReport, PathgradeSelectionReport, TrialResult } from '../types.js';
|
|
5
|
+
import type { ComparisonInputsState, TaskInventory } from './reliability-contract.js';
|
|
5
6
|
export type ReportCaseState = 'passed' | 'failed' | 'skipped' | 'pending';
|
|
6
7
|
export interface ReportRunInput {
|
|
8
|
+
runStatus?: RunStatus;
|
|
7
9
|
threshold?: number;
|
|
8
10
|
selection?: PathgradeSelectionReport;
|
|
9
11
|
attemptsRequested?: number;
|
|
10
12
|
attemptsCompleted?: number;
|
|
13
|
+
taskInventory?: TaskInventory;
|
|
11
14
|
groups: ReportGroupInput[];
|
|
12
15
|
}
|
|
13
16
|
export interface ReportGroupInput {
|
|
14
17
|
groupName: string;
|
|
15
18
|
sourceFile?: string;
|
|
16
19
|
sourceRevision?: string;
|
|
20
|
+
comparisonInputs?: ComparisonInputsState;
|
|
17
21
|
cases: ReportCaseInput[];
|
|
18
22
|
}
|
|
19
23
|
export interface ReportCaseInput {
|
|
@@ -3,4 +3,5 @@ import type { AdapterRunHandle } from './adapter.js';
|
|
|
3
3
|
import type { NormalizedRunSnapshot } from './model.js';
|
|
4
4
|
export declare function buildNormalizedRunSnapshotFromReportGroups(run: AdapterRunHandle, groups: ReportGroupInput[], options?: {
|
|
5
5
|
cwd?: string;
|
|
6
|
+
discoveredFiles?: readonly string[];
|
|
6
7
|
}): NormalizedRunSnapshot;
|