@wix/pathgrade 1.0.30 → 1.0.31

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.
@@ -23,6 +23,7 @@ export declare function normalizeJestRunResults(input: {
23
23
  run: AdapterRunHandle;
24
24
  results: JestAggregatedResult;
25
25
  metadataByCaseId?: Map<string, PathgradeTestMeta[]>;
26
+ cwd?: string;
26
27
  }): NormalizedRunSnapshot;
27
28
  export declare function jestCaseId(input: {
28
29
  filePath: string;
@@ -1,3 +1,4 @@
1
+ import { resolveSourceMetadata } from '../../reporting/source-metadata.js';
1
2
  export function normalizeJestRunResults(input) {
2
3
  const runId = `${input.run.adapterName}:run`;
3
4
  const files = input.results.testResults ?? [];
@@ -5,6 +6,7 @@ export function normalizeJestRunResults(input) {
5
6
  id: unitId(index),
6
7
  runId,
7
8
  displayName: fileResult.testFilePath,
9
+ ...resolveSourceMetadata(fileResult.testFilePath, input.cwd ?? process.cwd()),
8
10
  groupingHints: [{
9
11
  kind: 'source',
10
12
  key: fileResult.testFilePath,
@@ -68,6 +70,10 @@ function runCase(input) {
68
70
  : {}),
69
71
  ...(entry.recordedAt ? { recordedAt: entry.recordedAt } : {}),
70
72
  ...(entry.agent ? { agent: entry.agent } : {}),
73
+ ...(entry.evaluationDefinitionKey
74
+ ? { evaluationDefinitionKey: entry.evaluationDefinitionKey }
75
+ : {}),
76
+ ...(entry.scorerRevision ? { scorerRevision: entry.scorerRevision } : {}),
71
77
  }));
72
78
  const failureMessage = input.assertion.failureMessages?.find(message => message.trim().length > 0);
73
79
  return {
@@ -1,9 +1,11 @@
1
1
  import { normalizeJestRunResults } from './results.js';
2
2
  import { discoverPathgradeEvalFiles, } from '@wix/pathgrade/adapter-kit';
3
3
  export function createJestAdapter(options = {}) {
4
+ let discoveryCwd = process.cwd();
4
5
  return {
5
6
  name: 'jest',
6
7
  async discover(input) {
8
+ discoveryCwd = input.cwd;
7
9
  const include = input.include ?? ['**/*.eval.ts'];
8
10
  const exclude = input.exclude ?? [];
9
11
  const files = discoverPathgradeEvalFiles({
@@ -34,6 +36,7 @@ export function createJestAdapter(options = {}) {
34
36
  run,
35
37
  results: native.results ?? { testResults: [] },
36
38
  metadataByCaseId: native.metadataByCaseId,
39
+ cwd: discoveryCwd,
37
40
  });
38
41
  },
39
42
  };
@@ -85,6 +85,8 @@ function toReportEvaluations(evaluations) {
85
85
  scoringDurationMs: entry.scoringDurationMs,
86
86
  recordedAt: entry.recordedAt,
87
87
  agent: entry.agent,
88
+ evaluationDefinitionKey: entry.evaluationDefinitionKey,
89
+ scorerRevision: entry.scorerRevision,
88
90
  }));
89
91
  }
90
92
  function writeCase(testCase) {
@@ -37,7 +37,7 @@ export function createNodeTestAdapter() {
37
37
  adapterName: this.name,
38
38
  status: 'completed',
39
39
  exitCode: 0,
40
- native: { resultsPath, resultsDir },
40
+ native: { resultsPath, resultsDir, cwd },
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 },
57
+ native: { resultsPath, resultsDir, cwd },
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
- })));
74
+ })), { cwd: native.cwd });
75
75
  }
76
76
  finally {
77
77
  if (native.resultsDir)
@@ -103,10 +103,11 @@ function readNodeTestRunNative(run) {
103
103
  if (typeof run.native === 'object'
104
104
  && run.native !== null
105
105
  && typeof run.native.resultsPath === 'string'
106
- && typeof run.native.resultsDir === 'string') {
106
+ && typeof run.native.resultsDir === 'string'
107
+ && typeof run.native.cwd === 'string') {
107
108
  return run.native;
108
109
  }
109
- return { resultsPath: '', resultsDir: '' };
110
+ return { resultsPath: '', resultsDir: '', cwd: process.cwd() };
110
111
  }
111
112
  async function readCases(resultsPath) {
112
113
  if (!resultsPath || !(await fs.pathExists(resultsPath)))
@@ -0,0 +1,10 @@
1
+ import type { ComparisonContract, EvalReport } from '../types.js';
2
+ import type { Scorer } from '../sdk/types.js';
3
+ import type { ReportGroupInput } from './types.js';
4
+ export declare function buildComparisonContract(input: {
5
+ group: ReportGroupInput;
6
+ report: EvalReport;
7
+ attemptsRequested: number;
8
+ attemptsCompleted: number;
9
+ }): ComparisonContract;
10
+ export declare function createScorerRevision(scorers: readonly Scorer[]): string | undefined;
@@ -0,0 +1,127 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { canonicalizeJson } from '../core/canonical-json.js';
3
+ export function buildComparisonContract(input) {
4
+ const unavailable = new Set();
5
+ const evaluations = input.group.cases.flatMap(testCase => {
6
+ const caseIdentity = testCase.repeatKey ?? testCase.caseId ?? testCase.name;
7
+ return (testCase.attempts?.flatMap(attempt => attempt.evaluations ?? [])
8
+ ?? testCase.evaluations
9
+ ?? [])
10
+ .filter(evaluation => evaluation.resultKind !== 'synthetic_no_evaluation')
11
+ .map((evaluation, index) => ({
12
+ case: caseIdentity,
13
+ definition: evaluation.evaluationDefinitionKey ?? `position:${index}`,
14
+ scorer: evaluation.scorerRevision,
15
+ runtime: runtimeIdentity(evaluation.agent ?? evaluation.trial?.agent),
16
+ }));
17
+ });
18
+ const definitionRevision = input.group.sourceRevision
19
+ ? revision('definition', {
20
+ source: input.group.sourceRevision,
21
+ evaluations: uniqueSorted(evaluations.map(evaluation => ({
22
+ case: evaluation.case, definition: evaluation.definition,
23
+ }))),
24
+ })
25
+ : undefined;
26
+ if (!definitionRevision)
27
+ unavailable.add('definition_metadata_missing');
28
+ const scorerRevision = evaluations.length > 0 && evaluations.every(evaluation => evaluation.scorer)
29
+ ? revision('scorers', uniqueSorted(evaluations.map(evaluation => ({
30
+ case: evaluation.case,
31
+ definition: evaluation.definition,
32
+ scorer: evaluation.scorer,
33
+ }))))
34
+ : undefined;
35
+ if (!scorerRevision)
36
+ unavailable.add('scorer_metadata_missing');
37
+ const runtimeComplete = evaluations.length > 0
38
+ && evaluations.every(evaluation => Object.values(evaluation.runtime)
39
+ .every(value => value !== undefined));
40
+ const runtimeRevision = runtimeComplete
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');
49
+ const reportableCases = input.group.cases.filter(testCase => testCase.reportable !== false
50
+ && testCase.state !== 'skipped' && testCase.state !== 'pending');
51
+ const samplingComplete = input.attemptsCompleted === input.attemptsRequested
52
+ && reportableCases.every(testCase => (testCase.attempts?.length ?? (testCase.evaluations === undefined ? 0 : 1)) === input.attemptsRequested)
53
+ && reportableCases.every(testCase => testCase.attempts
54
+ ? testCase.attempts.every(attempt => hasTerminalEvaluation(attempt.evaluations))
55
+ : hasTerminalEvaluation(testCase.evaluations));
56
+ const samplingRevision = samplingComplete
57
+ ? revision('sampling', {
58
+ attempts: input.attemptsRequested,
59
+ cases: reportableCases.map(testCase => testCase.repeatKey ?? testCase.caseId ?? testCase.name).toSorted(),
60
+ })
61
+ : undefined;
62
+ if (!samplingRevision)
63
+ unavailable.add('sampling_incomplete');
64
+ return {
65
+ version: 1,
66
+ ...(definitionRevision ? { definition_revision: definitionRevision } : {}),
67
+ ...(scorerRevision ? { scorer_revision: scorerRevision } : {}),
68
+ ...(runtimeRevision ? { runtime_revision: runtimeRevision } : {}),
69
+ ...(samplingRevision ? { sampling_revision: samplingRevision } : {}),
70
+ report_schema_revision: 'pathgrade-results-v2',
71
+ ...(unavailable.size > 0 ? { unavailable_reasons: [...unavailable].sort() } : {}),
72
+ };
73
+ }
74
+ function hasTerminalEvaluation(evaluations) {
75
+ return evaluations?.some(evaluation => evaluation.resultKind !== 'synthetic_no_evaluation') === true;
76
+ }
77
+ export function createScorerRevision(scorers) {
78
+ try {
79
+ return revision('scorer-declaration', scorers
80
+ .map(scorer => canonicalizeJson(scorerDeclaration(scorer))).toSorted());
81
+ }
82
+ catch {
83
+ return undefined;
84
+ }
85
+ }
86
+ function runtimeIdentity(agent) {
87
+ return {
88
+ name: agent?.name,
89
+ model: agent?.resolvedModel,
90
+ transport: agent?.transport,
91
+ interaction: agent?.interactionMode,
92
+ };
93
+ }
94
+ function scorerDeclaration(scorer) {
95
+ const common = { type: scorer.type, name: scorer.name.normalize('NFC'), weight: scorer.weight };
96
+ if (scorer.type === 'check' || scorer.type === 'score') {
97
+ return { ...common, function: scorer.fn.toString() };
98
+ }
99
+ if (scorer.type === 'tool_usage') {
100
+ return {
101
+ ...common,
102
+ expectations: scorer.expectations
103
+ .toSorted((left, right) => canonicalizeJson(left).localeCompare(canonicalizeJson(right))),
104
+ };
105
+ }
106
+ return {
107
+ ...common,
108
+ rubric: scorer.rubric,
109
+ model: scorer.model ?? null,
110
+ retry: scorer.retry ?? null,
111
+ includeToolEvents: scorer.includeToolEvents ?? null,
112
+ input: typeof scorer.input === 'function' ? scorer.input.toString() : scorer.input ?? null,
113
+ tools: scorer.tools?.toSorted() ?? null,
114
+ maxRounds: scorer.maxRounds ?? null,
115
+ cacheControl: scorer.cacheControl ?? null,
116
+ };
117
+ }
118
+ function revision(namespace, value) {
119
+ return `sha256:${createHash('sha256')
120
+ .update(canonicalizeJson(['pathgrade-comparison', 1, namespace, value]))
121
+ .digest('hex')}`;
122
+ }
123
+ function uniqueSorted(values) {
124
+ return [...new Map(values.map(value => [canonicalizeJson(value), value])).entries()]
125
+ .toSorted(([left], [right]) => left.localeCompare(right))
126
+ .map(([, value]) => value);
127
+ }
@@ -1,5 +1,6 @@
1
1
  import { buildDiagnosticsReport } from '../sdk/diagnostics.js';
2
2
  import { extractSkillsFromLog } from '../tool-events.js';
3
+ import { buildComparisonContract } from './comparison-contract.js';
3
4
  export function buildPathgradeReport(input) {
4
5
  const warnings = [];
5
6
  const consolidatedGroups = [];
@@ -8,6 +9,7 @@ export function buildPathgradeReport(input) {
8
9
  const attemptsRequested = input.attemptsRequested ?? 1;
9
10
  const attemptsCompleted = input.attemptsCompleted ?? attemptsRequested;
10
11
  const builtGroups = input.groups.map(group => ({
12
+ input: group,
11
13
  groupName: group.groupName,
12
14
  cases: group.cases.map(toBuiltCase),
13
15
  }));
@@ -32,7 +34,18 @@ export function buildPathgradeReport(input) {
32
34
  return rest;
33
35
  });
34
36
  const { trials: _trials, ...rest } = report;
35
- consolidatedGroups.push({ ...rest, trials: strippedTrials, trace_file: traceFile });
37
+ consolidatedGroups.push({
38
+ ...rest,
39
+ trials: strippedTrials,
40
+ trace_file: traceFile,
41
+ ...(group.input.sourceFile ? { source_file: group.input.sourceFile } : {}),
42
+ comparison_contract: buildComparisonContract({
43
+ group: group.input,
44
+ report,
45
+ attemptsRequested,
46
+ attemptsCompleted,
47
+ }),
48
+ });
36
49
  }
37
50
  const scores = reportableGroups.flatMap(group => group.cases.flatMap(testCase => (testCase.attempts
38
51
  .filter(attempt => attempt.resultKind !== 'synthetic_no_evaluation')
@@ -0,0 +1,2 @@
1
+ import type { PathgradeReport } from '../types.js';
2
+ export declare function parsePathgradeReport(value: unknown): PathgradeReport;
@@ -0,0 +1,112 @@
1
+ export function parsePathgradeReport(value) {
2
+ if (!isRecord(value)
3
+ || (value.version !== 1 && value.version !== 2)
4
+ || typeof value.timestamp !== 'string'
5
+ || typeof value.overall_pass_rate !== 'number'
6
+ || (value.version === 2 && (typeof value.overall_mean_reward !== 'number' || !validAttemptCounts(value)))
7
+ || !optionalNumber(value.threshold)
8
+ || (value.status !== 'pass' && value.status !== 'fail')
9
+ || (value.run_kind !== undefined && value.run_kind !== 'evaluation' && value.run_kind !== 'no-affected')
10
+ || !Array.isArray(value.groups)
11
+ || !value.groups.every(group => isReportGroup(group, value.version))
12
+ || !isSelection(value.selection)) {
13
+ throw new Error('PathGrade results.json is missing or has an unsupported schema');
14
+ }
15
+ return value;
16
+ }
17
+ function isReportGroup(value, version) {
18
+ return isRecord(value)
19
+ && typeof value.task === 'string'
20
+ && (version === 1 ? legacyMetrics(value) : schemaV2Metrics(value))
21
+ && optionalString(value.source_file)
22
+ && typeof value.trace_file === 'string'
23
+ && Array.isArray(value.skills_used)
24
+ && value.skills_used.every(item => typeof item === 'string')
25
+ && Array.isArray(value.trials)
26
+ && value.trials.every(isTrial)
27
+ && (value.comparison_contract === undefined || isComparisonContract(value.comparison_contract));
28
+ }
29
+ function isTrial(value) {
30
+ return isRecord(value)
31
+ && typeof value.trial_id === 'number'
32
+ && typeof value.duration_ms === 'number'
33
+ && typeof value.input_tokens === 'number'
34
+ && typeof value.output_tokens === 'number'
35
+ && optionalNumber(value.reward)
36
+ && optionalNumber(value.conversation_input_tokens)
37
+ && optionalNumber(value.conversation_output_tokens)
38
+ && optionalNumber(value.conversation_cost_usd)
39
+ && Array.isArray(value.scorer_results)
40
+ && value.scorer_results.every(scorer => isRecord(scorer)
41
+ && typeof scorer.scorer_type === 'string'
42
+ && typeof scorer.score === 'number'
43
+ && typeof scorer.weight === 'number');
44
+ }
45
+ function legacyMetrics(value) {
46
+ return typeof value.pass_rate === 'number'
47
+ && typeof value.pass_at_k === 'number'
48
+ && typeof value.pass_pow_k === 'number';
49
+ }
50
+ function schemaV2Metrics(value) {
51
+ return (value.status === 'pass' || value.status === 'fail')
52
+ && typeof value.mean_reward === 'number'
53
+ && optionalNumber(value.success_rate)
54
+ && (value.pass_at_k === undefined || (isRecord(value.pass_at_k)
55
+ && Object.values(value.pass_at_k).every(item => typeof item === 'number')))
56
+ && (value.pass_at_k_method === undefined || value.pass_at_k_method === 'finite_sample_unbiased')
57
+ && (value.pass_at_k_unavailable_reason === undefined
58
+ || value.pass_at_k_unavailable_reason === 'non_binary_reward'
59
+ || value.pass_at_k_unavailable_reason === 'incomplete_attempts'
60
+ || value.pass_at_k_unavailable_reason === 'case_identity_mismatch')
61
+ && optionalNonNegativeInteger(value.eligible_case_count)
62
+ && optionalPositiveInteger(value.attempts_per_case);
63
+ }
64
+ function validAttemptCounts(value) {
65
+ return typeof value.attempts_requested === 'number'
66
+ && Number.isSafeInteger(value.attempts_requested)
67
+ && value.attempts_requested >= 1
68
+ && typeof value.attempts_completed === 'number'
69
+ && Number.isSafeInteger(value.attempts_completed)
70
+ && value.attempts_completed >= 0
71
+ && value.attempts_completed <= value.attempts_requested;
72
+ }
73
+ function isSelection(value) {
74
+ return value === undefined || (isRecord(value)
75
+ && typeof value.base_ref === 'string'
76
+ && typeof value.changed_files_count === 'number'
77
+ && Array.isArray(value.selected)
78
+ && value.selected.every(item => typeof item === 'string')
79
+ && Array.isArray(value.skipped)
80
+ && value.skipped.every(item => isRecord(item)
81
+ && typeof item.file === 'string'
82
+ && item.reason === 'no-matching-deps'));
83
+ }
84
+ function isComparisonContract(value) {
85
+ return isRecord(value)
86
+ && value.version === 1
87
+ && optionalString(value.definition_revision)
88
+ && optionalString(value.scorer_revision)
89
+ && optionalString(value.runtime_revision)
90
+ && optionalString(value.sampling_revision)
91
+ && value.report_schema_revision === 'pathgrade-results-v2'
92
+ && (value.unavailable_reasons === undefined || (Array.isArray(value.unavailable_reasons)
93
+ && value.unavailable_reasons.every(reason => reason === 'definition_metadata_missing'
94
+ || reason === 'scorer_metadata_missing'
95
+ || reason === 'runtime_metadata_missing'
96
+ || reason === 'sampling_incomplete')));
97
+ }
98
+ function optionalString(value) {
99
+ return value === undefined || typeof value === 'string';
100
+ }
101
+ function optionalNumber(value) {
102
+ return value === undefined || typeof value === 'number';
103
+ }
104
+ function optionalNonNegativeInteger(value) {
105
+ return value === undefined || (typeof value === 'number' && Number.isSafeInteger(value) && value >= 0);
106
+ }
107
+ function optionalPositiveInteger(value) {
108
+ return value === undefined || (typeof value === 'number' && Number.isSafeInteger(value) && value >= 1);
109
+ }
110
+ function isRecord(value) {
111
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
112
+ }
@@ -0,0 +1,5 @@
1
+ export interface SourceMetadata {
2
+ sourceFile?: string;
3
+ sourceRevision?: string;
4
+ }
5
+ export declare function resolveSourceMetadata(filePath: string | undefined, cwd: string): SourceMetadata;
@@ -0,0 +1,20 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readFileSync } from 'node:fs';
3
+ import * as path from 'node:path';
4
+ export function resolveSourceMetadata(filePath, cwd) {
5
+ if (!filePath)
6
+ return {};
7
+ const absolute = path.resolve(cwd, filePath);
8
+ const relative = path.relative(cwd, absolute).replaceAll(path.sep, '/');
9
+ if (!relative || relative === '..' || relative.startsWith('../'))
10
+ return {};
11
+ try {
12
+ return {
13
+ sourceFile: relative,
14
+ sourceRevision: `sha256:${createHash('sha256').update(readFileSync(absolute)).digest('hex')}`,
15
+ };
16
+ }
17
+ catch {
18
+ return { sourceFile: relative };
19
+ }
20
+ }
@@ -12,6 +12,8 @@ export interface ReportRunInput {
12
12
  }
13
13
  export interface ReportGroupInput {
14
14
  groupName: string;
15
+ sourceFile?: string;
16
+ sourceRevision?: string;
15
17
  cases: ReportCaseInput[];
16
18
  }
17
19
  export interface ReportCaseInput {
@@ -45,6 +47,8 @@ export interface ReportEvaluationInput {
45
47
  scoringDurationMs?: number;
46
48
  recordedAt?: string;
47
49
  agent?: AgentExecutionMetadata;
50
+ evaluationDefinitionKey?: string;
51
+ scorerRevision?: string;
48
52
  }
49
53
  export interface PathgradeReportBuildResult {
50
54
  report: PathgradeReport;
@@ -1,4 +1,6 @@
1
1
  import type { ReportGroupInput } from '../reporting/types.js';
2
2
  import type { AdapterRunHandle } from './adapter.js';
3
3
  import type { NormalizedRunSnapshot } from './model.js';
4
- export declare function buildNormalizedRunSnapshotFromReportGroups(run: AdapterRunHandle, groups: ReportGroupInput[]): NormalizedRunSnapshot;
4
+ export declare function buildNormalizedRunSnapshotFromReportGroups(run: AdapterRunHandle, groups: ReportGroupInput[], options?: {
5
+ cwd?: string;
6
+ }): NormalizedRunSnapshot;
@@ -1,4 +1,6 @@
1
- export function buildNormalizedRunSnapshotFromReportGroups(run, groups) {
1
+ import { resolveSourceMetadata } from '../reporting/source-metadata.js';
2
+ export function buildNormalizedRunSnapshotFromReportGroups(run, groups, options = {}) {
3
+ const cwd = options.cwd ?? process.cwd();
2
4
  return {
3
5
  version: 1,
4
6
  completeness: 'final',
@@ -12,12 +14,18 @@ export function buildNormalizedRunSnapshotFromReportGroups(run, groups) {
12
14
  message: diagnostic.message,
13
15
  })) } : {}),
14
16
  },
15
- units: groups.map((group, index) => ({
16
- id: unitId(group.groupName, index),
17
- runId: `${run.adapterName}:run`,
18
- displayName: group.groupName,
19
- groupingHints: [{ kind: 'suite', key: group.groupName, label: group.groupName, order: index }],
20
- })),
17
+ units: groups.map((group, index) => {
18
+ const source = group.sourceFile && group.sourceRevision
19
+ ? { sourceFile: group.sourceFile, sourceRevision: group.sourceRevision }
20
+ : resolveSourceMetadata(group.sourceFile ?? group.cases.map(testCase => testCase.filePath ?? testCase.sourceRef).find(Boolean), cwd);
21
+ return {
22
+ id: unitId(group.groupName, index),
23
+ runId: `${run.adapterName}:run`,
24
+ displayName: group.groupName,
25
+ ...source,
26
+ groupingHints: [{ kind: 'suite', key: group.groupName, label: group.groupName, order: index }],
27
+ };
28
+ }),
21
29
  cases: groups.flatMap((group, groupIndex) => group.cases.map((testCase, caseIndex) => {
22
30
  const caseId = testCase.caseId ?? `${unitId(group.groupName, groupIndex)}:case-${caseIndex + 1}`;
23
31
  const attemptId = `${caseId}:attempt-1`;
@@ -33,6 +41,10 @@ export function buildNormalizedRunSnapshotFromReportGroups(run, groups) {
33
41
  : {}),
34
42
  ...(evaluation.recordedAt ? { recordedAt: evaluation.recordedAt } : {}),
35
43
  ...(evaluation.agent ? { agent: evaluation.agent } : {}),
44
+ ...(evaluation.evaluationDefinitionKey
45
+ ? { evaluationDefinitionKey: evaluation.evaluationDefinitionKey }
46
+ : {}),
47
+ ...(evaluation.scorerRevision ? { scorerRevision: evaluation.scorerRevision } : {}),
36
48
  }));
37
49
  return {
38
50
  id: caseId,
@@ -25,6 +25,8 @@ export interface EvalUnitRecord {
25
25
  id: string;
26
26
  runId: string;
27
27
  displayName: string;
28
+ sourceFile?: string;
29
+ sourceRevision?: string;
28
30
  diagnostics?: Diagnostic[];
29
31
  nativeReferences?: NativeReference[];
30
32
  groupingHints?: GroupingHint[];
@@ -92,6 +94,8 @@ export interface EvaluationRecord {
92
94
  scoringDurationMs?: number;
93
95
  recordedAt?: string;
94
96
  agent?: import('../sdk/types.js').AgentExecutionMetadata;
97
+ evaluationDefinitionKey?: string;
98
+ scorerRevision?: string;
95
99
  }
96
100
  export interface AssertionRecord {
97
101
  id: string;
@@ -16,9 +16,15 @@ export function projectNormalizedRunSnapshotToReportInput(snapshot, options = {}
16
16
  ?? preferredGroupingLabel(unit?.groupingHints)
17
17
  ?? unit?.displayName;
18
18
  const key = groupName ?? snapshot.model.run.adapterName;
19
- const group = groups.get(key) ?? { groupName: key, cases: [] };
19
+ const group = groups.get(key) ?? {
20
+ groupName: key,
21
+ ...(unit?.sourceFile ? { sourceFile: unit.sourceFile } : {}),
22
+ ...(unit?.sourceRevision ? { sourceRevision: unit.sourceRevision } : {}),
23
+ cases: [],
24
+ };
20
25
  group.cases.push({
21
26
  caseId: runCase.id,
27
+ ...(runCase.repeatKey ? { repeatKey: runCase.repeatKey } : {}),
22
28
  name: runCase.name,
23
29
  state: runCase.state,
24
30
  ...(runCase.state === 'skipped' || runCase.state === 'pending' ? { reportable: repeated || runCase.scoringPolicy.kind !== 'non-scoring' } : {}),
@@ -53,6 +59,10 @@ function projectedEvaluations(runCase, attemptEvaluations) {
53
59
  ...(evaluation.scoringDurationMs !== undefined ? { scoringDurationMs: evaluation.scoringDurationMs } : {}),
54
60
  ...(evaluation.recordedAt ? { recordedAt: evaluation.recordedAt } : {}),
55
61
  ...(evaluation.agent ? { agent: evaluation.agent } : {}),
62
+ ...(evaluation.evaluationDefinitionKey
63
+ ? { evaluationDefinitionKey: evaluation.evaluationDefinitionKey }
64
+ : {}),
65
+ ...(evaluation.scorerRevision ? { scorerRevision: evaluation.scorerRevision } : {}),
56
66
  }));
57
67
  }
58
68
  if (runCase.scoringPolicy.kind === 'score') {
@@ -70,6 +80,10 @@ function projectedEvaluations(runCase, attemptEvaluations) {
70
80
  ...(evaluation.scoringDurationMs !== undefined ? { scoringDurationMs: evaluation.scoringDurationMs } : {}),
71
81
  ...(evaluation.recordedAt ? { recordedAt: evaluation.recordedAt } : {}),
72
82
  ...(evaluation.agent ? { agent: evaluation.agent } : {}),
83
+ ...(evaluation.evaluationDefinitionKey
84
+ ? { evaluationDefinitionKey: evaluation.evaluationDefinitionKey }
85
+ : {}),
86
+ ...(evaluation.scorerRevision ? { scorerRevision: evaluation.scorerRevision } : {}),
73
87
  }];
74
88
  }
75
89
  function preferredGroupingLabel(hints) {
@@ -2,9 +2,11 @@ import { discoverPathgradeEvalFiles } from '../evals/discovery.js';
2
2
  import { buildNormalizedRunSnapshotFromReportGroups } from './model-builders.js';
3
3
  import { buildDiagnosticsReport } from '../sdk/diagnostics.js';
4
4
  export function createVitestAdapter(options = {}) {
5
+ let discoveryCwd = process.cwd();
5
6
  return {
6
7
  name: 'vitest',
7
8
  async discover(input) {
9
+ discoveryCwd = input.cwd;
8
10
  const include = input.include ?? ['**/*.eval.ts'];
9
11
  const exclude = input.exclude ?? [];
10
12
  const files = discoverPathgradeEvalFiles({
@@ -28,11 +30,12 @@ export function createVitestAdapter(options = {}) {
28
30
  exitCode: input.signal?.aborted ? 1 : 0,
29
31
  native: {
30
32
  testModules: options.testModules ?? [],
33
+ cwd: discoveryCwd,
31
34
  },
32
35
  };
33
36
  },
34
37
  async collectNormalizedRunSnapshot(run) {
35
- return buildNormalizedRunSnapshotFromReportGroups(run, collectVitestReportGroups(readVitestRunNative(run).testModules));
38
+ return buildNormalizedRunSnapshotFromReportGroups(run, collectVitestReportGroups(readVitestRunNative(run).testModules), { cwd: readVitestRunNative(run).cwd });
36
39
  },
37
40
  };
38
41
  }
@@ -56,12 +59,13 @@ export function collectVitestReportGroups(testModules) {
56
59
  function readVitestRunNative(run) {
57
60
  if (isVitestRunNative(run.native))
58
61
  return run.native;
59
- return { testModules: [] };
62
+ return { testModules: [], cwd: process.cwd() };
60
63
  }
61
64
  function isVitestRunNative(native) {
62
65
  return typeof native === 'object'
63
66
  && native !== null
64
- && Array.isArray(native.testModules);
67
+ && Array.isArray(native.testModules)
68
+ && typeof native.cwd === 'string';
65
69
  }
66
70
  function getGroupName(testCase) {
67
71
  const modulePath = testCase.module.relativeModuleId;
@@ -94,6 +98,8 @@ function toReportCaseInput(testCase, groupName) {
94
98
  scoringDurationMs: entry.scoringDurationMs,
95
99
  recordedAt: entry.recordedAt,
96
100
  agent: entry.agent,
101
+ evaluationDefinitionKey: entry.evaluationDefinitionKey,
102
+ scorerRevision: entry.scorerRevision,
97
103
  })),
98
104
  diagnostics: normalized.diagnostics,
99
105
  };
@@ -1,4 +1,5 @@
1
1
  import { countShellCommandsFromLog, extractSkillsFromLog, extractToolEventsFromLog } from '../tool-events.js';
2
+ import { createScorerRevision } from '../reporting/comparison-contract.js';
2
3
  import { getRuntime } from './eval-runtime.js';
3
4
  import { emitEvalResult } from './result-capture.js';
4
5
  import { runJudgePipeline } from './judge-pipeline.js';
@@ -73,6 +74,7 @@ function makeEvaluateAgent() {
73
74
  : undefined;
74
75
  if (isFirstEval)
75
76
  conversationAttributed.add(agent);
77
+ const scorerRevision = createScorerRevision(scorers);
76
78
  const recordedResult = {
77
79
  ...evalResult,
78
80
  tokenUsage: deltaTokenUsage,
@@ -82,6 +84,7 @@ function makeEvaluateAgent() {
82
84
  ...(opts?.evaluationDefinitionKey
83
85
  ? { evaluationDefinitionKey: opts.evaluationDefinitionKey }
84
86
  : {}),
87
+ ...(scorerRevision ? { scorerRevision } : {}),
85
88
  trial: buildTrialResult(agent.log, { ...evalResult, tokenUsage: deltaTokenUsage }, agent.scenarioEvidence, conversationTokens, conversationCost),
86
89
  };
87
90
  emitEvalResult({ result: recordedResult, agent });
@@ -120,6 +123,7 @@ async function fromSnapshot(snapshotPath, scorers, opts) {
120
123
  };
121
124
  const scoringStartedAt = performance.now();
122
125
  const evalResult = await evaluateWithContext(ctx, scorers, { ...opts, llm: trackedLLM });
126
+ const scorerRevision = createScorerRevision(scorers);
123
127
  const recordedResult = {
124
128
  ...evalResult,
125
129
  resultKind: 'snapshot',
@@ -128,6 +132,7 @@ async function fromSnapshot(snapshotPath, scorers, opts) {
128
132
  ...(opts?.evaluationDefinitionKey
129
133
  ? { evaluationDefinitionKey: opts.evaluationDefinitionKey }
130
134
  : {}),
135
+ ...(scorerRevision ? { scorerRevision } : {}),
131
136
  trial: buildTrialResult(snapshot.log, evalResult, snapshot.scenarioEvidence),
132
137
  };
133
138
  maybeThrowOnScorerErrors(recordedResult, opts?.onScorerError ?? 'skip');
@@ -21,6 +21,7 @@ export { DEFAULT_COPY_IGNORE } from '../providers/copy-filter.js';
21
21
  export { extractToolEventsFromLog } from '../tool-events.js';
22
22
  export { collectSensitiveEnvValues, sanitizePersistenceValue, } from '../tool-event-results.js';
23
23
  export { parseEnvFile } from '../utils/env.js';
24
+ export { parsePathgradeReport } from '../reporting/report-parser.js';
24
25
  export { createAskBus, requireAskBusForLiveBatches, AskBusTimeoutError } from './ask-bus/bus.js';
25
26
  export { toAskUserToolEvent } from './ask-bus/projection.js';
26
27
  export type { AskUserToolEvent, AskUserToolEventArguments, AskUserToolEventQuestionArgument, } from './ask-bus/projection.js';
@@ -40,6 +41,7 @@ export type { DiagnosticsReport } from './diagnostics.js';
40
41
  export type { ExpectedMcpStartupStatus, ExpectedMcpToolCall, McpStartupStatusEvidence, McpToolCallEvidence, McpApprovalEvidence, ExpectedMcpApproval, McpInvocationResult, } from './mcp-evidence.js';
41
42
  export type { McpPolicyDenialReason, McpToolCallRequest, McpToolPolicyDecision, } from './mcp-safety.js';
42
43
  export type { ToolEvent, McpToolCallClassification } from '../tool-events.js';
44
+ export type { ComparisonContract, ComparisonUnavailableReason, PathgradeGroupReport, PathgradeReport, StrippedTrialResult, } from '../types.js';
43
45
  export type { LLMPort, EvalRuntime } from './eval-runtime.js';
44
46
  export { createAgentLLM, createLLMClient, ProviderNotSupportedError } from '../utils/llm.js';
45
47
  export type { CreateLLMClientOptions, LLMProviderAdapter, TokenUsage as LLMTokenUsage } from '../utils/llm.js';
package/dist/sdk/index.js CHANGED
@@ -19,6 +19,7 @@ export { DEFAULT_COPY_IGNORE } from '../providers/copy-filter.js';
19
19
  export { extractToolEventsFromLog } from '../tool-events.js';
20
20
  export { collectSensitiveEnvValues, sanitizePersistenceValue, } from '../tool-event-results.js';
21
21
  export { parseEnvFile } from '../utils/env.js';
22
+ export { parsePathgradeReport } from '../reporting/report-parser.js';
22
23
  export { createAskBus, requireAskBusForLiveBatches, AskBusTimeoutError } from './ask-bus/bus.js';
23
24
  export { toAskUserToolEvent } from './ask-bus/projection.js';
24
25
  export { buildAskBatchLogEntries } from './agent-result-log.js';
@@ -81,6 +81,7 @@ function recordResult(result, agent, attribution) {
81
81
  ...(result.evaluationDefinitionKey
82
82
  ? { evaluationDefinitionKey: result.evaluationDefinitionKey }
83
83
  : {}),
84
+ ...(result.scorerRevision ? { scorerRevision: result.scorerRevision } : {}),
84
85
  trial: result.trial,
85
86
  resultKind: result.resultKind ?? 'evaluated',
86
87
  ...(result.scoringDurationMs !== undefined
@@ -372,6 +372,8 @@ export interface RecordedEvalResult extends Omit<EvalResult, 'score'> {
372
372
  scoringDurationMs?: number;
373
373
  recordedAt?: string;
374
374
  evaluationDefinitionKey?: string;
375
+ /** Privacy-safe revision of the declared scorers used by this evaluation. */
376
+ scorerRevision?: string;
375
377
  }
376
378
  export type EvaluationResultKind = 'evaluated' | 'synthetic_no_evaluation' | 'snapshot';
377
379
  export interface ScorerResultEntry {
@@ -393,6 +395,7 @@ export interface PathgradeTestMeta {
393
395
  scorers: ScorerResultEntry[];
394
396
  /** Stable identity for this evaluation definition across separate runs. */
395
397
  evaluationDefinitionKey?: string;
398
+ scorerRevision?: string;
396
399
  trial?: TrialResult;
397
400
  diagnostics?: DiagnosticsReport;
398
401
  resultKind?: EvaluationResultKind;
package/dist/types.d.ts CHANGED
@@ -219,6 +219,16 @@ export interface EvalReport {
219
219
  trials: TrialResult[];
220
220
  skills_used: string[];
221
221
  }
222
+ export type ComparisonUnavailableReason = 'definition_metadata_missing' | 'scorer_metadata_missing' | 'runtime_metadata_missing' | 'sampling_incomplete';
223
+ export interface ComparisonContract {
224
+ version: 1;
225
+ definition_revision?: string;
226
+ scorer_revision?: string;
227
+ runtime_revision?: string;
228
+ sampling_revision?: string;
229
+ report_schema_revision: 'pathgrade-results-v2';
230
+ unavailable_reasons?: ComparisonUnavailableReason[];
231
+ }
222
232
  /**
223
233
  * TrialResult with `session_log` and `conversation` stripped. These fields
224
234
  * live only in the per-group trace files; the consolidated results.json keeps
@@ -232,6 +242,8 @@ export type StrippedTrialResult = Omit<TrialResult, 'session_log' | 'conversatio
232
242
  export type PathgradeGroupReport = Omit<EvalReport, 'trials' | 'status'> & {
233
243
  status?: 'pass' | 'fail';
234
244
  trials: StrippedTrialResult[];
245
+ source_file?: string;
246
+ comparison_contract?: ComparisonContract;
235
247
  /** Relative path from `.pathgrade/` to the trace file for this group. */
236
248
  trace_file: string;
237
249
  };
@@ -277,6 +289,8 @@ export interface PathgradeReport {
277
289
  * in every group passed its vitest test.
278
290
  */
279
291
  status: 'pass' | 'fail';
292
+ /** Agent Evals compatibility manifest when changed selection finds no runnable evals. */
293
+ run_kind?: 'evaluation' | 'no-affected';
280
294
  groups: PathgradeGroupReport[];
281
295
  /**
282
296
  * Present when `pathgrade run --changed` produced the run. Absent on
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/pathgrade",
3
- "version": "1.0.30",
3
+ "version": "1.0.31",
4
4
  "packageManager": "yarn@4.12.0",
5
5
  "description": "Evaluate whether AI agents discover and use your skills correctly",
6
6
  "exports": {
@@ -140,5 +140,5 @@
140
140
  "typescript": "^5.9.3",
141
141
  "zod": "4.3.6"
142
142
  },
143
- "falconPackageHash": "c85431c64d766806f41ca2adad9d390e73dc8d09a2feea2ae9285a1c"
143
+ "falconPackageHash": "1f94e7e2c7f2f999b9ba92d6b76b362a5adf549800b96e08994d6c49"
144
144
  }