@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
|
@@ -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/evaluate.js
CHANGED
|
@@ -270,7 +270,7 @@ function createLiveDeterministicContext(ctx) {
|
|
|
270
270
|
tool: event.arguments?.tool,
|
|
271
271
|
status: event.arguments?.status,
|
|
272
272
|
...structuredClone(originalInput),
|
|
273
|
-
}, ...(mcp ? { mcp } : {}) })
|
|
273
|
+
}, redactedArgumentPaths: undefined, ...(mcp ? { mcp } : {}) })
|
|
274
274
|
: event;
|
|
275
275
|
}),
|
|
276
276
|
};
|
package/dist/sdk/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export { AgentCrashError } from './agent-crash.js';
|
|
|
4
4
|
export { check, score, judge, toolUsage } from './scorers.js';
|
|
5
5
|
export { compileScenario, compileScenarioText } from './scenario-machine-v2.js';
|
|
6
6
|
export { findScenarioCaseMatches, getFinalScenarioState, getScenarioStateTimeline, ScenarioEvidenceError, wasScenarioCaseMatched, } from './scenario-evidence.js';
|
|
7
|
-
export { getMcpToolCall, isMcpToolCall, findMcpToolCalls, getMcpStartupStatus, isMcpStartupStatus, getMcpApproval, isMcpApproval, findMcpApprovals, getMcpInvocation, wasMcpToolInvoked, } from './mcp-evidence.js';
|
|
7
|
+
export { getMcpToolCall, isMcpToolCall, findMcpToolCalls, McpEvidenceUnavailableError, getMcpStartupStatus, isMcpStartupStatus, getMcpApproval, isMcpApproval, findMcpApprovals, getMcpInvocation, wasMcpToolInvoked, } from './mcp-evidence.js';
|
|
8
8
|
export type { McpMockApprovalRule, McpMockJsonValue } from './mcp-mock-approvals.js';
|
|
9
9
|
export type { CompileScenarioResult, EvidenceEnvelope, JsonObject, JsonPrimitive, JsonValue, MatchExpr, MatchPredicate, McpContent, ScenarioArtifact, ScenarioCase, ScenarioDiagnostic, ScenarioEvidence, ScenarioMachineV2, ScenarioRejection, ScenarioSuccess, ScenarioToolError, } from './scenario-machine-v2.js';
|
|
10
10
|
export type { ExpectedScenarioCaseMatch, ScenarioCaseMatchEvidence, ScenarioStateTransitionEvidence, } from './scenario-evidence.js';
|
package/dist/sdk/index.js
CHANGED
|
@@ -5,7 +5,7 @@ export { AgentCrashError } from './agent-crash.js';
|
|
|
5
5
|
export { check, score, judge, toolUsage } from './scorers.js';
|
|
6
6
|
export { compileScenario, compileScenarioText } from './scenario-machine-v2.js';
|
|
7
7
|
export { findScenarioCaseMatches, getFinalScenarioState, getScenarioStateTimeline, ScenarioEvidenceError, wasScenarioCaseMatched, } from './scenario-evidence.js';
|
|
8
|
-
export { getMcpToolCall, isMcpToolCall, findMcpToolCalls, getMcpStartupStatus, isMcpStartupStatus, getMcpApproval, isMcpApproval, findMcpApprovals, getMcpInvocation, wasMcpToolInvoked, } from './mcp-evidence.js';
|
|
8
|
+
export { getMcpToolCall, isMcpToolCall, findMcpToolCalls, McpEvidenceUnavailableError, getMcpStartupStatus, isMcpStartupStatus, getMcpApproval, isMcpApproval, findMcpApprovals, getMcpInvocation, wasMcpToolInvoked, } from './mcp-evidence.js';
|
|
9
9
|
export { decideMcpToolCall, redactMcpSecrets, } from './mcp-safety.js';
|
|
10
10
|
export { evaluate, evaluateFlow, EvalScorerError } from './evaluate.js';
|
|
11
11
|
export { RUN_SNAPSHOT_VERSION, buildRunSnapshot, loadRunSnapshot, SnapshotParseError, SnapshotVersionError, WorkspaceMissingError, } from './snapshots.js';
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import type { ToolEvent } from '../tool-events.js';
|
|
2
|
+
export declare class McpEvidenceUnavailableError extends TypeError {
|
|
3
|
+
readonly name = "McpEvidenceUnavailableError";
|
|
4
|
+
}
|
|
2
5
|
export type McpInvocationResult = {
|
|
3
6
|
serverName: string;
|
|
4
7
|
toolName: string;
|
|
@@ -46,6 +49,7 @@ export interface ExpectedMcpToolCall {
|
|
|
46
49
|
status?: string;
|
|
47
50
|
invocation?: McpInvocationResult['invocation'];
|
|
48
51
|
outcome?: McpInvocationResult['outcome'];
|
|
52
|
+
/** Throws when persisted redaction makes the requested argument match unknowable. */
|
|
49
53
|
argumentsContaining?: Record<string, unknown>;
|
|
50
54
|
}
|
|
51
55
|
export interface McpStartupStatusEvidence {
|
package/dist/sdk/mcp-evidence.js
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
1
|
+
const REDACTED_ARGUMENTS_ERROR = "argumentsContaining cannot be evaluated against redacted MCP arguments; use the scorer's ctx.toolEvents with deterministicToolEvidence: 'live'";
|
|
2
|
+
export class McpEvidenceUnavailableError extends TypeError {
|
|
3
|
+
name = 'McpEvidenceUnavailableError';
|
|
4
|
+
}
|
|
1
5
|
export function getMcpToolCall(event) {
|
|
2
6
|
if (event.action !== 'mcp_tool_call')
|
|
3
7
|
return undefined;
|
|
4
8
|
const args = event.arguments ?? {};
|
|
9
|
+
const separator = event.providerToolName.indexOf('.');
|
|
5
10
|
const serverName = event.mcp?.serverName
|
|
6
|
-
?? (typeof args.server === 'string' ? args.server : undefined)
|
|
11
|
+
?? (typeof args.server === 'string' ? args.server : undefined)
|
|
12
|
+
?? (separator > 0 ? event.providerToolName.slice(0, separator) : undefined);
|
|
7
13
|
const toolName = event.mcp?.toolName
|
|
8
|
-
?? (typeof args.tool === 'string' ? args.tool : undefined)
|
|
14
|
+
?? (typeof args.tool === 'string' ? args.tool : undefined)
|
|
15
|
+
?? (separator > 0 && separator < event.providerToolName.length - 1
|
|
16
|
+
? event.providerToolName.slice(separator + 1) : undefined);
|
|
9
17
|
const argumentStatus = typeof args.status === 'string' ? args.status : undefined;
|
|
10
|
-
const status = mcpClassificationStatus(event.mcp, argumentStatus) ?? argumentStatus;
|
|
18
|
+
const status = mcpClassificationStatus(event.mcp, argumentStatus) ?? argumentStatus ?? event.status;
|
|
11
19
|
if (!serverName || !toolName || !status)
|
|
12
20
|
return undefined;
|
|
13
21
|
return {
|
|
@@ -151,29 +159,46 @@ function matchesMcpToolCall(call, expected) {
|
|
|
151
159
|
if (expected.outcome !== undefined && invocation.outcome !== expected.outcome)
|
|
152
160
|
return false;
|
|
153
161
|
}
|
|
154
|
-
if (expected.argumentsContaining && !containsArguments(call.arguments, expected.argumentsContaining)) {
|
|
162
|
+
if (expected.argumentsContaining && !containsArguments(call.arguments, expected.argumentsContaining, call.event.redactedArgumentPaths)) {
|
|
155
163
|
return false;
|
|
156
164
|
}
|
|
157
165
|
return true;
|
|
158
166
|
}
|
|
159
|
-
function containsArguments(actual, expected) {
|
|
160
|
-
|
|
167
|
+
function containsArguments(actual, expected, redactedPaths = []) {
|
|
168
|
+
const matches = Object.entries(expected).every(([key, expectedValue]) => valuesEqual(actual[key], expectedValue, `/${escapeJsonPointer(key)}`, redactedPaths));
|
|
169
|
+
return matches;
|
|
161
170
|
}
|
|
162
|
-
function valuesEqual(actual, expected) {
|
|
163
|
-
if (Object.is(actual, expected))
|
|
171
|
+
function valuesEqual(actual, expected, path, redactedPaths) {
|
|
172
|
+
if (Object.is(actual, expected)) {
|
|
173
|
+
assertMcpArgumentAvailable(path, redactedPaths);
|
|
164
174
|
return true;
|
|
175
|
+
}
|
|
165
176
|
if (Array.isArray(actual) || Array.isArray(expected)) {
|
|
166
|
-
if (
|
|
167
|
-
return
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
return actual.every((value, index) => valuesEqual(value, expected[index]));
|
|
177
|
+
if (Array.isArray(actual) && Array.isArray(expected) && actual.length === expected.length) {
|
|
178
|
+
return actual.every((value, index) => valuesEqual(value, expected[index], `${path}/${index}`, redactedPaths));
|
|
179
|
+
}
|
|
180
|
+
return rejectRedactedMismatch(path, redactedPaths);
|
|
171
181
|
}
|
|
172
182
|
if (isRecord(actual) && isRecord(expected)) {
|
|
173
|
-
return Object.entries(expected).every(([key, value]) => valuesEqual(actual[key], value));
|
|
183
|
+
return Object.entries(expected).every(([key, value]) => valuesEqual(actual[key], value, `${path}/${escapeJsonPointer(key)}`, redactedPaths));
|
|
174
184
|
}
|
|
185
|
+
return rejectRedactedMismatch(path, redactedPaths);
|
|
186
|
+
}
|
|
187
|
+
function rejectRedactedMismatch(path, redactedPaths) {
|
|
188
|
+
assertMcpArgumentAvailable(path, redactedPaths);
|
|
175
189
|
return false;
|
|
176
190
|
}
|
|
191
|
+
function assertMcpArgumentAvailable(path, redactedPaths) {
|
|
192
|
+
if (redactedPaths.some((redactedPath) => redactedPath === ''
|
|
193
|
+
|| redactedPath === path
|
|
194
|
+
|| redactedPath.startsWith(`${path}/`)
|
|
195
|
+
|| path.startsWith(`${redactedPath}/`))) {
|
|
196
|
+
throw new McpEvidenceUnavailableError(REDACTED_ARGUMENTS_ERROR);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
function escapeJsonPointer(value) {
|
|
200
|
+
return value.replaceAll('~', '~0').replaceAll('/', '~1');
|
|
201
|
+
}
|
|
177
202
|
function isRecord(value) {
|
|
178
203
|
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
179
204
|
}
|
package/dist/sdk/run-scorer.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { runJudgePipeline } from './judge-pipeline.js';
|
|
2
2
|
import { runJudgeSession } from './judge-tool-session.js';
|
|
3
3
|
import { clamp, makeErroredResult, matchesExpectation } from './scorer-utils.js';
|
|
4
|
+
import { McpEvidenceUnavailableError } from './mcp-evidence.js';
|
|
4
5
|
export async function runScorer(scorer, ctx, opts = {}) {
|
|
5
6
|
switch (scorer.type) {
|
|
6
7
|
case 'check':
|
|
@@ -56,6 +57,9 @@ async function runCheckScorer(scorer, ctx) {
|
|
|
56
57
|
};
|
|
57
58
|
}
|
|
58
59
|
catch (error) {
|
|
60
|
+
if (error instanceof McpEvidenceUnavailableError) {
|
|
61
|
+
return makeUnavailableEvidenceResult('check', scorer.name, scorer.weight, error);
|
|
62
|
+
}
|
|
59
63
|
return makeErroredResult('check', scorer.name, scorer.weight, error);
|
|
60
64
|
}
|
|
61
65
|
}
|
|
@@ -81,9 +85,15 @@ async function runScoreScorer(scorer, ctx) {
|
|
|
81
85
|
};
|
|
82
86
|
}
|
|
83
87
|
catch (error) {
|
|
88
|
+
if (error instanceof McpEvidenceUnavailableError) {
|
|
89
|
+
return makeUnavailableEvidenceResult('score', scorer.name, scorer.weight, error);
|
|
90
|
+
}
|
|
84
91
|
return makeErroredResult('score', scorer.name, scorer.weight, error);
|
|
85
92
|
}
|
|
86
93
|
}
|
|
94
|
+
function makeUnavailableEvidenceResult(type, name, weight, error) {
|
|
95
|
+
return { name, type, score: 0, weight, details: error.message, status: 'ok' };
|
|
96
|
+
}
|
|
87
97
|
async function runToolUsageScorer(scorer, ctx) {
|
|
88
98
|
try {
|
|
89
99
|
const toolEvents = ctx.toolEvents;
|
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
|
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
1
2
|
import { collectStructuredSensitiveValues, isSensitiveValueScanLimitError, sanitizePersistenceValue, } from '../tool-event-results.js';
|
|
2
3
|
import { attachToolEventSensitiveValues, cloneToolEventWithRuntimeMetadata, getToolEventSensitiveValues, redactToolEventPayload, } from './tool-event-secrets.js';
|
|
4
|
+
import { getOriginalMcpInput } from './mcp-event-input.js';
|
|
5
|
+
const MCP_METADATA_ARGUMENTS = new Set(['server', 'tool', 'status']);
|
|
3
6
|
export function buildToolEventLogEntry(toolEvent, fallbackTimestamp) {
|
|
4
7
|
let event = toolEvent;
|
|
5
8
|
let discoveredValues;
|
|
@@ -16,10 +19,37 @@ export function buildToolEventLogEntry(toolEvent, fallbackTimestamp) {
|
|
|
16
19
|
...getToolEventSensitiveValues(toolEvent),
|
|
17
20
|
...discoveredValues,
|
|
18
21
|
])];
|
|
19
|
-
const
|
|
22
|
+
const sanitizedEvent = sanitizePersistenceValue(event, sensitiveValues);
|
|
23
|
+
const originalInput = getOriginalMcpInput(event);
|
|
24
|
+
const originalArguments = originalInput ? {
|
|
25
|
+
...event.arguments,
|
|
26
|
+
...Object.fromEntries(Object.entries(originalInput).filter(([key]) => !MCP_METADATA_ARGUMENTS.has(key))),
|
|
27
|
+
} : event.arguments;
|
|
28
|
+
const redactedArgumentPaths = [...new Set([
|
|
29
|
+
...(event.redactedArgumentPaths ?? []),
|
|
30
|
+
...collectChangedPaths(originalArguments, sanitizedEvent.arguments),
|
|
31
|
+
])];
|
|
32
|
+
const persistedEvent = attachToolEventSensitiveValues(cloneToolEventWithRuntimeMetadata(event, {
|
|
33
|
+
...sanitizedEvent,
|
|
34
|
+
...(redactedArgumentPaths.length > 0 ? { redactedArgumentPaths } : {}),
|
|
35
|
+
}), sensitiveValues);
|
|
20
36
|
return {
|
|
21
37
|
type: 'tool_event',
|
|
22
38
|
timestamp: persistedEvent.startedAt ?? fallbackTimestamp,
|
|
23
39
|
tool_event: persistedEvent,
|
|
24
40
|
};
|
|
25
41
|
}
|
|
42
|
+
function collectChangedPaths(original, sanitized, path = '') {
|
|
43
|
+
if (isDeepStrictEqual(original, sanitized))
|
|
44
|
+
return [];
|
|
45
|
+
if (Array.isArray(original) && Array.isArray(sanitized) && original.length === sanitized.length) {
|
|
46
|
+
return original.flatMap((value, index) => collectChangedPaths(value, sanitized[index], `${path}/${index}`));
|
|
47
|
+
}
|
|
48
|
+
if (isRecord(original) && isRecord(sanitized)) {
|
|
49
|
+
return Object.keys(original).flatMap((key) => collectChangedPaths(original[key], sanitized[key], `${path}/${key.replaceAll('~', '~0').replaceAll('/', '~1')}`));
|
|
50
|
+
}
|
|
51
|
+
return [path];
|
|
52
|
+
}
|
|
53
|
+
function isRecord(value) {
|
|
54
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
55
|
+
}
|
package/dist/sdk/types.d.ts
CHANGED
|
@@ -348,6 +348,7 @@ export interface ScorerContext {
|
|
|
348
348
|
workspace: string;
|
|
349
349
|
log: LogEntry[];
|
|
350
350
|
transcript: string;
|
|
351
|
+
/** Use this scorer-context field for live MCP arguments; `log` always remains sanitized. */
|
|
351
352
|
toolEvents: import('../tool-events.js').ToolEvent[];
|
|
352
353
|
runCommand: (cmd: string) => Promise<CommandResult>;
|
|
353
354
|
artifacts: SessionArtifacts;
|
|
@@ -360,7 +361,7 @@ export interface EvaluateOptions {
|
|
|
360
361
|
failFast?: boolean;
|
|
361
362
|
llm?: LLMPort;
|
|
362
363
|
onScorerError?: 'skip' | 'zero' | 'fail';
|
|
363
|
-
/**
|
|
364
|
+
/** Restores exact MCP arguments only on deterministic scorers' `ctx.toolEvents`; `agent.log` remains sanitized. Live mode is unavailable for snapshot replay. */
|
|
364
365
|
deterministicToolEvidence?: 'persisted' | 'live';
|
|
365
366
|
/** Stable identity for this evaluation definition across separate runs. */
|
|
366
367
|
evaluationDefinitionKey?: string;
|
package/dist/tool-events.d.ts
CHANGED
|
@@ -20,6 +20,8 @@ export interface ToolEvent {
|
|
|
20
20
|
toolUseId?: string;
|
|
21
21
|
turnNumber?: number;
|
|
22
22
|
arguments?: Record<string, unknown>;
|
|
23
|
+
/** Producer-controlled JSON Pointer paths identifying persisted arguments that were sanitized. */
|
|
24
|
+
redactedArgumentPaths?: string[];
|
|
23
25
|
/** Lifecycle state observed by PathGrade. Absent on legacy provider events. */
|
|
24
26
|
status?: 'completed' | 'error' | 'incomplete';
|
|
25
27
|
/** Receipt/enforcement-backed classification for canonical MCP calls. */
|