@wix/pathgrade 1.0.39 → 1.0.41
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 +6 -6
- 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/evaluate.js +1 -1
- package/dist/sdk/index.d.ts +1 -1
- package/dist/sdk/index.js +1 -1
- package/dist/sdk/mcp-evidence.d.ts +4 -0
- package/dist/sdk/mcp-evidence.js +39 -14
- package/dist/sdk/run-scorer.js +10 -0
- package/dist/sdk/scorers.d.ts +5 -0
- package/dist/sdk/scorers.js +4 -0
- package/dist/sdk/tool-event-log.js +31 -1
- package/dist/sdk/tool-event-secrets.js +1 -0
- package/dist/sdk/types.d.ts +2 -1
- package/dist/tool-events.d.ts +2 -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
|
|
@@ -320,7 +321,7 @@ Pathgrade exposes a few useful features that are easy to miss from the basic exa
|
|
|
320
321
|
- `createAgent({ skillDir, workspace })` stages a real skill and a fixture workspace into the sandbox, which is how Pathgrade's skill examples are evaluated.
|
|
321
322
|
- `createAgent({ debug: true })` preserves the final workspace under the backward-compatible `pathgrade-debug/<test-name>/` path; when you use `runConversation()`, it also writes `run-snapshot.json`.
|
|
322
323
|
- `createAgent({ debug: { retainRuns: 5 } })` opts into managed run retention under `pathgrade-debug/runs/<run-id>/<test-name>/`. Use `pathgrade clean --debug`, `--keep=N`, and `--dry-run` to clean marked, inactive debug runs safely.
|
|
323
|
-
- `evaluate.fromSnapshot(snapshotPath, scorers)` re-runs grading against a saved snapshot without re-running the agent.
|
|
324
|
+
- `evaluate.fromSnapshot(snapshotPath, scorers)` re-runs grading against a saved snapshot without re-running the agent. For trusted live deterministic scoring, `evaluate(agent, scorers, { deterministicToolEvidence: 'live' })` exposes exact transient MCP arguments only through `ctx.toolEvents`; `agent.log` stays sanitized, and redaction-blocked argument predicates fail closed.
|
|
324
325
|
- `previewReactions(messages, reactions)` lets you inspect which scripted reactions would fire offline.
|
|
325
326
|
- `conversationWindow` on agents and personas keeps long transcripts bounded with summarization instead of sending the full conversation every turn.
|
|
326
327
|
- `copyIgnore` and `DEFAULT_COPY_IGNORE` let you control what gets copied into the sandbox when seeding from large fixtures or skill directories.
|
|
@@ -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;
|