@wix/pathgrade 1.0.39 → 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/commands/report.js +1 -2
- package/dist/reporting/comparison-contract.d.ts +3 -2
- package/dist/reporting/comparison-contract.js +66 -33
- package/dist/reporting/core.js +18 -1
- package/dist/reporting/reliability-contract.d.ts +1 -0
- package/dist/reporting/report-parser.js +68 -3
- package/dist/reporting/source-metadata.d.ts +3 -0
- package/dist/reporting/source-metadata.js +67 -2
- package/dist/reporting/types.d.ts +3 -0
- 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/report-projection.js +83 -0
- package/dist/runners/vitest-adapter.js +6 -2
- package/dist/sdk/scorers.d.ts +5 -0
- package/dist/sdk/scorers.js +4 -0
- package/dist/types.d.ts +5 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -178,7 +178,7 @@ Use `check()` for binary requirements, `score()` for partial credit, `judge()` f
|
|
|
178
178
|
check('tests-pass', async ({ runCommand }) => {
|
|
179
179
|
const { exitCode } = await runCommand('npm test');
|
|
180
180
|
return exitCode === 0;
|
|
181
|
-
});
|
|
181
|
+
}, { revision: { contract: 1 } });
|
|
182
182
|
```
|
|
183
183
|
|
|
184
184
|
### `score()` - Partial credit
|
|
@@ -187,7 +187,7 @@ check('tests-pass', async ({ runCommand }) => {
|
|
|
187
187
|
score('coverage', async ({ runCommand }) => {
|
|
188
188
|
const { stdout } = await runCommand('npx coverage-summary');
|
|
189
189
|
return parseFloat(stdout) / 100;
|
|
190
|
-
});
|
|
190
|
+
}, { revision: { contract: 1 } });
|
|
191
191
|
```
|
|
192
192
|
|
|
193
193
|
### `judge()` - Rubric evaluation
|
|
@@ -199,6 +199,7 @@ Was the fix minimal and correct? (0-0.5)`,
|
|
|
199
199
|
});
|
|
200
200
|
```
|
|
201
201
|
|
|
202
|
+
Function-bearing `check()` and `score()` scorers require explicit canonical-JSON `revision` data for authoritative baseline comparisons. A `judge()` with a function-valued `input` requires the same declaration. Imported scorer helpers belong in `comparisonInputs`; closed-over values belong in `revision`.
|
|
202
203
|
Judge scorers also support:
|
|
203
204
|
|
|
204
205
|
- `retry` for transient judge failures
|
|
@@ -440,9 +441,8 @@ Useful details:
|
|
|
440
441
|
- `pathgrade preview browser` starts a local viewer on `http://localhost:3847`.
|
|
441
442
|
- `pathgrade report` posts or updates a PR comment in GitHub Actions; locally it prints the markdown report and then the numeric pass rate. Provider orchestrators can add a `--details-url`, suppress stale updates with `--expected-head-sha`, and opt into surfaced API failures with `--strict`.
|
|
442
443
|
- `pathgrade validate --affected` is a strict mode for CI: every discovered eval must either live under a `SKILL.md` anchor or export valid `__pathgradeMeta`.
|
|
443
|
-
|
|
444
|
+
Declare methodology files separately from affected-selection dependencies with `__pathgradeMeta.comparisonInputs`, for example `comparisonInputs: ['evals/fixtures/**', 'evals/scorers/**']`. An empty array explicitly declares a self-contained eval. Pathgrade hashes the raw eval source plus each declared glob, matched repository-relative path, and raw file bytes; `.git` internals and `.pathgrade` outputs are excluded. Missing, invalid, unmatched, escaping, or unreadable inputs suppress numeric deltas. `deps`, `extraDeps`, inferred skill roots, and global triggers affect selection only and never definition identity.
|
|
444
445
|
Run `pathgrade --help` for the full help text.
|
|
445
|
-
|
|
446
446
|
## Configuration
|
|
447
447
|
|
|
448
448
|
```typescript
|
|
@@ -461,7 +461,7 @@ export default {
|
|
|
461
461
|
|
|
462
462
|
Pathgrade reads `pathgrade.config.*` for CLI and affected-selection behavior. `runner.adapter` and `--adapter=<name|path>` select the runner; `--adapter` wins over config. `attempts` defaults to `1` and must be a positive integer. Built-in adapters `vitest`, `jest`, and `node-test` support repeated attempts; third-party invocation adapters must advertise `supportsRepeatedAttempts: true` and produce the normalized child snapshot contract.
|
|
463
463
|
|
|
464
|
-
New reports use schema version
|
|
464
|
+
New reports use schema version 3. They add a versioned task inventory with every selected eval file, collection completeness, stable task keys, and bounded non-scoring reasons. They also preserve every attempt with `case_id`, `attempt_id`, and `attempt_index`; include the display-safe `runner_outcome`; publish `mean_reward`; and publish finite-sample pass@k only for complete binary attempts. `runner_status` and `threshold_status` expose the two gates independently, and canonical `status` passes only when runner assertions pass and any configured threshold passes. Runner assertions, runner diagnostics, native runner references, and full flow traces remain transient or in trace artifacts rather than being copied into consolidated public reports. A partial reward or incomplete attempt makes pass@k explicitly unavailable. Report versions 1 and 2 remain readable, but they cannot be promoted as authoritative task-accounted baselines and must be rerun.
|
|
465
465
|
|
|
466
466
|
Third-party runner adapters are supported through `@wix/pathgrade/adapter-kit`. Adapter names resolve as follows:
|
|
467
467
|
|
|
@@ -23,6 +23,7 @@ export declare function normalizeJestRunResults(input: {
|
|
|
23
23
|
run: AdapterRunHandle;
|
|
24
24
|
results: JestAggregatedResult;
|
|
25
25
|
metadataByCaseId?: Map<string, PathgradeTestMeta[]>;
|
|
26
|
+
discoveredFiles?: readonly string[];
|
|
26
27
|
cwd?: string;
|
|
27
28
|
}): NormalizedRunSnapshot;
|
|
28
29
|
export declare function jestCaseId(input: {
|
|
@@ -1,20 +1,39 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createSourceMetadataResolver } from '../../reporting/source-metadata.js';
|
|
2
2
|
export function normalizeJestRunResults(input) {
|
|
3
3
|
const runId = `${input.run.adapterName}:run`;
|
|
4
|
+
const cwd = input.cwd ?? process.cwd();
|
|
5
|
+
const resolveSourceMetadata = createSourceMetadataResolver(cwd);
|
|
4
6
|
const files = input.results.testResults ?? [];
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
...
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
7
|
+
const collection = input.run.status === 'completed'
|
|
8
|
+
? { state: 'complete' }
|
|
9
|
+
: { state: 'incomplete', reason: 'run-incomplete' };
|
|
10
|
+
const units = [
|
|
11
|
+
...files.map((fileResult, index) => ({
|
|
12
|
+
id: unitId(index),
|
|
13
|
+
runId,
|
|
14
|
+
displayName: fileResult.testFilePath,
|
|
15
|
+
collection,
|
|
16
|
+
...resolveSourceMetadata(fileResult.testFilePath),
|
|
17
|
+
groupingHints: [{
|
|
18
|
+
kind: 'source',
|
|
19
|
+
key: fileResult.testFilePath,
|
|
20
|
+
label: fileResult.testFilePath,
|
|
21
|
+
order: index,
|
|
22
|
+
}],
|
|
23
|
+
nativeReferences: [{ kind: 'jest-file', id: fileResult.testFilePath }],
|
|
24
|
+
})),
|
|
25
|
+
...(input.discoveredFiles ?? [])
|
|
26
|
+
.filter(file => !files.some(result => normalizePath(result.testFilePath, resolveSourceMetadata) === file))
|
|
27
|
+
.map((file, index) => ({
|
|
28
|
+
id: unitId(files.length + index),
|
|
29
|
+
runId,
|
|
30
|
+
displayName: file,
|
|
31
|
+
collection,
|
|
32
|
+
...resolveSourceMetadata(file),
|
|
33
|
+
groupingHints: [{ kind: 'source', key: file, label: file, order: files.length + index }],
|
|
34
|
+
nativeReferences: [{ kind: 'jest-file', id: file }],
|
|
35
|
+
})),
|
|
36
|
+
];
|
|
18
37
|
const occurrenceCounts = new Map();
|
|
19
38
|
return {
|
|
20
39
|
version: 1,
|
|
@@ -45,6 +64,9 @@ export function normalizeJestRunResults(input) {
|
|
|
45
64
|
},
|
|
46
65
|
};
|
|
47
66
|
}
|
|
67
|
+
function normalizePath(file, resolveSourceMetadata) {
|
|
68
|
+
return resolveSourceMetadata(file).sourceFile ?? file.replaceAll('\\', '/');
|
|
69
|
+
}
|
|
48
70
|
function assertionsForFile(fileResult) {
|
|
49
71
|
return fileResult.assertionResults ?? fileResult.testResults ?? [];
|
|
50
72
|
}
|
|
@@ -3,6 +3,7 @@ import { type PathgradeTestMeta, RunnerAdapter } from '@wix/pathgrade/adapter-ki
|
|
|
3
3
|
interface JestRunNative {
|
|
4
4
|
results?: JestAggregatedResult;
|
|
5
5
|
metadataByCaseId?: Map<string, PathgradeTestMeta[]>;
|
|
6
|
+
discoveredFiles?: string[];
|
|
6
7
|
}
|
|
7
8
|
export declare function createJestAdapter(options?: JestRunNative): RunnerAdapter;
|
|
8
9
|
export declare function createPathgradeAdapter(): RunnerAdapter;
|
|
@@ -27,7 +27,10 @@ export function createJestAdapter(options = {}) {
|
|
|
27
27
|
adapterName: this.name,
|
|
28
28
|
status: input.signal?.aborted ? 'cancelled' : 'completed',
|
|
29
29
|
exitCode: input.signal?.aborted ? 1 : 0,
|
|
30
|
-
native:
|
|
30
|
+
native: {
|
|
31
|
+
...options,
|
|
32
|
+
discoveredFiles: input.discovered.units.flatMap(unit => unit.sourceRef ? [unit.sourceRef] : []),
|
|
33
|
+
},
|
|
31
34
|
};
|
|
32
35
|
},
|
|
33
36
|
async collectNormalizedRunSnapshot(run) {
|
|
@@ -36,6 +39,7 @@ export function createJestAdapter(options = {}) {
|
|
|
36
39
|
run,
|
|
37
40
|
results: native.results ?? { testResults: [] },
|
|
38
41
|
metadataByCaseId: native.metadataByCaseId,
|
|
42
|
+
discoveredFiles: native.discoveredFiles,
|
|
39
43
|
cwd: discoveryCwd,
|
|
40
44
|
});
|
|
41
45
|
},
|
|
@@ -37,7 +37,7 @@ export function createNodeTestAdapter() {
|
|
|
37
37
|
adapterName: this.name,
|
|
38
38
|
status: 'completed',
|
|
39
39
|
exitCode: 0,
|
|
40
|
-
native: { resultsPath, resultsDir, cwd },
|
|
40
|
+
native: { resultsPath, resultsDir, cwd, discoveredFiles: files },
|
|
41
41
|
};
|
|
42
42
|
}
|
|
43
43
|
const exitCode = await spawnNodeTest({
|
|
@@ -54,7 +54,7 @@ export function createNodeTestAdapter() {
|
|
|
54
54
|
adapterName: this.name,
|
|
55
55
|
status: input.signal?.aborted ? 'cancelled' : (exitCode === 0 ? 'completed' : 'failed'),
|
|
56
56
|
exitCode,
|
|
57
|
-
native: { resultsPath, resultsDir, cwd },
|
|
57
|
+
native: { resultsPath, resultsDir, cwd, discoveredFiles: files },
|
|
58
58
|
};
|
|
59
59
|
},
|
|
60
60
|
async collectNormalizedRunSnapshot(run) {
|
|
@@ -71,7 +71,7 @@ export function createNodeTestAdapter() {
|
|
|
71
71
|
return buildNormalizedRunSnapshotFromReportGroups(run, Array.from(groupMap.entries()).map(([groupName, groupedCases]) => ({
|
|
72
72
|
groupName,
|
|
73
73
|
cases: groupedCases,
|
|
74
|
-
})), { cwd: native.cwd });
|
|
74
|
+
})), { cwd: native.cwd, discoveredFiles: native.discoveredFiles });
|
|
75
75
|
}
|
|
76
76
|
finally {
|
|
77
77
|
if (native.resultsDir)
|
|
@@ -104,10 +104,11 @@ function readNodeTestRunNative(run) {
|
|
|
104
104
|
&& run.native !== null
|
|
105
105
|
&& typeof run.native.resultsPath === 'string'
|
|
106
106
|
&& typeof run.native.resultsDir === 'string'
|
|
107
|
-
&& typeof run.native.cwd === 'string'
|
|
107
|
+
&& typeof run.native.cwd === 'string'
|
|
108
|
+
&& Array.isArray(run.native.discoveredFiles)) {
|
|
108
109
|
return run.native;
|
|
109
110
|
}
|
|
110
|
-
return { resultsPath: '', resultsDir: '', cwd: process.cwd() };
|
|
111
|
+
return { resultsPath: '', resultsDir: '', cwd: process.cwd(), discoveredFiles: [] };
|
|
111
112
|
}
|
|
112
113
|
async function readCases(resultsPath) {
|
|
113
114
|
if (!resultsPath || !(await fs.pathExists(resultsPath)))
|
package/dist/affected/meta.d.ts
CHANGED
|
@@ -17,10 +17,13 @@
|
|
|
17
17
|
export interface ParsedMeta {
|
|
18
18
|
deps?: string[];
|
|
19
19
|
extraDeps?: string[];
|
|
20
|
+
comparisonInputs?: string[];
|
|
20
21
|
alwaysRun?: boolean;
|
|
21
22
|
}
|
|
22
23
|
/**
|
|
23
24
|
* Parse `__pathgradeMeta` from an eval file's AST. Returns `null` if the
|
|
24
25
|
* export is not present.
|
|
25
26
|
*/
|
|
26
|
-
export declare function parsePathgradeMeta(evalFile: string
|
|
27
|
+
export declare function parsePathgradeMeta(evalFile: string, options?: {
|
|
28
|
+
validateComparisonInputs?: boolean;
|
|
29
|
+
}): ParsedMeta | null;
|
package/dist/affected/meta.js
CHANGED
|
@@ -21,14 +21,14 @@ import picomatch from 'picomatch';
|
|
|
21
21
|
* Parse `__pathgradeMeta` from an eval file's AST. Returns `null` if the
|
|
22
22
|
* export is not present.
|
|
23
23
|
*/
|
|
24
|
-
export function parsePathgradeMeta(evalFile) {
|
|
24
|
+
export function parsePathgradeMeta(evalFile, options = {}) {
|
|
25
25
|
const source = fs.readFileSync(evalFile, 'utf-8');
|
|
26
26
|
const sourceFile = ts.createSourceFile(evalFile, source, ts.ScriptTarget.Latest,
|
|
27
27
|
/* setParentNodes */ true, ts.ScriptKind.TS);
|
|
28
28
|
for (const stmt of sourceFile.statements) {
|
|
29
29
|
const initializer = findMetaInitializer(stmt);
|
|
30
30
|
if (initializer) {
|
|
31
|
-
return extractMeta(initializer, evalFile);
|
|
31
|
+
return extractMeta(initializer, evalFile, options);
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
return null;
|
|
@@ -50,7 +50,7 @@ function findMetaInitializer(stmt) {
|
|
|
50
50
|
}
|
|
51
51
|
return null;
|
|
52
52
|
}
|
|
53
|
-
function extractMeta(expr, evalFile) {
|
|
53
|
+
function extractMeta(expr, evalFile, options) {
|
|
54
54
|
// Optionally unwrap a type assertion: `{...} as PathgradeMeta` / `<PathgradeMeta>{...}`.
|
|
55
55
|
let node = expr;
|
|
56
56
|
while (ts.isAsExpression(node) || ts.isTypeAssertionExpression(node) || ts.isSatisfiesExpression(node)) {
|
|
@@ -64,7 +64,9 @@ function extractMeta(expr, evalFile) {
|
|
|
64
64
|
if (!ts.isPropertyAssignment(prop))
|
|
65
65
|
continue;
|
|
66
66
|
const name = propertyKeyName(prop.name);
|
|
67
|
-
if (name === 'deps' || name === 'extraDeps') {
|
|
67
|
+
if (name === 'deps' || name === 'extraDeps' || name === 'comparisonInputs') {
|
|
68
|
+
if (name === 'comparisonInputs' && options.validateComparisonInputs === false)
|
|
69
|
+
continue;
|
|
68
70
|
const globs = extractStringArray(prop.initializer, evalFile, name);
|
|
69
71
|
for (const g of globs)
|
|
70
72
|
validateGlob(g, evalFile, name);
|
|
@@ -89,7 +91,9 @@ function validateGlob(glob, evalFile, field) {
|
|
|
89
91
|
if (glob.length === 0) {
|
|
90
92
|
throw new Error(`${evalFile}: __pathgradeMeta.${field} contains an empty glob.`);
|
|
91
93
|
}
|
|
92
|
-
|
|
94
|
+
const normalized = glob.replaceAll('\\', '/');
|
|
95
|
+
if (normalized.startsWith('/') || /^[A-Za-z]:\//.test(normalized)
|
|
96
|
+
|| normalized.startsWith('../') || normalized.includes('/../') || normalized === '..') {
|
|
93
97
|
throw new Error(`${evalFile}: __pathgradeMeta.${field} entry "${glob}" escapes the repo root — ` +
|
|
94
98
|
`globs must be repo-root-relative.`);
|
|
95
99
|
}
|
package/dist/affected/select.js
CHANGED
|
@@ -44,7 +44,7 @@ export function selectAffected(input) {
|
|
|
44
44
|
for (const evalFile of evalFiles) {
|
|
45
45
|
const absEval = path.resolve(repoRoot, evalFile);
|
|
46
46
|
const skillRoot = findSkillRoot(absEval, repoRoot);
|
|
47
|
-
const meta = parsePathgradeMeta(absEval);
|
|
47
|
+
const meta = parsePathgradeMeta(absEval, { validateComparisonInputs: false });
|
|
48
48
|
// Precedence #2: alwaysRun wins over dep matching.
|
|
49
49
|
if (meta?.alwaysRun === true) {
|
|
50
50
|
selected.push({ file: evalFile, reason: 'always-run' });
|
package/dist/commands/report.js
CHANGED
|
@@ -44,9 +44,8 @@ async function loadReport(resolvedPath) {
|
|
|
44
44
|
if (!(await fs.pathExists(resolvedPath))) {
|
|
45
45
|
throw new Error(`results file not found at ${resolvedPath}`);
|
|
46
46
|
}
|
|
47
|
-
const raw = await fs.readJSON(resolvedPath);
|
|
48
47
|
try {
|
|
49
|
-
return parsePathgradeReport(
|
|
48
|
+
return parsePathgradeReport(await fs.readJSON(resolvedPath));
|
|
50
49
|
}
|
|
51
50
|
catch {
|
|
52
51
|
throw new Error(`results file at ${resolvedPath} is not a valid pathgrade report`);
|
|
@@ -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 = [];
|
|
@@ -60,7 +61,7 @@ export function buildPathgradeReport(input) {
|
|
|
60
61
|
: overallMeanReward >= input.threshold ? 'pass' : 'fail';
|
|
61
62
|
return {
|
|
62
63
|
report: {
|
|
63
|
-
version:
|
|
64
|
+
version: 3,
|
|
64
65
|
timestamp: new Date().toISOString(),
|
|
65
66
|
...(input.threshold != null ? { threshold: input.threshold } : {}),
|
|
66
67
|
attempts_requested: attemptsRequested,
|
|
@@ -71,6 +72,15 @@ export function buildPathgradeReport(input) {
|
|
|
71
72
|
threshold_status: thresholdStatus,
|
|
72
73
|
status: runnerStatus === 'fail' || thresholdStatus === 'fail' ? 'fail' : 'pass',
|
|
73
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
|
+
},
|
|
74
84
|
...(input.selection ? { selection: input.selection } : {}),
|
|
75
85
|
},
|
|
76
86
|
traces,
|
|
@@ -78,6 +88,13 @@ export function buildPathgradeReport(input) {
|
|
|
78
88
|
warnings,
|
|
79
89
|
};
|
|
80
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
|
+
}
|
|
81
98
|
function reportableBuiltCase(testCase) {
|
|
82
99
|
if (testCase.reportable !== undefined)
|
|
83
100
|
return testCase.reportable;
|
|
@@ -1,9 +1,9 @@
|
|
|
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
9
|
|| !optionalGateStatus(value.runner_status)
|
|
@@ -11,6 +11,7 @@ export function parsePathgradeReport(value) {
|
|
|
11
11
|
|| (value.run_kind !== undefined && value.run_kind !== 'evaluation' && value.run_kind !== 'no-affected')
|
|
12
12
|
|| !Array.isArray(value.groups)
|
|
13
13
|
|| !value.groups.every(group => isReportGroup(group, value.version))
|
|
14
|
+
|| (value.version === 3 && !isTaskInventory(value.task_inventory))
|
|
14
15
|
|| !isSelection(value.selection)) {
|
|
15
16
|
throw new Error('PathGrade results.json is missing or has an unsupported schema');
|
|
16
17
|
}
|
|
@@ -50,7 +51,9 @@ function isReportGroup(value, version) {
|
|
|
50
51
|
&& value.skills_used.every(item => typeof item === 'string')
|
|
51
52
|
&& Array.isArray(value.trials)
|
|
52
53
|
&& value.trials.every(isTrial)
|
|
53
|
-
&& (
|
|
54
|
+
&& (version === 3
|
|
55
|
+
? isComparisonContractV2(value.comparison_contract)
|
|
56
|
+
: value.comparison_contract === undefined || isComparisonContract(value.comparison_contract));
|
|
54
57
|
}
|
|
55
58
|
function deriveRunnerStatus(value) {
|
|
56
59
|
if (value.runner_status === 'fail')
|
|
@@ -137,6 +140,9 @@ function isSelection(value) {
|
|
|
137
140
|
&& item.reason === 'no-matching-deps'));
|
|
138
141
|
}
|
|
139
142
|
function isComparisonContract(value) {
|
|
143
|
+
return isComparisonContractV1(value) || isComparisonContractV2(value);
|
|
144
|
+
}
|
|
145
|
+
function isComparisonContractV1(value) {
|
|
140
146
|
return isRecord(value)
|
|
141
147
|
&& value.version === 1
|
|
142
148
|
&& optionalString(value.definition_revision)
|
|
@@ -150,6 +156,65 @@ function isComparisonContract(value) {
|
|
|
150
156
|
|| reason === 'runtime_metadata_missing'
|
|
151
157
|
|| reason === 'sampling_incomplete')));
|
|
152
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
|
+
}
|
|
153
218
|
function optionalString(value) {
|
|
154
219
|
return value === undefined || typeof value === 'string';
|
|
155
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
|
+
}
|
|
@@ -2,6 +2,7 @@ import type { DiagnosticsReport } from '../sdk/diagnostics.js';
|
|
|
2
2
|
import type { AgentExecutionMetadata, EvaluationResultKind } from '../sdk/types.js';
|
|
3
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 {
|
|
7
8
|
runStatus?: RunStatus;
|
|
@@ -9,12 +10,14 @@ export interface ReportRunInput {
|
|
|
9
10
|
selection?: PathgradeSelectionReport;
|
|
10
11
|
attemptsRequested?: number;
|
|
11
12
|
attemptsCompleted?: number;
|
|
13
|
+
taskInventory?: TaskInventory;
|
|
12
14
|
groups: ReportGroupInput[];
|
|
13
15
|
}
|
|
14
16
|
export interface ReportGroupInput {
|
|
15
17
|
groupName: string;
|
|
16
18
|
sourceFile?: string;
|
|
17
19
|
sourceRevision?: string;
|
|
20
|
+
comparisonInputs?: ComparisonInputsState;
|
|
18
21
|
cases: ReportCaseInput[];
|
|
19
22
|
}
|
|
20
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;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createSourceMetadataResolver } from '../reporting/source-metadata.js';
|
|
2
2
|
export function buildNormalizedRunSnapshotFromReportGroups(run, groups, options = {}) {
|
|
3
3
|
const cwd = options.cwd ?? process.cwd();
|
|
4
|
+
const resolveSourceMetadata = createSourceMetadataResolver(cwd);
|
|
4
5
|
return {
|
|
5
6
|
version: 1,
|
|
6
7
|
completeness: 'final',
|
|
@@ -14,18 +15,41 @@ export function buildNormalizedRunSnapshotFromReportGroups(run, groups, options
|
|
|
14
15
|
message: diagnostic.message,
|
|
15
16
|
})) } : {}),
|
|
16
17
|
},
|
|
17
|
-
units:
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
units: [
|
|
19
|
+
...groups.map((group, index) => {
|
|
20
|
+
const source = group.sourceFile && group.sourceRevision
|
|
21
|
+
? {
|
|
22
|
+
sourceFile: group.sourceFile,
|
|
23
|
+
sourceRevision: group.sourceRevision,
|
|
24
|
+
...(group.comparisonInputs ? { comparisonInputs: group.comparisonInputs } : {}),
|
|
25
|
+
}
|
|
26
|
+
: resolveSourceMetadata(group.sourceFile ?? group.cases.map(testCase => testCase.filePath ?? testCase.sourceRef).find(Boolean));
|
|
27
|
+
return {
|
|
28
|
+
id: unitId(group.groupName, index),
|
|
29
|
+
runId: `${run.adapterName}:run`,
|
|
30
|
+
displayName: group.groupName,
|
|
31
|
+
collection: run.status === 'completed' || run.status === 'failed'
|
|
32
|
+
? { state: 'complete' }
|
|
33
|
+
: { state: 'incomplete', reason: 'run-incomplete' },
|
|
34
|
+
...source,
|
|
35
|
+
groupingHints: [{ kind: 'suite', key: group.groupName, label: group.groupName, order: index }],
|
|
36
|
+
};
|
|
37
|
+
}),
|
|
38
|
+
...(options.discoveredFiles ?? [])
|
|
39
|
+
.filter(file => !groups.some(group => group.sourceFile === file
|
|
40
|
+
|| group.cases.some(testCase => testCase.filePath === file || testCase.sourceRef === file)))
|
|
41
|
+
.map((file, index) => ({
|
|
42
|
+
id: unitId(file, groups.length + index),
|
|
23
43
|
runId: `${run.adapterName}:run`,
|
|
24
|
-
displayName:
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
44
|
+
displayName: file,
|
|
45
|
+
collection: {
|
|
46
|
+
state: 'incomplete',
|
|
47
|
+
reason: 'adapter-cannot-prove-completeness',
|
|
48
|
+
},
|
|
49
|
+
...resolveSourceMetadata(file),
|
|
50
|
+
groupingHints: [{ kind: 'source', key: file, label: file, order: groups.length + index }],
|
|
51
|
+
})),
|
|
52
|
+
],
|
|
29
53
|
cases: groups.flatMap((group, groupIndex) => group.cases.map((testCase, caseIndex) => {
|
|
30
54
|
const caseId = testCase.caseId ?? `${unitId(group.groupName, groupIndex)}:case-${caseIndex + 1}`;
|
|
31
55
|
const attemptId = `${caseId}:attempt-1`;
|
package/dist/runners/model.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { DiagnosticsReport } from '../sdk/diagnostics.js';
|
|
2
2
|
import type { TrialResult } from '../types.js';
|
|
3
3
|
import type { CollectionIncompleteReason } from '../reporting/reliability-contract.js';
|
|
4
|
+
import type { ComparisonInputsState } from '../reporting/reliability-contract.js';
|
|
4
5
|
export interface NormalizedRunSnapshot {
|
|
5
6
|
version: 1;
|
|
6
7
|
completeness: SnapshotCompleteness;
|
|
@@ -29,6 +30,7 @@ export interface EvalUnitRecord {
|
|
|
29
30
|
collection?: EvalUnitCollection;
|
|
30
31
|
sourceFile?: string;
|
|
31
32
|
sourceRevision?: string;
|
|
33
|
+
comparisonInputs?: ComparisonInputsState;
|
|
32
34
|
diagnostics?: Diagnostic[];
|
|
33
35
|
nativeReferences?: NativeReference[];
|
|
34
36
|
groupingHints?: GroupingHint[];
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { TASK_INVENTORY_VERSION } from '../reporting/reliability-contract.js';
|
|
1
2
|
import { validateNormalizedRunSnapshot } from './model-validation.js';
|
|
2
3
|
export function projectNormalizedRunSnapshotToReportInput(snapshot, options = {}) {
|
|
3
4
|
const validation = validateNormalizedRunSnapshot(snapshot, { completeness: 'final' });
|
|
@@ -20,6 +21,7 @@ export function projectNormalizedRunSnapshotToReportInput(snapshot, options = {}
|
|
|
20
21
|
groupName: key,
|
|
21
22
|
...(unit?.sourceFile ? { sourceFile: unit.sourceFile } : {}),
|
|
22
23
|
...(unit?.sourceRevision ? { sourceRevision: unit.sourceRevision } : {}),
|
|
24
|
+
...(unit?.comparisonInputs ? { comparisonInputs: unit.comparisonInputs } : {}),
|
|
23
25
|
cases: [],
|
|
24
26
|
};
|
|
25
27
|
group.cases.push({
|
|
@@ -44,9 +46,90 @@ export function projectNormalizedRunSnapshotToReportInput(snapshot, options = {}
|
|
|
44
46
|
runStatus: snapshot.model.run.status,
|
|
45
47
|
attemptsRequested: snapshot.model.run.attemptsRequested ?? 1,
|
|
46
48
|
attemptsCompleted: snapshot.model.run.attemptsCompleted ?? 1,
|
|
49
|
+
taskInventory: buildTaskInventory(snapshot, [...groups.values()]),
|
|
47
50
|
groups: [...groups.values()],
|
|
48
51
|
};
|
|
49
52
|
}
|
|
53
|
+
function buildTaskInventory(snapshot, groups) {
|
|
54
|
+
const unitsBySource = new Map(snapshot.model.units
|
|
55
|
+
.filter(unit => unit.sourceFile)
|
|
56
|
+
.map(unit => [unit.sourceFile, unit]));
|
|
57
|
+
const files = new Map();
|
|
58
|
+
for (const unit of snapshot.model.units) {
|
|
59
|
+
if (!unit.sourceFile)
|
|
60
|
+
continue;
|
|
61
|
+
const collection = unit.collection ?? {
|
|
62
|
+
state: 'incomplete',
|
|
63
|
+
reason: 'adapter-cannot-prove-completeness',
|
|
64
|
+
};
|
|
65
|
+
const existing = files.get(unit.sourceFile);
|
|
66
|
+
if (!existing) {
|
|
67
|
+
files.set(unit.sourceFile, {
|
|
68
|
+
tasks: [],
|
|
69
|
+
completeness: collection.state,
|
|
70
|
+
...(collection.state === 'incomplete' ? { reason: collection.reason } : {}),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
else if (collection.state === 'incomplete') {
|
|
74
|
+
existing.completeness = 'incomplete';
|
|
75
|
+
existing.reason = collection.reason;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
for (const group of groups) {
|
|
79
|
+
if (!group.sourceFile)
|
|
80
|
+
continue;
|
|
81
|
+
const unit = unitsBySource.get(group.sourceFile);
|
|
82
|
+
const collection = unit?.collection ?? {
|
|
83
|
+
state: 'incomplete',
|
|
84
|
+
reason: 'adapter-cannot-prove-completeness',
|
|
85
|
+
};
|
|
86
|
+
const file = files.get(group.sourceFile) ?? {
|
|
87
|
+
tasks: [],
|
|
88
|
+
completeness: collection.state,
|
|
89
|
+
...(collection.state === 'incomplete' ? { reason: collection.reason } : {}),
|
|
90
|
+
};
|
|
91
|
+
file.tasks.push(taskInventoryEntry(group));
|
|
92
|
+
if (collection.state === 'incomplete') {
|
|
93
|
+
file.completeness = 'incomplete';
|
|
94
|
+
file.reason = collection.reason;
|
|
95
|
+
}
|
|
96
|
+
files.set(group.sourceFile, file);
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
version: TASK_INVENTORY_VERSION,
|
|
100
|
+
files: [...files.entries()].toSorted(([left], [right]) => left.localeCompare(right)).map(([eval_file, file]) => ({
|
|
101
|
+
eval_file,
|
|
102
|
+
tasks: file.tasks.toSorted((left, right) => left.task_key.localeCompare(right.task_key)),
|
|
103
|
+
...(file.completeness === 'complete'
|
|
104
|
+
? { completeness: 'complete' }
|
|
105
|
+
: { completeness: 'incomplete', reason: file.reason ?? 'adapter-cannot-prove-completeness' }),
|
|
106
|
+
})),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function taskInventoryEntry(group) {
|
|
110
|
+
const states = group.cases.map(testCase => testCase.state);
|
|
111
|
+
if (states.every(state => state === 'skipped')) {
|
|
112
|
+
return { task_key: group.groupName, state: 'not-scored', reason: 'skipped' };
|
|
113
|
+
}
|
|
114
|
+
if (states.every(state => state === 'pending')) {
|
|
115
|
+
return { task_key: group.groupName, state: 'not-scored', reason: 'pending' };
|
|
116
|
+
}
|
|
117
|
+
if (group.cases.some(testCase => testCase.attempts?.some(attempt => attempt.outcome.kind === 'timed-out'))) {
|
|
118
|
+
return { task_key: group.groupName, state: 'not-scored', reason: 'timed-out' };
|
|
119
|
+
}
|
|
120
|
+
if (group.cases.some(isPromotableCase))
|
|
121
|
+
return { task_key: group.groupName, state: 'scored' };
|
|
122
|
+
return { task_key: group.groupName, state: 'not-scored', reason: 'failed-before-evaluation' };
|
|
123
|
+
}
|
|
124
|
+
function isPromotableCase(testCase) {
|
|
125
|
+
if (testCase.state === 'skipped' || testCase.state === 'pending')
|
|
126
|
+
return false;
|
|
127
|
+
const evaluations = testCase.attempts?.flatMap(attempt => attempt.evaluations ?? []) ?? testCase.evaluations;
|
|
128
|
+
if (evaluations === undefined)
|
|
129
|
+
return true;
|
|
130
|
+
return evaluations.some(evaluation => evaluation.score !== undefined
|
|
131
|
+
&& evaluation.resultKind !== 'synthetic_no_evaluation');
|
|
132
|
+
}
|
|
50
133
|
function totalDurationMs(runCase) {
|
|
51
134
|
return runCase.attempts.reduce((sum, attempt) => sum + (attempt.durationMs ?? 0), 0);
|
|
52
135
|
}
|
|
@@ -31,11 +31,15 @@ export function createVitestAdapter(options = {}) {
|
|
|
31
31
|
native: {
|
|
32
32
|
testModules: options.testModules ?? [],
|
|
33
33
|
cwd: discoveryCwd,
|
|
34
|
+
discoveredFiles: input.discovered.units.flatMap(unit => unit.sourceRef ? [unit.sourceRef] : []),
|
|
34
35
|
},
|
|
35
36
|
};
|
|
36
37
|
},
|
|
37
38
|
async collectNormalizedRunSnapshot(run) {
|
|
38
|
-
return buildNormalizedRunSnapshotFromReportGroups(run, collectVitestReportGroups(readVitestRunNative(run).testModules), {
|
|
39
|
+
return buildNormalizedRunSnapshotFromReportGroups(run, collectVitestReportGroups(readVitestRunNative(run).testModules), {
|
|
40
|
+
cwd: readVitestRunNative(run).cwd,
|
|
41
|
+
discoveredFiles: readVitestRunNative(run).discoveredFiles,
|
|
42
|
+
});
|
|
39
43
|
},
|
|
40
44
|
};
|
|
41
45
|
}
|
|
@@ -59,7 +63,7 @@ export function collectVitestReportGroups(testModules) {
|
|
|
59
63
|
function readVitestRunNative(run) {
|
|
60
64
|
if (isVitestRunNative(run.native))
|
|
61
65
|
return run.native;
|
|
62
|
-
return { testModules: [], cwd: process.cwd() };
|
|
66
|
+
return { testModules: [], cwd: process.cwd(), discoveredFiles: [] };
|
|
63
67
|
}
|
|
64
68
|
function isVitestRunNative(native) {
|
|
65
69
|
return typeof native === 'object'
|
package/dist/sdk/scorers.d.ts
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import type { CheckScorer, CodeJudgeToolName, ScorerContext, JudgeScorer, ScoreScorer, ScoreResult, ToolExpectation, ToolUsageScorer } from './types.js';
|
|
2
|
+
import type { JsonValue } from '../internal/direct-mcp-v2/types.js';
|
|
2
3
|
/**
|
|
3
4
|
* Create a check scorer — a boolean gate that passes (1.0) or fails (0.0).
|
|
4
5
|
*/
|
|
5
6
|
export declare function check(name: string, fn: (ctx: ScorerContext) => boolean | Promise<boolean>, opts?: {
|
|
6
7
|
weight?: number;
|
|
8
|
+
revision?: JsonValue;
|
|
7
9
|
}): CheckScorer;
|
|
8
10
|
/**
|
|
9
11
|
* Create a score scorer — returns a number (0-1) or { score, details } for partial credit.
|
|
10
12
|
*/
|
|
11
13
|
export declare function score(name: string, fn: (ctx: ScorerContext) => number | ScoreResult | Promise<number | ScoreResult>, opts?: {
|
|
12
14
|
weight?: number;
|
|
15
|
+
revision?: JsonValue;
|
|
13
16
|
}): ScoreScorer;
|
|
14
17
|
/**
|
|
15
18
|
* Create a judge scorer — uses an LLM to evaluate against a rubric.
|
|
@@ -55,10 +58,12 @@ export declare function judge(name: string, opts: {
|
|
|
55
58
|
maxRounds?: number;
|
|
56
59
|
/** Anthropic prompt caching. Default: true when tools is set, unchanged otherwise. */
|
|
57
60
|
cacheControl?: boolean;
|
|
61
|
+
revision?: JsonValue;
|
|
58
62
|
}): JudgeScorer;
|
|
59
63
|
/**
|
|
60
64
|
* Create a tool usage scorer — matches tool events against expectations.
|
|
61
65
|
*/
|
|
62
66
|
export declare function toolUsage(name: string, expectations: ToolExpectation[], opts?: {
|
|
63
67
|
weight?: number;
|
|
68
|
+
revision?: JsonValue;
|
|
64
69
|
}): ToolUsageScorer;
|
package/dist/sdk/scorers.js
CHANGED
|
@@ -7,6 +7,7 @@ export function check(name, fn, opts) {
|
|
|
7
7
|
name,
|
|
8
8
|
weight: opts?.weight ?? 1,
|
|
9
9
|
fn,
|
|
10
|
+
...(opts?.revision === undefined ? {} : { revision: opts.revision }),
|
|
10
11
|
};
|
|
11
12
|
}
|
|
12
13
|
/**
|
|
@@ -18,6 +19,7 @@ export function score(name, fn, opts) {
|
|
|
18
19
|
name,
|
|
19
20
|
weight: opts?.weight ?? 1,
|
|
20
21
|
fn,
|
|
22
|
+
...(opts?.revision === undefined ? {} : { revision: opts.revision }),
|
|
21
23
|
};
|
|
22
24
|
}
|
|
23
25
|
/**
|
|
@@ -65,6 +67,7 @@ export function judge(name, opts) {
|
|
|
65
67
|
tools,
|
|
66
68
|
maxRounds: opts.maxRounds,
|
|
67
69
|
cacheControl,
|
|
70
|
+
...(opts.revision === undefined ? {} : { revision: opts.revision }),
|
|
68
71
|
};
|
|
69
72
|
}
|
|
70
73
|
/**
|
|
@@ -76,5 +79,6 @@ export function toolUsage(name, expectations, opts) {
|
|
|
76
79
|
name,
|
|
77
80
|
weight: opts?.weight ?? 1,
|
|
78
81
|
expectations,
|
|
82
|
+
...(opts?.revision === undefined ? {} : { revision: opts.revision }),
|
|
79
83
|
};
|
|
80
84
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -207,7 +207,7 @@ export interface EvalReport {
|
|
|
207
207
|
skills_used: string[];
|
|
208
208
|
}
|
|
209
209
|
export type ComparisonUnavailableReason = 'definition_metadata_missing' | 'scorer_metadata_missing' | 'runtime_metadata_missing' | 'sampling_incomplete';
|
|
210
|
-
export interface
|
|
210
|
+
export interface ComparisonContractV1 {
|
|
211
211
|
version: 1;
|
|
212
212
|
definition_revision?: string;
|
|
213
213
|
scorer_revision?: string;
|
|
@@ -216,6 +216,7 @@ export interface ComparisonContract {
|
|
|
216
216
|
report_schema_revision: 'pathgrade-results-v2';
|
|
217
217
|
unavailable_reasons?: ComparisonUnavailableReason[];
|
|
218
218
|
}
|
|
219
|
+
export type ComparisonContract = ComparisonContractV1 | import('./reporting/reliability-contract.js').ComparisonContractV2;
|
|
219
220
|
/**
|
|
220
221
|
* TrialResult with large trace fields stripped. These fields
|
|
221
222
|
* live only in the per-group trace files; the consolidated results.json keeps
|
|
@@ -260,7 +261,7 @@ export interface PathgradeSelectionReport {
|
|
|
260
261
|
* external consumers can gate on schema revisions.
|
|
261
262
|
*/
|
|
262
263
|
export interface PathgradeReport {
|
|
263
|
-
version: 1 | 2;
|
|
264
|
+
version: 1 | 2 | 3;
|
|
264
265
|
timestamp: string;
|
|
265
266
|
/** `ci.threshold` from Pathgrade config or legacy plugin config, if configured. */
|
|
266
267
|
threshold?: number;
|
|
@@ -279,6 +280,8 @@ export interface PathgradeReport {
|
|
|
279
280
|
/** Agent Evals compatibility manifest when changed selection finds no runnable evals. */
|
|
280
281
|
run_kind?: 'evaluation' | 'no-affected';
|
|
281
282
|
groups: PathgradeGroupReport[];
|
|
283
|
+
/** Current selected-task accounting, required for schema-v3 reports. */
|
|
284
|
+
task_inventory?: import('./reporting/reliability-contract.js').TaskInventory;
|
|
282
285
|
/**
|
|
283
286
|
* Present when `pathgrade run --changed` produced the run. Absent on
|
|
284
287
|
* plain `pathgrade run`. Backward compatible — older consumers ignore
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wix/pathgrade",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.40",
|
|
4
4
|
"packageManager": "yarn@4.12.0",
|
|
5
5
|
"description": "Evaluate whether AI agents discover and use your skills correctly",
|
|
6
6
|
"exports": {
|
|
@@ -142,5 +142,5 @@
|
|
|
142
142
|
"typescript": "^5.9.3",
|
|
143
143
|
"zod": "4.3.6"
|
|
144
144
|
},
|
|
145
|
-
"falconPackageHash": "
|
|
145
|
+
"falconPackageHash": "4c4d43a7c8d439f02756404accbc72b44db02c52db4d0c2b2d18452f"
|
|
146
146
|
}
|