@wix/pathgrade 0.37.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.
- package/README.md +116 -32
- package/dist/adapter-kit/index.d.ts +17 -0
- package/dist/adapter-kit/index.js +14 -0
- package/dist/adapters/jest/index.d.ts +3 -0
- package/dist/adapters/jest/index.js +3 -0
- package/dist/adapters/jest/invocation-adapter.d.ts +15 -0
- package/dist/adapters/jest/invocation-adapter.js +115 -0
- package/dist/adapters/jest/lifecycle.d.ts +21 -0
- package/dist/adapters/jest/lifecycle.js +62 -0
- package/dist/adapters/jest/metadata.d.ts +6 -0
- package/dist/adapters/jest/metadata.js +35 -0
- package/dist/adapters/jest/reporter.cjs +10 -0
- package/dist/adapters/jest/reporter.d.ts +5 -0
- package/dist/adapters/jest/reporter.js +67 -0
- package/dist/adapters/jest/results.d.ts +31 -0
- package/dist/adapters/jest/results.js +144 -0
- package/dist/adapters/jest/runner-adapter.d.ts +9 -0
- package/dist/adapters/jest/runner-adapter.js +49 -0
- package/dist/adapters/jest/setup.d.ts +1 -0
- package/dist/adapters/jest/setup.js +10 -0
- package/dist/adapters/node-test/index.d.ts +8 -0
- package/dist/adapters/node-test/index.js +106 -0
- package/dist/adapters/node-test/invocation-adapter.d.ts +5 -0
- package/dist/adapters/node-test/invocation-adapter.js +47 -0
- package/dist/adapters/node-test/runner-adapter.d.ts +2 -0
- package/dist/adapters/node-test/runner-adapter.js +116 -0
- package/dist/adapters/vitest/index.d.ts +9 -0
- package/dist/adapters/vitest/index.js +82 -0
- package/dist/adapters/vitest/lifecycle.d.ts +24 -0
- package/dist/adapters/vitest/lifecycle.js +41 -0
- package/dist/adapters/vitest/reporter.d.ts +12 -0
- package/dist/adapters/vitest/reporter.js +75 -0
- package/dist/adapters/vitest/setup.d.ts +1 -0
- package/dist/adapters/vitest/setup.js +3 -0
- package/dist/agents/codex-app-server/agent.js +5 -1
- package/dist/agents/codex-app-server/protocol/index.js +3 -3
- package/dist/agents/codex-app-server/turn-completion.d.ts +1 -0
- package/dist/agents/codex-app-server/turn-completion.js +9 -0
- package/dist/commands/affected.js +22 -15
- package/dist/commands/run-args.d.ts +4 -3
- package/dist/commands/run-args.js +12 -6
- package/dist/commands/run-changed.d.ts +4 -4
- package/dist/commands/run-changed.js +45 -58
- package/dist/config/pathgrade.d.ts +48 -0
- package/dist/config/pathgrade.js +204 -0
- package/dist/pathgrade.d.ts +2 -2
- package/dist/pathgrade.js +35 -34
- package/dist/plugin/index.d.ts +2 -9
- package/dist/plugin/index.js +1 -81
- package/dist/plugin/lifecycle.d.ts +1 -45
- package/dist/plugin/lifecycle.js +1 -146
- package/dist/plugin/reporter.d.ts +1 -37
- package/dist/plugin/reporter.js +1 -280
- package/dist/plugin/setup.d.ts +1 -1
- package/dist/plugin/setup.js +1 -3
- package/dist/providers/copy-filter.d.ts +5 -0
- package/dist/providers/copy-filter.js +9 -0
- package/dist/providers/sandbox.js +2 -2
- package/dist/providers/workspace.d.ts +6 -0
- package/dist/providers/workspace.js +34 -14
- package/dist/reporters/diagnostics.d.ts +2 -40
- package/dist/reporters/diagnostics.js +1 -80
- package/dist/reporters/report-summary.d.ts +6 -0
- package/dist/reporters/report-summary.js +29 -0
- package/dist/reporting/artifacts.d.ts +2 -0
- package/dist/reporting/artifacts.js +20 -0
- package/dist/reporting/core.d.ts +2 -0
- package/dist/reporting/core.js +173 -0
- package/dist/reporting/types.d.ts +57 -0
- package/dist/reporting/types.js +1 -0
- package/dist/reporting/vitest-edge.d.ts +1 -0
- package/dist/reporting/vitest-edge.js +1 -0
- package/dist/runners/adapter-loader.d.ts +17 -0
- package/dist/runners/adapter-loader.js +67 -0
- package/dist/runners/adapter.d.ts +65 -0
- package/dist/runners/adapter.js +1 -0
- package/dist/runners/invocation.d.ts +10 -0
- package/dist/runners/invocation.js +1 -0
- package/dist/runners/lifecycle-hooks.d.ts +2 -0
- package/dist/runners/lifecycle-hooks.js +18 -0
- package/dist/runners/model-builders.d.ts +4 -0
- package/dist/runners/model-builders.js +67 -0
- package/dist/runners/model-validation.d.ts +12 -0
- package/dist/runners/model-validation.js +213 -0
- package/dist/runners/model.d.ts +117 -0
- package/dist/runners/model.js +1 -0
- package/dist/runners/orchestrator.d.ts +30 -0
- package/dist/runners/orchestrator.js +74 -0
- package/dist/runners/report-projection.d.ts +6 -0
- package/dist/runners/report-projection.js +62 -0
- package/dist/runners/selection.d.ts +6 -0
- package/dist/runners/selection.js +10 -0
- package/dist/runners/vitest-adapter.d.ts +8 -0
- package/dist/runners/vitest-adapter.js +108 -0
- package/dist/runners/vitest-invocation.d.ts +10 -0
- package/dist/runners/vitest-invocation.js +44 -0
- package/dist/runners/vitest-lifecycle.d.ts +43 -0
- package/dist/runners/vitest-lifecycle.js +150 -0
- package/dist/sdk/agent.js +15 -16
- package/dist/sdk/case-context.d.ts +20 -0
- package/dist/sdk/case-context.js +31 -0
- package/dist/sdk/diagnostics.d.ts +40 -0
- package/dist/sdk/diagnostics.js +80 -0
- package/dist/sdk/eval-runtime.d.ts +4 -0
- package/dist/sdk/eval-runtime.js +11 -1
- package/dist/sdk/evaluate.js +2 -1
- package/dist/sdk/index.d.ts +2 -0
- package/dist/sdk/index.js +1 -0
- package/dist/sdk/lifecycle.d.ts +40 -0
- package/dist/sdk/lifecycle.js +170 -0
- package/dist/sdk/result-capture.d.ts +18 -0
- package/dist/sdk/result-capture.js +49 -0
- package/dist/sdk/types.d.ts +1 -1
- package/dist/types.d.ts +2 -2
- package/dist/utils/llm-providers/anthropic.js +9 -2
- package/package.json +56 -4
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
export function validateNormalizedRunSnapshot(snapshot, options) {
|
|
2
|
+
const errors = [];
|
|
3
|
+
validateRun(snapshot, options.completeness, errors);
|
|
4
|
+
const unitIds = validateUnits(snapshot, errors);
|
|
5
|
+
validateCases(snapshot, options.completeness, unitIds, errors);
|
|
6
|
+
return { ok: errors.length === 0, errors };
|
|
7
|
+
}
|
|
8
|
+
function validateRun(snapshot, completeness, errors) {
|
|
9
|
+
if (snapshot.version !== 1) {
|
|
10
|
+
errors.push({ path: 'version', message: 'Normalized run snapshot version must be 1.' });
|
|
11
|
+
}
|
|
12
|
+
if (snapshot.completeness !== completeness) {
|
|
13
|
+
errors.push({ path: 'completeness', message: `Expected ${completeness} snapshot.` });
|
|
14
|
+
}
|
|
15
|
+
if (!snapshot.model.run.id) {
|
|
16
|
+
errors.push({ path: 'model.run.id', message: 'Run id is required.' });
|
|
17
|
+
}
|
|
18
|
+
if (!snapshot.model.run.adapterName) {
|
|
19
|
+
errors.push({ path: 'model.run.adapterName', message: 'Run adapterName is required.' });
|
|
20
|
+
}
|
|
21
|
+
if (completeness === 'final' && snapshot.model.run.status === 'parked' && (snapshot.model.run.diagnostics ?? []).length === 0) {
|
|
22
|
+
errors.push({ path: 'model.run.diagnostics', message: 'Final parked snapshots require diagnostics explaining why parked is terminal.' });
|
|
23
|
+
}
|
|
24
|
+
validateNativeReferences(snapshot.model.run.nativeReferences, 'model.run.nativeReferences', errors);
|
|
25
|
+
for (const [diagnosticIndex, diagnostic] of (snapshot.model.run.diagnostics ?? []).entries()) {
|
|
26
|
+
validateNativeReferences(diagnostic.nativeReferences, `model.run.diagnostics.${diagnosticIndex}.nativeReferences`, errors);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function validateUnits(snapshot, errors) {
|
|
30
|
+
const unitIds = new Set();
|
|
31
|
+
for (const [unitIndex, unit] of snapshot.model.units.entries()) {
|
|
32
|
+
if (!unit.id) {
|
|
33
|
+
errors.push({ path: `model.units.${unitIndex}.id`, message: 'Eval unit id is required.' });
|
|
34
|
+
}
|
|
35
|
+
else if (unitIds.has(unit.id)) {
|
|
36
|
+
errors.push({ path: `model.units.${unitIndex}.id`, message: 'Eval unit id must be unique.' });
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
unitIds.add(unit.id);
|
|
40
|
+
}
|
|
41
|
+
if (unit.runId !== snapshot.model.run.id) {
|
|
42
|
+
errors.push({ path: `model.units.${unitIndex}.runId`, message: 'Eval unit runId must reference the run.' });
|
|
43
|
+
}
|
|
44
|
+
if (!unit.displayName) {
|
|
45
|
+
errors.push({ path: `model.units.${unitIndex}.displayName`, message: 'Eval unit displayName is required.' });
|
|
46
|
+
}
|
|
47
|
+
validateNativeReferences(unit.nativeReferences, `model.units.${unitIndex}.nativeReferences`, errors);
|
|
48
|
+
for (const [diagnosticIndex, diagnostic] of (unit.diagnostics ?? []).entries()) {
|
|
49
|
+
validateNativeReferences(diagnostic.nativeReferences, `model.units.${unitIndex}.diagnostics.${diagnosticIndex}.nativeReferences`, errors);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return unitIds;
|
|
53
|
+
}
|
|
54
|
+
function validateCases(snapshot, completeness, unitIds, errors) {
|
|
55
|
+
const caseIds = new Set();
|
|
56
|
+
const attemptIds = new Set();
|
|
57
|
+
const evaluationIds = new Set();
|
|
58
|
+
const assertionIds = new Set();
|
|
59
|
+
for (const [caseIndex, runCase] of snapshot.model.cases.entries()) {
|
|
60
|
+
if (!runCase.id) {
|
|
61
|
+
errors.push({ path: `model.cases.${caseIndex}.id`, message: 'Run case id is required.' });
|
|
62
|
+
}
|
|
63
|
+
else if (caseIds.has(runCase.id)) {
|
|
64
|
+
errors.push({ path: `model.cases.${caseIndex}.id`, message: 'Run case id must be unique.' });
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
caseIds.add(runCase.id);
|
|
68
|
+
}
|
|
69
|
+
if (runCase.runId !== snapshot.model.run.id) {
|
|
70
|
+
errors.push({ path: `model.cases.${caseIndex}.runId`, message: 'Run case runId must reference the run.' });
|
|
71
|
+
}
|
|
72
|
+
if (runCase.unitId && !unitIds.has(runCase.unitId)) {
|
|
73
|
+
errors.push({ path: `model.cases.${caseIndex}.unitId`, message: 'Run case unitId must reference an eval unit.' });
|
|
74
|
+
}
|
|
75
|
+
if (!runCase.name) {
|
|
76
|
+
errors.push({ path: `model.cases.${caseIndex}.name`, message: 'Run case name is required.' });
|
|
77
|
+
}
|
|
78
|
+
validateNativeReferences(runCase.nativeReferences, `model.cases.${caseIndex}.nativeReferences`, errors);
|
|
79
|
+
for (const [diagnosticIndex, diagnostic] of (runCase.diagnostics ?? []).entries()) {
|
|
80
|
+
validateNativeReferences(diagnostic.nativeReferences, `model.cases.${caseIndex}.diagnostics.${diagnosticIndex}.nativeReferences`, errors);
|
|
81
|
+
}
|
|
82
|
+
if (completeness === 'final' && runCase.attempts.length === 0) {
|
|
83
|
+
errors.push({ path: `model.cases.${caseIndex}.attempts`, message: 'Final run cases require at least one attempt.' });
|
|
84
|
+
}
|
|
85
|
+
const evaluations = runCase.attempts.flatMap(attempt => attempt.evaluations ?? []);
|
|
86
|
+
if (runCase.scoringPolicy.kind === 'from-evaluations' && completeness === 'final' && evaluations.length === 0) {
|
|
87
|
+
errors.push({ path: `model.cases.${caseIndex}.scoringPolicy`, message: 'from-evaluations scoring requires at least one evaluation.' });
|
|
88
|
+
}
|
|
89
|
+
if (runCase.scoringPolicy.kind === 'score' && (!Number.isFinite(runCase.scoringPolicy.score) || runCase.scoringPolicy.score < 0 || runCase.scoringPolicy.score > 1)) {
|
|
90
|
+
errors.push({ path: `model.cases.${caseIndex}.scoringPolicy.score`, message: 'Scoring policy score must be a finite number in [0, 1].' });
|
|
91
|
+
}
|
|
92
|
+
if (runCase.scoringPolicy.kind === 'non-scoring' && runCase.scoringPolicy.reason.trim() === '') {
|
|
93
|
+
errors.push({ path: `model.cases.${caseIndex}.scoringPolicy.reason`, message: 'Non-scoring cases require a reason.' });
|
|
94
|
+
}
|
|
95
|
+
for (const [attemptIndex, attempt] of runCase.attempts.entries()) {
|
|
96
|
+
validateAttempt({
|
|
97
|
+
caseIndex,
|
|
98
|
+
attemptIndex,
|
|
99
|
+
attempt,
|
|
100
|
+
runCaseId: runCase.id,
|
|
101
|
+
runCaseState: runCase.state,
|
|
102
|
+
attemptIds,
|
|
103
|
+
evaluationIds,
|
|
104
|
+
assertionIds,
|
|
105
|
+
errors,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function validateAttempt(input) {
|
|
111
|
+
const attemptPath = `model.cases.${input.caseIndex}.attempts.${input.attemptIndex}`;
|
|
112
|
+
if (!input.attempt.id) {
|
|
113
|
+
input.errors.push({ path: `${attemptPath}.id`, message: 'Attempt id is required.' });
|
|
114
|
+
}
|
|
115
|
+
else if (input.attemptIds.has(input.attempt.id)) {
|
|
116
|
+
input.errors.push({ path: `${attemptPath}.id`, message: 'Attempt id must be unique.' });
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
input.attemptIds.add(input.attempt.id);
|
|
120
|
+
}
|
|
121
|
+
if (input.attempt.caseId !== input.runCaseId) {
|
|
122
|
+
input.errors.push({ path: `${attemptPath}.caseId`, message: 'Attempt caseId must reference its run case.' });
|
|
123
|
+
}
|
|
124
|
+
if ((input.runCaseState === 'skipped' || input.runCaseState === 'pending') && input.attempt.outcome.kind !== 'not-run') {
|
|
125
|
+
input.errors.push({ path: `${attemptPath}.outcome`, message: 'Non-executed final cases require a not-run attempt outcome.' });
|
|
126
|
+
}
|
|
127
|
+
for (const [diagnosticIndex, diagnostic] of (input.attempt.diagnostics ?? []).entries()) {
|
|
128
|
+
validateNativeReferences(diagnostic.nativeReferences, `${attemptPath}.diagnostics.${diagnosticIndex}.nativeReferences`, input.errors);
|
|
129
|
+
}
|
|
130
|
+
for (const [evaluationIndex, evaluation] of (input.attempt.evaluations ?? []).entries()) {
|
|
131
|
+
validateEvaluation({
|
|
132
|
+
attemptPath,
|
|
133
|
+
evaluationIndex,
|
|
134
|
+
evaluation,
|
|
135
|
+
attemptId: input.attempt.id,
|
|
136
|
+
evaluationIds: input.evaluationIds,
|
|
137
|
+
errors: input.errors,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
for (const [assertionIndex, assertion] of (input.attempt.assertions ?? []).entries()) {
|
|
141
|
+
validateAssertion({
|
|
142
|
+
attemptPath,
|
|
143
|
+
assertionIndex,
|
|
144
|
+
assertion,
|
|
145
|
+
attemptId: input.attempt.id,
|
|
146
|
+
assertionIds: input.assertionIds,
|
|
147
|
+
errors: input.errors,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function validateEvaluation(input) {
|
|
152
|
+
const evaluationPath = `${input.attemptPath}.evaluations.${input.evaluationIndex}`;
|
|
153
|
+
if (!input.evaluation.id) {
|
|
154
|
+
input.errors.push({ path: `${evaluationPath}.id`, message: 'Evaluation id is required.' });
|
|
155
|
+
}
|
|
156
|
+
else if (input.evaluationIds.has(input.evaluation.id)) {
|
|
157
|
+
input.errors.push({ path: `${evaluationPath}.id`, message: 'Evaluation id must be unique.' });
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
input.evaluationIds.add(input.evaluation.id);
|
|
161
|
+
}
|
|
162
|
+
if (input.evaluation.attemptId !== input.attemptId) {
|
|
163
|
+
input.errors.push({ path: `${evaluationPath}.attemptId`, message: 'Evaluation attemptId must reference its attempt.' });
|
|
164
|
+
}
|
|
165
|
+
if (!Number.isFinite(input.evaluation.score) || input.evaluation.score < 0 || input.evaluation.score > 1) {
|
|
166
|
+
input.errors.push({ path: `${evaluationPath}.score`, message: 'Evaluation score must be a finite number in [0, 1].' });
|
|
167
|
+
}
|
|
168
|
+
validateNativeReferences(input.evaluation.nativeReferences, `${evaluationPath}.nativeReferences`, input.errors);
|
|
169
|
+
}
|
|
170
|
+
function validateAssertion(input) {
|
|
171
|
+
const assertionPath = `${input.attemptPath}.assertions.${input.assertionIndex}`;
|
|
172
|
+
if (!input.assertion.id) {
|
|
173
|
+
input.errors.push({ path: `${assertionPath}.id`, message: 'Assertion id is required.' });
|
|
174
|
+
}
|
|
175
|
+
else if (input.assertionIds.has(input.assertion.id)) {
|
|
176
|
+
input.errors.push({ path: `${assertionPath}.id`, message: 'Assertion id must be unique.' });
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
input.assertionIds.add(input.assertion.id);
|
|
180
|
+
}
|
|
181
|
+
if (input.assertion.attemptId !== input.attemptId) {
|
|
182
|
+
input.errors.push({ path: `${assertionPath}.attemptId`, message: 'Assertion attemptId must reference its attempt.' });
|
|
183
|
+
}
|
|
184
|
+
validateNativeReferences(input.assertion.nativeReferences, `${assertionPath}.nativeReferences`, input.errors);
|
|
185
|
+
}
|
|
186
|
+
function validateNativeReferences(references, path, errors) {
|
|
187
|
+
for (const [index, reference] of (references ?? []).entries()) {
|
|
188
|
+
const referencePath = `${path}.${index}`;
|
|
189
|
+
if (!reference.kind) {
|
|
190
|
+
errors.push({ path: `${referencePath}.kind`, message: 'Native reference kind is required.' });
|
|
191
|
+
}
|
|
192
|
+
if (reference.url && !isSafeUrl(reference.url)) {
|
|
193
|
+
errors.push({ path: `${referencePath}.url`, message: 'Native reference URL must use http: or https:.' });
|
|
194
|
+
}
|
|
195
|
+
for (const [key, value] of Object.entries(reference.metadata ?? {})) {
|
|
196
|
+
if (value !== null
|
|
197
|
+
&& typeof value !== 'string'
|
|
198
|
+
&& typeof value !== 'number'
|
|
199
|
+
&& typeof value !== 'boolean') {
|
|
200
|
+
errors.push({ path: `${referencePath}.metadata.${key}`, message: 'Native reference metadata values must be flat primitives.' });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
function isSafeUrl(value) {
|
|
206
|
+
try {
|
|
207
|
+
const url = new URL(value);
|
|
208
|
+
return url.protocol === 'http:' || url.protocol === 'https:';
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { DiagnosticsReport } from '../sdk/diagnostics.js';
|
|
2
|
+
import type { TrialResult } from '../types.js';
|
|
3
|
+
export interface NormalizedRunSnapshot {
|
|
4
|
+
version: 1;
|
|
5
|
+
completeness: SnapshotCompleteness;
|
|
6
|
+
model: NormalizedRunModel;
|
|
7
|
+
}
|
|
8
|
+
export type SnapshotCompleteness = 'partial' | 'final';
|
|
9
|
+
export interface NormalizedRunModel {
|
|
10
|
+
run: RunRecord;
|
|
11
|
+
units: EvalUnitRecord[];
|
|
12
|
+
cases: RunCaseRecord[];
|
|
13
|
+
}
|
|
14
|
+
export type RunStatus = 'completed' | 'failed' | 'cancelled' | 'timed_out' | 'parked';
|
|
15
|
+
export interface RunRecord {
|
|
16
|
+
id: string;
|
|
17
|
+
adapterName: string;
|
|
18
|
+
status: RunStatus;
|
|
19
|
+
diagnostics?: Diagnostic[];
|
|
20
|
+
nativeReferences?: NativeReference[];
|
|
21
|
+
}
|
|
22
|
+
export interface EvalUnitRecord {
|
|
23
|
+
id: string;
|
|
24
|
+
runId: string;
|
|
25
|
+
displayName: string;
|
|
26
|
+
diagnostics?: Diagnostic[];
|
|
27
|
+
nativeReferences?: NativeReference[];
|
|
28
|
+
groupingHints?: GroupingHint[];
|
|
29
|
+
}
|
|
30
|
+
export type RunCaseState = 'passed' | 'failed' | 'skipped' | 'pending';
|
|
31
|
+
export type ScoringPolicy = {
|
|
32
|
+
kind: 'score';
|
|
33
|
+
score: number;
|
|
34
|
+
} | {
|
|
35
|
+
kind: 'non-scoring';
|
|
36
|
+
reason: string;
|
|
37
|
+
} | {
|
|
38
|
+
kind: 'from-evaluations';
|
|
39
|
+
};
|
|
40
|
+
export interface RunCaseRecord {
|
|
41
|
+
id: string;
|
|
42
|
+
runId: string;
|
|
43
|
+
unitId?: string;
|
|
44
|
+
name: string;
|
|
45
|
+
state: RunCaseState;
|
|
46
|
+
scoringPolicy: ScoringPolicy;
|
|
47
|
+
attempts: AttemptRecord[];
|
|
48
|
+
groupingHints?: GroupingHint[];
|
|
49
|
+
diagnostics?: Diagnostic[];
|
|
50
|
+
nativeReferences?: NativeReference[];
|
|
51
|
+
}
|
|
52
|
+
export type AttemptOutcome = {
|
|
53
|
+
kind: 'passed';
|
|
54
|
+
} | {
|
|
55
|
+
kind: 'failed';
|
|
56
|
+
reason?: string;
|
|
57
|
+
} | {
|
|
58
|
+
kind: 'errored';
|
|
59
|
+
reason?: string;
|
|
60
|
+
} | {
|
|
61
|
+
kind: 'timed-out';
|
|
62
|
+
durationMs?: number;
|
|
63
|
+
} | {
|
|
64
|
+
kind: 'cancelled';
|
|
65
|
+
reason?: string;
|
|
66
|
+
} | {
|
|
67
|
+
kind: 'not-run';
|
|
68
|
+
reason: 'skipped' | 'pending' | 'filtered';
|
|
69
|
+
};
|
|
70
|
+
export interface AttemptRecord {
|
|
71
|
+
id: string;
|
|
72
|
+
caseId: string;
|
|
73
|
+
outcome: AttemptOutcome;
|
|
74
|
+
durationMs?: number;
|
|
75
|
+
evaluations?: EvaluationRecord[];
|
|
76
|
+
assertions?: AssertionRecord[];
|
|
77
|
+
diagnostics?: Diagnostic[];
|
|
78
|
+
}
|
|
79
|
+
export interface EvaluationRecord {
|
|
80
|
+
id: string;
|
|
81
|
+
attemptId: string;
|
|
82
|
+
score: number;
|
|
83
|
+
name?: string;
|
|
84
|
+
diagnostics?: DiagnosticsReport;
|
|
85
|
+
trial?: TrialResult;
|
|
86
|
+
nativeReferences?: NativeReference[];
|
|
87
|
+
}
|
|
88
|
+
export interface AssertionRecord {
|
|
89
|
+
id: string;
|
|
90
|
+
attemptId: string;
|
|
91
|
+
name: string;
|
|
92
|
+
status: 'passed' | 'failed' | 'skipped' | 'unknown';
|
|
93
|
+
severity?: 'info' | 'warning' | 'error' | 'fatal';
|
|
94
|
+
message?: string;
|
|
95
|
+
evidence?: string;
|
|
96
|
+
nativeReferences?: NativeReference[];
|
|
97
|
+
}
|
|
98
|
+
export interface Diagnostic {
|
|
99
|
+
severity: 'info' | 'warning' | 'error';
|
|
100
|
+
message: string;
|
|
101
|
+
code?: string;
|
|
102
|
+
detail?: string;
|
|
103
|
+
nativeReferences?: NativeReference[];
|
|
104
|
+
}
|
|
105
|
+
export interface NativeReference {
|
|
106
|
+
kind: string;
|
|
107
|
+
id?: string;
|
|
108
|
+
label?: string;
|
|
109
|
+
url?: string;
|
|
110
|
+
metadata?: Record<string, string | number | boolean | null>;
|
|
111
|
+
}
|
|
112
|
+
export interface GroupingHint {
|
|
113
|
+
kind: 'source' | 'suite' | 'dataset' | 'adapter' | 'custom';
|
|
114
|
+
key: string;
|
|
115
|
+
label: string;
|
|
116
|
+
order?: number;
|
|
117
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { PathgradeSelectionReport } from '../types.js';
|
|
2
|
+
import type { ReportSummaryGroup } from '../reporting/types.js';
|
|
3
|
+
import type { AdapterDiscoveryInput, AdapterLifecycleHooks, RunnerAdapter } from './adapter.js';
|
|
4
|
+
export type AdapterReporterMode = 'cli' | 'browser' | 'json';
|
|
5
|
+
export interface PathgradeRunOptions {
|
|
6
|
+
cwd: string;
|
|
7
|
+
discovery: AdapterDiscoveryInput;
|
|
8
|
+
runnerArgs: string[];
|
|
9
|
+
env: NodeJS.ProcessEnv;
|
|
10
|
+
artifactRoot: string;
|
|
11
|
+
reporterMode?: AdapterReporterMode;
|
|
12
|
+
threshold?: number;
|
|
13
|
+
selection?: PathgradeSelectionReport;
|
|
14
|
+
lifecycle?: AdapterLifecycleHooks;
|
|
15
|
+
signal?: AbortSignal;
|
|
16
|
+
printSummary?: (summaries: ReportSummaryGroup[]) => void;
|
|
17
|
+
openBrowser?: () => void | Promise<void>;
|
|
18
|
+
loadSelection?: () => Promise<PathgradeSelectionReport | undefined>;
|
|
19
|
+
onThresholdFailure?: (input: {
|
|
20
|
+
overallPassRate: number;
|
|
21
|
+
threshold: number;
|
|
22
|
+
}) => void;
|
|
23
|
+
writeEmptyReport?: boolean;
|
|
24
|
+
log?: (message: string) => void;
|
|
25
|
+
warn?: (message: string) => void;
|
|
26
|
+
}
|
|
27
|
+
export declare function runWithAdapter(input: {
|
|
28
|
+
adapter: RunnerAdapter;
|
|
29
|
+
options: PathgradeRunOptions;
|
|
30
|
+
}): Promise<number>;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { buildPathgradeReport } from '../reporting/core.js';
|
|
2
|
+
import { writePathgradeArtifacts } from '../reporting/artifacts.js';
|
|
3
|
+
import { projectNormalizedRunSnapshotToReportInput } from './report-projection.js';
|
|
4
|
+
export async function runWithAdapter(input) {
|
|
5
|
+
const { adapter, options } = input;
|
|
6
|
+
const lifecycle = options.lifecycle ?? createNoopLifecycleHooks();
|
|
7
|
+
try {
|
|
8
|
+
const selection = options.selection ?? options.discovery.selection;
|
|
9
|
+
const discovered = await adapter.discover({
|
|
10
|
+
...options.discovery,
|
|
11
|
+
...(selection ? { selection } : {}),
|
|
12
|
+
});
|
|
13
|
+
const run = await adapter.invoke({
|
|
14
|
+
discovered,
|
|
15
|
+
argv: options.runnerArgs,
|
|
16
|
+
env: options.env,
|
|
17
|
+
lifecycle,
|
|
18
|
+
signal: options.signal,
|
|
19
|
+
});
|
|
20
|
+
const runExitCode = normalizeRunExitCode(run.status, run.exitCode);
|
|
21
|
+
const reportInput = projectNormalizedRunSnapshotToReportInput(await adapter.collectNormalizedRunSnapshot(run), { selection });
|
|
22
|
+
let built = buildPathgradeReport({
|
|
23
|
+
threshold: options.threshold,
|
|
24
|
+
...reportInput,
|
|
25
|
+
});
|
|
26
|
+
for (const warning of built.warnings) {
|
|
27
|
+
options.warn?.(` [pathgrade] warning: ${warning}`);
|
|
28
|
+
}
|
|
29
|
+
if (built.report.groups.length === 0 && options.writeEmptyReport === false) {
|
|
30
|
+
return runExitCode;
|
|
31
|
+
}
|
|
32
|
+
const loadedSelection = await options.loadSelection?.();
|
|
33
|
+
if (loadedSelection) {
|
|
34
|
+
built = buildPathgradeReport({
|
|
35
|
+
threshold: options.threshold,
|
|
36
|
+
selection: loadedSelection,
|
|
37
|
+
groups: reportInput.groups,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
const mode = options.reporterMode ?? 'cli';
|
|
41
|
+
if (mode === 'cli' || mode === 'browser') {
|
|
42
|
+
options.printSummary?.(built.summaries);
|
|
43
|
+
}
|
|
44
|
+
await writePathgradeArtifacts(options.artifactRoot, built);
|
|
45
|
+
options.log?.(`results:${options.artifactRoot}`);
|
|
46
|
+
if (mode === 'browser') {
|
|
47
|
+
await options.openBrowser?.();
|
|
48
|
+
}
|
|
49
|
+
if (options.threshold != null && built.report.status === 'fail') {
|
|
50
|
+
options.onThresholdFailure?.({
|
|
51
|
+
overallPassRate: built.report.overall_pass_rate,
|
|
52
|
+
threshold: options.threshold,
|
|
53
|
+
});
|
|
54
|
+
return runExitCode === 0 ? 1 : runExitCode;
|
|
55
|
+
}
|
|
56
|
+
return runExitCode;
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
await lifecycle.cleanupRun();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function normalizeRunExitCode(status, exitCode) {
|
|
63
|
+
if (status === 'completed')
|
|
64
|
+
return exitCode;
|
|
65
|
+
return exitCode === 0 ? 1 : exitCode;
|
|
66
|
+
}
|
|
67
|
+
function createNoopLifecycleHooks() {
|
|
68
|
+
return {
|
|
69
|
+
onResult: () => undefined,
|
|
70
|
+
withCaseContext: (_context, run) => run(),
|
|
71
|
+
flushCase: async () => [],
|
|
72
|
+
cleanupRun: async () => undefined,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ReportRunInput } from '../reporting/types.js';
|
|
2
|
+
import type { PathgradeSelectionReport } from '../types.js';
|
|
3
|
+
import type { NormalizedRunSnapshot } from './model.js';
|
|
4
|
+
export declare function projectNormalizedRunSnapshotToReportInput(snapshot: NormalizedRunSnapshot, options?: {
|
|
5
|
+
selection?: PathgradeSelectionReport;
|
|
6
|
+
}): ReportRunInput;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { validateNormalizedRunSnapshot } from './model-validation.js';
|
|
2
|
+
export function projectNormalizedRunSnapshotToReportInput(snapshot, options = {}) {
|
|
3
|
+
const validation = validateNormalizedRunSnapshot(snapshot, { completeness: 'final' });
|
|
4
|
+
if (!validation.ok) {
|
|
5
|
+
throw new Error(`Invalid normalized run snapshot: ${validation.errors.map(error => `${error.path}: ${error.message}`).join('; ')}`);
|
|
6
|
+
}
|
|
7
|
+
const unitsById = new Map(snapshot.model.units.map(unit => [unit.id, unit]));
|
|
8
|
+
const groups = new Map();
|
|
9
|
+
for (const runCase of snapshot.model.cases) {
|
|
10
|
+
if (runCase.scoringPolicy.kind === 'non-scoring' && runCase.state !== 'skipped' && runCase.state !== 'pending') {
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
const unit = runCase.unitId ? unitsById.get(runCase.unitId) : undefined;
|
|
14
|
+
const groupName = preferredGroupingLabel(runCase.groupingHints)
|
|
15
|
+
?? preferredGroupingLabel(unit?.groupingHints)
|
|
16
|
+
?? unit?.displayName;
|
|
17
|
+
const key = groupName ?? snapshot.model.run.adapterName;
|
|
18
|
+
const group = groups.get(key) ?? { groupName: key, cases: [] };
|
|
19
|
+
group.cases.push({
|
|
20
|
+
caseId: runCase.id,
|
|
21
|
+
name: runCase.name,
|
|
22
|
+
state: runCase.state,
|
|
23
|
+
...(runCase.state === 'skipped' || runCase.state === 'pending' ? { reportable: runCase.scoringPolicy.kind !== 'non-scoring' } : {}),
|
|
24
|
+
runnerDurationMs: totalDurationMs(runCase),
|
|
25
|
+
evaluations: projectedEvaluations(runCase),
|
|
26
|
+
});
|
|
27
|
+
groups.set(key, group);
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
...(options.selection ? { selection: options.selection } : {}),
|
|
31
|
+
groups: [...groups.values()],
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function totalDurationMs(runCase) {
|
|
35
|
+
return runCase.attempts.reduce((sum, attempt) => sum + (attempt.durationMs ?? 0), 0);
|
|
36
|
+
}
|
|
37
|
+
function projectedEvaluations(runCase) {
|
|
38
|
+
if (runCase.scoringPolicy.kind === 'non-scoring') {
|
|
39
|
+
return runCase.attempts.flatMap(attempt => (attempt.evaluations ?? []).map(evaluation => ({
|
|
40
|
+
score: evaluation.score,
|
|
41
|
+
...(evaluation.trial ? { trial: evaluation.trial } : {}),
|
|
42
|
+
...(evaluation.diagnostics ? { diagnostics: evaluation.diagnostics } : {}),
|
|
43
|
+
})));
|
|
44
|
+
}
|
|
45
|
+
if (runCase.scoringPolicy.kind === 'score') {
|
|
46
|
+
return [{ score: runCase.scoringPolicy.score }];
|
|
47
|
+
}
|
|
48
|
+
const evaluation = runCase.attempts.flatMap(attempt => attempt.evaluations ?? []).at(-1);
|
|
49
|
+
if (!evaluation)
|
|
50
|
+
return [];
|
|
51
|
+
return [{
|
|
52
|
+
score: evaluation.score,
|
|
53
|
+
...(evaluation.trial ? { trial: evaluation.trial } : {}),
|
|
54
|
+
...(evaluation.diagnostics ? { diagnostics: evaluation.diagnostics } : {}),
|
|
55
|
+
}];
|
|
56
|
+
}
|
|
57
|
+
function preferredGroupingLabel(hints) {
|
|
58
|
+
return hints
|
|
59
|
+
?.toSorted((left, right) => (left.order ?? 0) - (right.order ?? 0))
|
|
60
|
+
.at(0)
|
|
61
|
+
?.label;
|
|
62
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createNodeTestAdapter } from '../adapters/node-test/runner-adapter.js';
|
|
2
|
+
import { createVitestAdapter } from './vitest-adapter.js';
|
|
3
|
+
export function resolveRunnerAdapter(input) {
|
|
4
|
+
const name = input.adapterName ?? 'vitest';
|
|
5
|
+
if (name === 'vitest')
|
|
6
|
+
return createVitestAdapter(input.vitest);
|
|
7
|
+
if (name === 'node-test')
|
|
8
|
+
return createNodeTestAdapter();
|
|
9
|
+
throw new Error(`Unsupported Pathgrade runner adapter: ${name}`);
|
|
10
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { TestModule } from 'vitest/node';
|
|
2
|
+
import type { ReportGroupInput } from '../reporting/types.js';
|
|
3
|
+
import type { RunnerAdapter } from './adapter.js';
|
|
4
|
+
export interface VitestAdapterOptions {
|
|
5
|
+
testModules?: ReadonlyArray<TestModule>;
|
|
6
|
+
}
|
|
7
|
+
export declare function createVitestAdapter(options?: VitestAdapterOptions): RunnerAdapter;
|
|
8
|
+
export declare function collectVitestReportGroups(testModules: ReadonlyArray<TestModule>): ReportGroupInput[];
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { discoverPathgradeEvalFiles } from '../evals/discovery.js';
|
|
2
|
+
import { buildNormalizedRunSnapshotFromReportGroups } from './model-builders.js';
|
|
3
|
+
import { buildDiagnosticsReport } from '../sdk/diagnostics.js';
|
|
4
|
+
export function createVitestAdapter(options = {}) {
|
|
5
|
+
return {
|
|
6
|
+
name: 'vitest',
|
|
7
|
+
async discover(input) {
|
|
8
|
+
const include = input.include ?? ['**/*.eval.ts'];
|
|
9
|
+
const exclude = input.exclude ?? [];
|
|
10
|
+
const files = discoverPathgradeEvalFiles({
|
|
11
|
+
cwd: input.cwd,
|
|
12
|
+
include,
|
|
13
|
+
exclude,
|
|
14
|
+
});
|
|
15
|
+
return {
|
|
16
|
+
units: files.map(file => ({
|
|
17
|
+
id: file,
|
|
18
|
+
displayName: file,
|
|
19
|
+
sourceRef: file,
|
|
20
|
+
native: { file },
|
|
21
|
+
})),
|
|
22
|
+
};
|
|
23
|
+
},
|
|
24
|
+
async invoke(input) {
|
|
25
|
+
return {
|
|
26
|
+
adapterName: this.name,
|
|
27
|
+
status: input.signal?.aborted ? 'cancelled' : 'completed',
|
|
28
|
+
exitCode: input.signal?.aborted ? 1 : 0,
|
|
29
|
+
native: {
|
|
30
|
+
testModules: options.testModules ?? [],
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
async collectNormalizedRunSnapshot(run) {
|
|
35
|
+
return buildNormalizedRunSnapshotFromReportGroups(run, collectVitestReportGroups(readVitestRunNative(run).testModules));
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export function collectVitestReportGroups(testModules) {
|
|
40
|
+
const groupMap = new Map();
|
|
41
|
+
for (const mod of testModules) {
|
|
42
|
+
for (const testCase of mod.children.allTests()) {
|
|
43
|
+
const groupName = getGroupName(testCase);
|
|
44
|
+
const entry = toReportCaseInput(testCase, groupName);
|
|
45
|
+
if (!groupMap.has(groupName)) {
|
|
46
|
+
groupMap.set(groupName, []);
|
|
47
|
+
}
|
|
48
|
+
groupMap.get(groupName).push(entry);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return Array.from(groupMap.entries()).map(([groupName, cases]) => ({
|
|
52
|
+
groupName,
|
|
53
|
+
cases,
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
function readVitestRunNative(run) {
|
|
57
|
+
if (isVitestRunNative(run.native))
|
|
58
|
+
return run.native;
|
|
59
|
+
return { testModules: [] };
|
|
60
|
+
}
|
|
61
|
+
function isVitestRunNative(native) {
|
|
62
|
+
return typeof native === 'object'
|
|
63
|
+
&& native !== null
|
|
64
|
+
&& Array.isArray(native.testModules);
|
|
65
|
+
}
|
|
66
|
+
function getGroupName(testCase) {
|
|
67
|
+
const modulePath = testCase.module.relativeModuleId;
|
|
68
|
+
if (testCase.parent.type === 'suite') {
|
|
69
|
+
return `${modulePath} > ${testCase.parent.fullName}`;
|
|
70
|
+
}
|
|
71
|
+
return modulePath;
|
|
72
|
+
}
|
|
73
|
+
function toReportCaseInput(testCase, groupName) {
|
|
74
|
+
const pathgradeMeta = testCase.meta().pathgrade;
|
|
75
|
+
const result = testCase.result();
|
|
76
|
+
const diagnostics = testCase.diagnostic();
|
|
77
|
+
const normalized = normalizeState(result.state);
|
|
78
|
+
const filePath = testCase.module.relativeModuleId;
|
|
79
|
+
const runnerCaseId = typeof testCase.id === 'string' ? testCase.id : undefined;
|
|
80
|
+
return {
|
|
81
|
+
...(runnerCaseId ? { caseId: runnerCaseId, runnerCaseId } : {}),
|
|
82
|
+
name: testCase.name,
|
|
83
|
+
state: normalized.state,
|
|
84
|
+
runnerDurationMs: diagnostics?.duration ?? 0,
|
|
85
|
+
sourceRef: filePath,
|
|
86
|
+
filePath,
|
|
87
|
+
groupName,
|
|
88
|
+
evaluations: pathgradeMeta?.map(entry => ({
|
|
89
|
+
score: entry.score,
|
|
90
|
+
trial: entry.trial,
|
|
91
|
+
diagnostics: entry.diagnostics,
|
|
92
|
+
})),
|
|
93
|
+
diagnostics: normalized.diagnostics,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function normalizeState(state) {
|
|
97
|
+
if (state === 'passed' || state === 'failed' || state === 'skipped' || state === 'pending') {
|
|
98
|
+
return { state };
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
state: 'failed',
|
|
102
|
+
diagnostics: buildDiagnosticsReport({
|
|
103
|
+
completionReason: state,
|
|
104
|
+
score: 0,
|
|
105
|
+
log: [],
|
|
106
|
+
}),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { RunnerInvocationAdapter } from './invocation.js';
|
|
2
|
+
export interface SpawnVitestRequest {
|
|
3
|
+
argv: string[];
|
|
4
|
+
env: NodeJS.ProcessEnv;
|
|
5
|
+
cwd: string;
|
|
6
|
+
}
|
|
7
|
+
export type SpawnVitest = (req: SpawnVitestRequest) => Promise<number> | number;
|
|
8
|
+
export declare function createVitestInvocationAdapter(input?: {
|
|
9
|
+
spawnVitest?: SpawnVitest;
|
|
10
|
+
}): RunnerInvocationAdapter;
|