@wix/pathgrade 1.0.38 → 1.0.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +5 -5
  2. package/dist/adapters/jest/results.d.ts +1 -0
  3. package/dist/adapters/jest/results.js +36 -14
  4. package/dist/adapters/jest/runner-adapter.d.ts +1 -0
  5. package/dist/adapters/jest/runner-adapter.js +5 -1
  6. package/dist/adapters/node-test/runner-adapter.js +6 -5
  7. package/dist/affected/meta.d.ts +4 -1
  8. package/dist/affected/meta.js +9 -5
  9. package/dist/affected/select.js +1 -1
  10. package/dist/agents/claude/sdk-message-projector.js +4 -5
  11. package/dist/agents/claude/tool-permission-bridge.js +2 -1
  12. package/dist/agents/codex-app-server/item-projection.js +2 -2
  13. package/dist/agents/codex-app-server/mcp-approval-correlator.js +3 -2
  14. package/dist/agents/opencode.js +4 -5
  15. package/dist/commands/report.js +5 -26
  16. package/dist/internal/direct-mcp-v2/acp-author-projector.js +20 -7
  17. package/dist/reporters/cli.js +20 -1
  18. package/dist/reporters/github-comment.js +18 -7
  19. package/dist/reporters/loader.d.ts +4 -0
  20. package/dist/reporters/loader.js +26 -9
  21. package/dist/reporting/comparison-contract.d.ts +3 -2
  22. package/dist/reporting/comparison-contract.js +66 -33
  23. package/dist/reporting/core.js +35 -8
  24. package/dist/reporting/reliability-contract.d.ts +1 -0
  25. package/dist/reporting/report-parser.js +125 -5
  26. package/dist/reporting/source-metadata.d.ts +3 -0
  27. package/dist/reporting/source-metadata.js +67 -2
  28. package/dist/reporting/types.d.ts +5 -1
  29. package/dist/runners/model-builders.d.ts +1 -0
  30. package/dist/runners/model-builders.js +36 -12
  31. package/dist/runners/model.d.ts +2 -0
  32. package/dist/runners/orchestrator.js +3 -4
  33. package/dist/runners/repeated-invocation.js +2 -5
  34. package/dist/runners/report-projection.js +84 -0
  35. package/dist/runners/vitest-adapter.js +6 -2
  36. package/dist/sdk/agent.js +10 -1
  37. package/dist/sdk/evaluate.js +58 -3
  38. package/dist/sdk/judge-prompt-builder.js +11 -7
  39. package/dist/sdk/mcp-event-input.d.ts +1 -0
  40. package/dist/sdk/mcp-event-input.js +3 -0
  41. package/dist/sdk/mcp-evidence.js +16 -3
  42. package/dist/sdk/mcp-safety.js +2 -2
  43. package/dist/sdk/scorers.d.ts +5 -0
  44. package/dist/sdk/scorers.js +4 -0
  45. package/dist/sdk/scripted-mcp-events.js +3 -2
  46. package/dist/sdk/tool-event-log.js +18 -3
  47. package/dist/sdk/tool-event-secrets.d.ts +4 -0
  48. package/dist/sdk/tool-event-secrets.js +33 -2
  49. package/dist/sdk/types.d.ts +2 -0
  50. package/dist/tool-event-results.d.ts +3 -0
  51. package/dist/tool-event-results.js +153 -23
  52. package/dist/types.d.ts +14 -7
  53. package/package.json +2 -2
@@ -34,11 +34,18 @@ export async function loadReports(resultsDir, opts) {
34
34
  if (raw.version !== 1 && raw.version !== 2)
35
35
  continue;
36
36
  for (const group of raw.groups) {
37
+ const runnerStatus = groupRunnerStatus(group);
38
+ const thresholdStatus = groupThresholdStatus(raw, group);
37
39
  const report = {
38
40
  file,
39
41
  timestamp: raw.timestamp,
42
+ threshold: raw.threshold,
40
43
  ...group,
41
- status: groupStatus(raw, group),
44
+ runner_status: runnerStatus,
45
+ ...(raw.runner_status === 'pass' || raw.runner_status === 'fail'
46
+ ? { run_runner_status: raw.runner_status } : {}),
47
+ threshold_status: thresholdStatus,
48
+ status: runnerStatus === 'fail' || thresholdStatus === 'fail' ? 'fail' : 'pass',
42
49
  };
43
50
  if (!opts?.skipTraces && group.trace_file) {
44
51
  await hydrateTraces(report, group.trace_file, resolved);
@@ -50,15 +57,25 @@ export async function loadReports(resultsDir, opts) {
50
57
  }
51
58
  return results;
52
59
  }
53
- function groupStatus(raw, group) {
60
+ function groupRunnerStatus(group) {
61
+ if (group.runner_status === 'pass' || group.runner_status === 'fail')
62
+ return group.runner_status;
63
+ const outcomes = Array.isArray(group.trials)
64
+ ? group.trials.map((trial) => trial.runner_outcome).filter((outcome) => outcome !== undefined)
65
+ : [];
66
+ if (outcomes.length > 0) {
67
+ return outcomes.every((outcome) => outcome === 'passed') ? 'pass' : 'fail';
68
+ }
54
69
  if (group.status === 'pass' || group.status === 'fail')
55
70
  return group.status;
56
71
  const meanReward = group.mean_reward ?? group.pass_rate ?? 0;
57
- if (typeof raw.threshold === 'number')
58
- return meanReward >= raw.threshold ? 'pass' : 'fail';
59
- const outcomes = Array.isArray(group.trials) ? group.trials.map((trial) => trial.runner_outcome) : [];
60
- if (outcomes.length > 0 && outcomes.every((outcome) => (outcome === 'passed' || outcome === 'failed' || outcome === 'not-run'))) {
61
- return outcomes.every((outcome) => outcome === 'passed') ? 'pass' : 'fail';
62
- }
63
- return meanReward >= 0.5 ? 'pass' : 'fail';
72
+ return meanReward === 1 ? 'pass' : 'fail';
73
+ }
74
+ function groupThresholdStatus(raw, group) {
75
+ if (group.threshold_status === 'pass' || group.threshold_status === 'fail'
76
+ || group.threshold_status === 'not_configured')
77
+ return group.threshold_status;
78
+ if (typeof raw.threshold !== 'number')
79
+ return 'not_configured';
80
+ return (group.mean_reward ?? group.pass_rate ?? 0) >= raw.threshold ? 'pass' : 'fail';
64
81
  }
@@ -1,10 +1,11 @@
1
- import type { ComparisonContract, EvalReport } from '../types.js';
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
- }): ComparisonContract;
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 definitionRevision = input.group.sourceRevision
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 => 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');
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: 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() } : {}),
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
- name: agent.name,
104
+ agent_name: agent.name,
91
105
  model: agent.resolvedModel,
92
106
  transport: agent.transport,
93
- interaction: agent.interactionMode,
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 = { type: scorer.type, name: scorer.name.normalize('NFC'), weight: scorer.weight };
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
- return { ...common, function: scorer.fn.toString() };
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' ? scorer.input.toString() : scorer.input ?? null,
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,
@@ -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 = [];
@@ -52,20 +53,34 @@ export function buildPathgradeReport(input) {
52
53
  .map(attempt => attempt.score)
53
54
  .filter((score) => score !== undefined))));
54
55
  const overallMeanReward = average(scores);
55
- const status = input.threshold != null
56
- ? (overallMeanReward >= input.threshold ? 'pass' : 'fail')
57
- : (reportableGroups.every(group => group.cases.every(testCase => testCase.state === 'passed')) ? 'pass' : 'fail');
56
+ const runnerStatus = (input.runStatus === undefined || input.runStatus === 'completed')
57
+ && reportableGroups.every(group => group.cases.every(testCase => (testCase.state !== 'failed'
58
+ && testCase.attempts.every(attempt => attempt.outcome.kind === 'passed')))) ? 'pass' : 'fail';
59
+ const thresholdStatus = input.threshold == null
60
+ ? 'not_configured'
61
+ : overallMeanReward >= input.threshold ? 'pass' : 'fail';
58
62
  return {
59
63
  report: {
60
- version: 2,
64
+ version: 3,
61
65
  timestamp: new Date().toISOString(),
62
66
  ...(input.threshold != null ? { threshold: input.threshold } : {}),
63
67
  attempts_requested: attemptsRequested,
64
68
  attempts_completed: attemptsCompleted,
65
69
  overall_mean_reward: overallMeanReward,
66
70
  overall_pass_rate: overallMeanReward,
67
- status,
71
+ runner_status: runnerStatus,
72
+ threshold_status: thresholdStatus,
73
+ status: runnerStatus === 'fail' || thresholdStatus === 'fail' ? 'fail' : 'pass',
68
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
+ },
69
84
  ...(input.selection ? { selection: input.selection } : {}),
70
85
  },
71
86
  traces,
@@ -73,6 +88,13 @@ export function buildPathgradeReport(input) {
73
88
  warnings,
74
89
  };
75
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
+ }
76
98
  function reportableBuiltCase(testCase) {
77
99
  if (testCase.reportable !== undefined)
78
100
  return testCase.reportable;
@@ -185,15 +207,20 @@ function buildEvalReport(groupName, cases, run) {
185
207
  .filter((score) => score !== undefined);
186
208
  const meanReward = average(rewards);
187
209
  const metrics = binaryMetrics(cases, run);
210
+ const runnerStatus = cases.every(testCase => (testCase.state !== 'failed'
211
+ && testCase.attempts.every(attempt => attempt.outcome.kind === 'passed'))) ? 'pass' : 'fail';
212
+ const thresholdStatus = run.threshold == null
213
+ ? 'not_configured'
214
+ : meanReward >= run.threshold ? 'pass' : 'fail';
188
215
  const skills = new Set();
189
216
  for (const trial of trials)
190
217
  for (const skill of trial.skills_used ?? [])
191
218
  skills.add(skill);
192
219
  return {
193
220
  task: groupName,
194
- status: run.threshold != null
195
- ? (meanReward >= run.threshold ? 'pass' : 'fail')
196
- : (cases.every(testCase => testCase.state === 'passed') ? 'pass' : 'fail'),
221
+ runner_status: runnerStatus,
222
+ threshold_status: thresholdStatus,
223
+ status: runnerStatus === 'fail' || thresholdStatus === 'fail' ? 'fail' : 'pass',
197
224
  mean_reward: meanReward,
198
225
  ...(metrics.successRate !== undefined ? { success_rate: metrics.successRate } : {}),
199
226
  ...(metrics.passAtK ? {
@@ -53,6 +53,7 @@ export interface RuntimeComponentRevisions {
53
53
  model: string;
54
54
  transport: string;
55
55
  interaction_mode: string;
56
+ flow?: string;
56
57
  }
57
58
  type DefinitionRevisionContract = {
58
59
  comparison_inputs: Extract<ComparisonInputsState, {
@@ -1,30 +1,87 @@
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 === 2 && (typeof value.overall_mean_reward !== 'number' || !validAttemptCounts(value)))
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
+ || !optionalGateStatus(value.runner_status)
10
+ || !optionalThresholdStatus(value.threshold_status)
9
11
  || (value.run_kind !== undefined && value.run_kind !== 'evaluation' && value.run_kind !== 'no-affected')
10
12
  || !Array.isArray(value.groups)
11
13
  || !value.groups.every(group => isReportGroup(group, value.version))
14
+ || (value.version === 3 && !isTaskInventory(value.task_inventory))
12
15
  || !isSelection(value.selection)) {
13
16
  throw new Error('PathGrade results.json is missing or has an unsupported schema');
14
17
  }
15
- return value;
18
+ const threshold = value.threshold;
19
+ const groups = value.groups.map(group => {
20
+ const runnerStatus = deriveRunnerStatus(group);
21
+ const thresholdStatus = deriveThresholdStatus(group.mean_reward ?? group.pass_rate, threshold);
22
+ return {
23
+ ...group,
24
+ runner_status: runnerStatus,
25
+ threshold_status: thresholdStatus,
26
+ status: composeStatus(runnerStatus, thresholdStatus),
27
+ };
28
+ });
29
+ const legacyFailure = value.runner_status === undefined && value.status === 'fail' && groups.length === 0;
30
+ const runnerStatus = value.runner_status !== 'fail' && !legacyFailure
31
+ && (groups.length > 0 || value.runner_status === 'pass' || value.status === 'pass')
32
+ && groups.every(group => group.runner_status === 'pass') ? 'pass' : 'fail';
33
+ const thresholdStatus = deriveThresholdStatus(value.overall_mean_reward ?? value.overall_pass_rate, threshold);
34
+ return {
35
+ ...value,
36
+ groups,
37
+ runner_status: runnerStatus,
38
+ threshold_status: thresholdStatus,
39
+ status: composeStatus(runnerStatus, thresholdStatus),
40
+ };
16
41
  }
17
42
  function isReportGroup(value, version) {
18
43
  return isRecord(value)
19
44
  && typeof value.task === 'string'
20
45
  && (version === 1 ? legacyMetrics(value) : schemaV2Metrics(value))
46
+ && optionalGateStatus(value.runner_status)
47
+ && optionalThresholdStatus(value.threshold_status)
21
48
  && optionalString(value.source_file)
22
49
  && typeof value.trace_file === 'string'
23
50
  && Array.isArray(value.skills_used)
24
51
  && value.skills_used.every(item => typeof item === 'string')
25
52
  && Array.isArray(value.trials)
26
53
  && value.trials.every(isTrial)
27
- && (value.comparison_contract === undefined || isComparisonContract(value.comparison_contract));
54
+ && (version === 3
55
+ ? isComparisonContractV2(value.comparison_contract)
56
+ : value.comparison_contract === undefined || isComparisonContract(value.comparison_contract));
57
+ }
58
+ function deriveRunnerStatus(value) {
59
+ if (value.runner_status === 'fail')
60
+ return 'fail';
61
+ const outcomes = value.trials
62
+ .map(trial => trial.runner_outcome)
63
+ .filter(outcome => outcome !== undefined);
64
+ if (outcomes.length > 0)
65
+ return outcomes.every(outcome => outcome === 'passed') ? 'pass' : 'fail';
66
+ if (value.runner_status === 'pass')
67
+ return value.runner_status;
68
+ if (value.status === 'pass' || value.status === 'fail')
69
+ return value.status;
70
+ return value.pass_rate === 1 ? 'pass' : 'fail';
71
+ }
72
+ function deriveThresholdStatus(reward, threshold) {
73
+ if (threshold === undefined)
74
+ return 'not_configured';
75
+ return typeof reward === 'number' && reward >= threshold ? 'pass' : 'fail';
76
+ }
77
+ function composeStatus(runnerStatus, thresholdStatus) {
78
+ return runnerStatus === 'fail' || thresholdStatus === 'fail' ? 'fail' : 'pass';
79
+ }
80
+ function optionalGateStatus(value) {
81
+ return value === undefined || value === 'pass' || value === 'fail';
82
+ }
83
+ function optionalThresholdStatus(value) {
84
+ return optionalGateStatus(value) || value === 'not_configured';
28
85
  }
29
86
  function isTrial(value) {
30
87
  return isRecord(value)
@@ -40,7 +97,8 @@ function isTrial(value) {
40
97
  && value.scorer_results.every(scorer => isRecord(scorer)
41
98
  && typeof scorer.scorer_type === 'string'
42
99
  && typeof scorer.score === 'number'
43
- && typeof scorer.weight === 'number');
100
+ && typeof scorer.weight === 'number'
101
+ && (scorer.status === undefined || scorer.status === 'ok' || scorer.status === 'error' || scorer.status === 'skipped'));
44
102
  }
45
103
  function legacyMetrics(value) {
46
104
  return typeof value.pass_rate === 'number'
@@ -82,6 +140,9 @@ function isSelection(value) {
82
140
  && item.reason === 'no-matching-deps'));
83
141
  }
84
142
  function isComparisonContract(value) {
143
+ return isComparisonContractV1(value) || isComparisonContractV2(value);
144
+ }
145
+ function isComparisonContractV1(value) {
85
146
  return isRecord(value)
86
147
  && value.version === 1
87
148
  && optionalString(value.definition_revision)
@@ -95,6 +156,65 @@ function isComparisonContract(value) {
95
156
  || reason === 'runtime_metadata_missing'
96
157
  || reason === 'sampling_incomplete')));
97
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
+ }
98
218
  function optionalString(value) {
99
219
  return value === undefined || typeof value === 'string';
100
220
  }
@@ -1,5 +1,8 @@
1
+ import type { ComparisonInputsState } from './reliability-contract.js';
1
2
  export interface SourceMetadata {
2
3
  sourceFile?: string;
3
4
  sourceRevision?: string;
5
+ comparisonInputs?: ComparisonInputsState;
4
6
  }
5
7
  export declare function resolveSourceMetadata(filePath: string | undefined, cwd: string): SourceMetadata;
8
+ export declare function createSourceMetadataResolver(cwd: string): (filePath: string | undefined) => SourceMetadata;
@@ -1,7 +1,18 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { readFileSync } from 'node:fs';
3
+ import { readdirSync } from 'node:fs';
3
4
  import * as path from 'node:path';
5
+ import picomatch from 'picomatch';
6
+ import { canonicalizeJson } from '../core/canonical-json.js';
7
+ import { parsePathgradeMeta } from '../affected/meta.js';
4
8
  export function resolveSourceMetadata(filePath, cwd) {
9
+ return resolveSourceMetadataWithFiles(filePath, cwd, () => listRepositoryFiles(cwd));
10
+ }
11
+ export function createSourceMetadataResolver(cwd) {
12
+ let repositoryFiles;
13
+ return filePath => resolveSourceMetadataWithFiles(filePath, cwd, () => repositoryFiles ??= listRepositoryFiles(cwd));
14
+ }
15
+ function resolveSourceMetadataWithFiles(filePath, cwd, repositoryFiles) {
5
16
  if (!filePath)
6
17
  return {};
7
18
  const absolute = path.resolve(cwd, filePath);
@@ -11,10 +22,64 @@ export function resolveSourceMetadata(filePath, cwd) {
11
22
  try {
12
23
  return {
13
24
  sourceFile: relative,
14
- sourceRevision: `sha256:${createHash('sha256').update(readFileSync(absolute)).digest('hex')}`,
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 { sourceFile: relative };
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
+ }
@@ -1,19 +1,23 @@
1
1
  import type { DiagnosticsReport } from '../sdk/diagnostics.js';
2
2
  import type { AgentExecutionMetadata, EvaluationResultKind } from '../sdk/types.js';
3
- import type { AttemptOutcome } from '../runners/model.js';
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 {
8
+ runStatus?: RunStatus;
7
9
  threshold?: number;
8
10
  selection?: PathgradeSelectionReport;
9
11
  attemptsRequested?: number;
10
12
  attemptsCompleted?: number;
13
+ taskInventory?: TaskInventory;
11
14
  groups: ReportGroupInput[];
12
15
  }
13
16
  export interface ReportGroupInput {
14
17
  groupName: string;
15
18
  sourceFile?: string;
16
19
  sourceRevision?: string;
20
+ comparisonInputs?: ComparisonInputsState;
17
21
  cases: ReportCaseInput[];
18
22
  }
19
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;