@wix/pathgrade 0.37.0 → 1.0.0

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 (116) hide show
  1. package/README.md +116 -32
  2. package/dist/adapter-kit/index.d.ts +17 -0
  3. package/dist/adapter-kit/index.js +14 -0
  4. package/dist/adapters/jest/index.d.ts +3 -0
  5. package/dist/adapters/jest/index.js +3 -0
  6. package/dist/adapters/jest/invocation-adapter.d.ts +15 -0
  7. package/dist/adapters/jest/invocation-adapter.js +115 -0
  8. package/dist/adapters/jest/lifecycle.d.ts +21 -0
  9. package/dist/adapters/jest/lifecycle.js +62 -0
  10. package/dist/adapters/jest/metadata.d.ts +6 -0
  11. package/dist/adapters/jest/metadata.js +35 -0
  12. package/dist/adapters/jest/reporter.cjs +10 -0
  13. package/dist/adapters/jest/reporter.d.ts +5 -0
  14. package/dist/adapters/jest/reporter.js +67 -0
  15. package/dist/adapters/jest/results.d.ts +31 -0
  16. package/dist/adapters/jest/results.js +144 -0
  17. package/dist/adapters/jest/runner-adapter.d.ts +9 -0
  18. package/dist/adapters/jest/runner-adapter.js +49 -0
  19. package/dist/adapters/jest/setup.d.ts +1 -0
  20. package/dist/adapters/jest/setup.js +10 -0
  21. package/dist/adapters/node-test/index.d.ts +8 -0
  22. package/dist/adapters/node-test/index.js +106 -0
  23. package/dist/adapters/node-test/invocation-adapter.d.ts +5 -0
  24. package/dist/adapters/node-test/invocation-adapter.js +47 -0
  25. package/dist/adapters/node-test/runner-adapter.d.ts +2 -0
  26. package/dist/adapters/node-test/runner-adapter.js +116 -0
  27. package/dist/adapters/vitest/index.d.ts +9 -0
  28. package/dist/adapters/vitest/index.js +82 -0
  29. package/dist/adapters/vitest/lifecycle.d.ts +24 -0
  30. package/dist/adapters/vitest/lifecycle.js +41 -0
  31. package/dist/adapters/vitest/reporter.d.ts +12 -0
  32. package/dist/adapters/vitest/reporter.js +75 -0
  33. package/dist/adapters/vitest/setup.d.ts +1 -0
  34. package/dist/adapters/vitest/setup.js +3 -0
  35. package/dist/agents/codex-app-server/agent.js +5 -1
  36. package/dist/agents/codex-app-server/protocol/index.js +3 -3
  37. package/dist/agents/codex-app-server/turn-completion.d.ts +1 -0
  38. package/dist/agents/codex-app-server/turn-completion.js +9 -0
  39. package/dist/commands/affected.js +22 -15
  40. package/dist/commands/run-args.d.ts +4 -3
  41. package/dist/commands/run-args.js +12 -6
  42. package/dist/commands/run-changed.d.ts +4 -4
  43. package/dist/commands/run-changed.js +45 -58
  44. package/dist/config/pathgrade.d.ts +48 -0
  45. package/dist/config/pathgrade.js +204 -0
  46. package/dist/pathgrade.d.ts +2 -2
  47. package/dist/pathgrade.js +35 -34
  48. package/dist/plugin/index.d.ts +2 -9
  49. package/dist/plugin/index.js +1 -81
  50. package/dist/plugin/lifecycle.d.ts +1 -45
  51. package/dist/plugin/lifecycle.js +1 -146
  52. package/dist/plugin/reporter.d.ts +1 -37
  53. package/dist/plugin/reporter.js +1 -280
  54. package/dist/plugin/setup.d.ts +1 -1
  55. package/dist/plugin/setup.js +1 -3
  56. package/dist/providers/copy-filter.d.ts +5 -0
  57. package/dist/providers/copy-filter.js +9 -0
  58. package/dist/providers/sandbox.js +2 -2
  59. package/dist/providers/workspace.d.ts +6 -0
  60. package/dist/providers/workspace.js +34 -14
  61. package/dist/reporters/diagnostics.d.ts +2 -40
  62. package/dist/reporters/diagnostics.js +1 -80
  63. package/dist/reporters/report-summary.d.ts +6 -0
  64. package/dist/reporters/report-summary.js +29 -0
  65. package/dist/reporting/artifacts.d.ts +2 -0
  66. package/dist/reporting/artifacts.js +20 -0
  67. package/dist/reporting/core.d.ts +2 -0
  68. package/dist/reporting/core.js +173 -0
  69. package/dist/reporting/types.d.ts +57 -0
  70. package/dist/reporting/types.js +1 -0
  71. package/dist/reporting/vitest-edge.d.ts +1 -0
  72. package/dist/reporting/vitest-edge.js +1 -0
  73. package/dist/runners/adapter-loader.d.ts +17 -0
  74. package/dist/runners/adapter-loader.js +67 -0
  75. package/dist/runners/adapter.d.ts +65 -0
  76. package/dist/runners/adapter.js +1 -0
  77. package/dist/runners/invocation.d.ts +10 -0
  78. package/dist/runners/invocation.js +1 -0
  79. package/dist/runners/lifecycle-hooks.d.ts +2 -0
  80. package/dist/runners/lifecycle-hooks.js +18 -0
  81. package/dist/runners/model-builders.d.ts +4 -0
  82. package/dist/runners/model-builders.js +67 -0
  83. package/dist/runners/model-validation.d.ts +12 -0
  84. package/dist/runners/model-validation.js +213 -0
  85. package/dist/runners/model.d.ts +117 -0
  86. package/dist/runners/model.js +1 -0
  87. package/dist/runners/orchestrator.d.ts +30 -0
  88. package/dist/runners/orchestrator.js +74 -0
  89. package/dist/runners/report-projection.d.ts +6 -0
  90. package/dist/runners/report-projection.js +62 -0
  91. package/dist/runners/selection.d.ts +6 -0
  92. package/dist/runners/selection.js +10 -0
  93. package/dist/runners/vitest-adapter.d.ts +8 -0
  94. package/dist/runners/vitest-adapter.js +108 -0
  95. package/dist/runners/vitest-invocation.d.ts +10 -0
  96. package/dist/runners/vitest-invocation.js +44 -0
  97. package/dist/runners/vitest-lifecycle.d.ts +43 -0
  98. package/dist/runners/vitest-lifecycle.js +150 -0
  99. package/dist/sdk/agent.js +15 -16
  100. package/dist/sdk/case-context.d.ts +20 -0
  101. package/dist/sdk/case-context.js +31 -0
  102. package/dist/sdk/diagnostics.d.ts +40 -0
  103. package/dist/sdk/diagnostics.js +80 -0
  104. package/dist/sdk/eval-runtime.d.ts +4 -0
  105. package/dist/sdk/eval-runtime.js +11 -1
  106. package/dist/sdk/evaluate.js +2 -1
  107. package/dist/sdk/index.d.ts +2 -0
  108. package/dist/sdk/index.js +1 -0
  109. package/dist/sdk/lifecycle.d.ts +40 -0
  110. package/dist/sdk/lifecycle.js +170 -0
  111. package/dist/sdk/result-capture.d.ts +18 -0
  112. package/dist/sdk/result-capture.js +49 -0
  113. package/dist/sdk/types.d.ts +1 -1
  114. package/dist/types.d.ts +2 -2
  115. package/dist/utils/llm-providers/anthropic.js +9 -2
  116. package/package.json +56 -4
@@ -0,0 +1,82 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { PathgradeReporter } from './reporter.js';
4
+ import { discoverPathgradeEvalFiles } from '../../evals/discovery.js';
5
+ import { DEFAULT_EVAL_EXCLUDE, DEFAULT_EVAL_INCLUDE } from '../../config/pathgrade.js';
6
+ function resolveSetupFile() {
7
+ // Prefer .ts (running from source via vitest) over .js (built dist)
8
+ const tsPath = path.resolve(import.meta.dirname, 'setup.ts');
9
+ if (fs.existsSync(tsPath))
10
+ return tsPath;
11
+ return path.resolve(import.meta.dirname, 'setup.js');
12
+ }
13
+ /**
14
+ * Vitest plugin for pathgrade evaluations.
15
+ *
16
+ * Configures test file patterns, timeout, auto-dispose lifecycle hooks,
17
+ * and the pathgrade reporter for aggregate statistics.
18
+ */
19
+ export function pathgrade(opts) {
20
+ const timeoutSec = opts?.timeout ?? 300;
21
+ // Verbose live-streaming seam: the SDK (createAgent) reads
22
+ // `process.env.PATHGRADE_VERBOSE` to decide whether to emit. Wiring the
23
+ // plugin option through the env var keeps a single source of truth and
24
+ // preserves CLI/env precedence — we only set the env if the user hasn't
25
+ // already set it via their shell or the `pathgrade run --verbose` CLI.
26
+ if (opts?.verbose === true && process.env.PATHGRADE_VERBOSE === undefined) {
27
+ process.env.PATHGRADE_VERBOSE = '1';
28
+ }
29
+ return {
30
+ name: 'pathgrade',
31
+ // Surface the user's options to pre-vitest CLI commands
32
+ // (`pathgrade affected`, `pathgrade run --changed`) so they can
33
+ // read `affected.global` after loading vitest.config.ts.
34
+ // See `src/affected/config.ts`.
35
+ __pathgradeOptions: opts ?? {},
36
+ config() {
37
+ return {
38
+ resolve: {
39
+ alias: resolveLocalAliases(),
40
+ },
41
+ test: {
42
+ include: opts?.include ?? DEFAULT_EVAL_INCLUDE,
43
+ exclude: opts?.exclude ?? DEFAULT_EVAL_EXCLUDE,
44
+ testTimeout: (timeoutSec + 30) * 1000,
45
+ setupFiles: [resolveSetupFile()],
46
+ reporters: [
47
+ 'default',
48
+ new PathgradeReporter(opts),
49
+ ],
50
+ },
51
+ };
52
+ },
53
+ configResolved(resolved) {
54
+ const root = resolved.root ?? process.cwd();
55
+ const include = resolved.test?.include ?? opts?.include ?? DEFAULT_EVAL_INCLUDE;
56
+ const exclude = resolved.test?.exclude ?? opts?.exclude ?? DEFAULT_EVAL_EXCLUDE;
57
+ const discovered = discoverPathgradeEvalFiles({ cwd: root, include, exclude });
58
+ if (!resolved.test) {
59
+ resolved.test = {};
60
+ }
61
+ resolved.test.include = discovered;
62
+ },
63
+ };
64
+ }
65
+ /**
66
+ * When running from source (not installed via npm), resolve
67
+ * `@wix/pathgrade` imports to the local src/ directory so examples
68
+ * can use canonical package imports while developing locally.
69
+ */
70
+ function resolveLocalAliases() {
71
+ const sdkSource = path.resolve(import.meta.dirname, '..', '..', 'sdk', 'index.ts');
72
+ if (!fs.existsSync(sdkSource))
73
+ return undefined;
74
+ const src = path.resolve(import.meta.dirname, '..', '..');
75
+ return {
76
+ '@wix/pathgrade/mcp-mock': path.join(src, 'core', 'mcp-mock.ts'),
77
+ '@wix/pathgrade/plugin': path.join(src, 'plugin', 'index.ts'),
78
+ '@wix/pathgrade/plugin/vitest': path.join(src, 'adapters', 'vitest', 'index.ts'),
79
+ '@wix/pathgrade/adapters/vitest': path.join(src, 'adapters', 'vitest', 'index.ts'),
80
+ '@wix/pathgrade': path.join(src, 'sdk', 'index.ts'),
81
+ };
82
+ }
@@ -0,0 +1,24 @@
1
+ import type { Agent, RecordedEvalResult } from '../../sdk/types.js';
2
+ import { type VitestAfterAllFn, type VitestAfterEachFn, type VitestAroundEachFn } from '../../runners/vitest-lifecycle.js';
3
+ declare function trackAgent(agent: Agent): void;
4
+ declare function untrackAgent(agent: Agent): void;
5
+ declare function releaseAgent(agent: Agent): void;
6
+ declare function flush(task: {
7
+ id: string;
8
+ meta: Record<string, unknown>;
9
+ }): Promise<void>;
10
+ declare function onResult(result: RecordedEvalResult, agent: Agent): void;
11
+ declare function flushAll(): Promise<void>;
12
+ declare function reset(): void;
13
+ declare function install(afterEach: VitestAfterEachFn, afterAll?: VitestAfterAllFn, aroundEach?: VitestAroundEachFn): void;
14
+ export declare const lifecycle: {
15
+ trackAgent: typeof trackAgent;
16
+ untrackAgent: typeof untrackAgent;
17
+ releaseAgent: typeof releaseAgent;
18
+ flush: typeof flush;
19
+ flushAll: typeof flushAll;
20
+ onResult: typeof onResult;
21
+ reset: typeof reset;
22
+ install: typeof install;
23
+ };
24
+ export {};
@@ -0,0 +1,41 @@
1
+ import { createVitestLifecycleHooks, installVitestLifecycle, releaseVitestAgent, resetVitestLifecycle, trackVitestAgent, untrackVitestAgent, } from '../../runners/vitest-lifecycle.js';
2
+ let installHandle = null;
3
+ function trackAgent(agent) {
4
+ trackVitestAgent(agent, installHandle !== null);
5
+ }
6
+ function untrackAgent(agent) {
7
+ untrackVitestAgent(agent);
8
+ }
9
+ function releaseAgent(agent) {
10
+ releaseVitestAgent(agent);
11
+ }
12
+ async function flush(task) {
13
+ const results = await createVitestLifecycleHooks().flushCase(task.id);
14
+ if (results.length > 0) {
15
+ task.meta.pathgrade = results;
16
+ }
17
+ }
18
+ function onResult(result, agent) {
19
+ createVitestLifecycleHooks().onResult({ result, agent });
20
+ }
21
+ async function flushAll() {
22
+ await createVitestLifecycleHooks().cleanupRun();
23
+ }
24
+ function reset() {
25
+ resetVitestLifecycle(installHandle);
26
+ installHandle = null;
27
+ }
28
+ function install(afterEach, afterAll, aroundEach) {
29
+ installHandle?.restore();
30
+ installHandle = installVitestLifecycle({ afterEach, afterAll, aroundEach });
31
+ }
32
+ export const lifecycle = {
33
+ trackAgent,
34
+ untrackAgent,
35
+ releaseAgent,
36
+ flush,
37
+ flushAll,
38
+ onResult,
39
+ reset,
40
+ install,
41
+ };
@@ -0,0 +1,12 @@
1
+ import type { Reporter, TestModule } from 'vitest/node';
2
+ import type { PathgradePluginOptions } from '../../sdk/types.js';
3
+ /**
4
+ * Custom vitest reporter that layers pathgrade aggregate statistics
5
+ * on top of vitest's default output.
6
+ */
7
+ export declare class PathgradeReporter implements Reporter {
8
+ private opts;
9
+ constructor(opts?: PathgradePluginOptions);
10
+ onTestRunEnd(testModules: ReadonlyArray<TestModule>): Promise<void>;
11
+ private openBrowserViewer;
12
+ }
@@ -0,0 +1,75 @@
1
+ import * as path from 'path';
2
+ import { execSync } from 'child_process';
3
+ import { fmt } from '../../utils/cli.js';
4
+ import { getPathgradeDir } from '../../reporters/results-path.js';
5
+ import { readSidecar } from '../../affected/sidecar.js';
6
+ import { printReportSummary } from '../../reporters/report-summary.js';
7
+ import { createVitestAdapter } from '../../runners/vitest-adapter.js';
8
+ import { runWithAdapter } from '../../runners/orchestrator.js';
9
+ import { resolvePathgradeConfig } from '../../config/pathgrade.js';
10
+ /**
11
+ * Custom vitest reporter that layers pathgrade aggregate statistics
12
+ * on top of vitest's default output.
13
+ */
14
+ export class PathgradeReporter {
15
+ opts;
16
+ constructor(opts) {
17
+ this.opts = opts ?? {};
18
+ }
19
+ async onTestRunEnd(testModules) {
20
+ const cwd = process.cwd();
21
+ const config = await resolvePathgradeConfig({ cwd });
22
+ const mode = this.opts.reporter ?? config.reporter ?? 'cli';
23
+ const outputDir = getPathgradeDir(cwd);
24
+ const adapter = createVitestAdapter({ testModules });
25
+ const exitCode = await runWithAdapter({
26
+ adapter,
27
+ options: {
28
+ cwd,
29
+ discovery: { cwd },
30
+ runnerArgs: [],
31
+ env: process.env,
32
+ artifactRoot: outputDir,
33
+ reporterMode: mode,
34
+ threshold: this.opts.ci?.threshold ?? config.ci.threshold,
35
+ writeEmptyReport: false,
36
+ warn: warning => console.warn(warning),
37
+ log: () => console.log(`\n ${fmt.dim('Results written to')} ${outputDir}\n`),
38
+ loadSelection: async () => (await readSidecar(cwd, msg => {
39
+ console.warn(`[pathgrade] ${msg}`);
40
+ })) ?? undefined,
41
+ printSummary: summaries => {
42
+ printReportSummary(summaries, {
43
+ forceVerbose: this.opts.diagnostics === true
44
+ || config.diagnostics
45
+ || process.env.PATHGRADE_DIAGNOSTICS === '1',
46
+ currentTimeoutMs: this.opts.timeout != null ? this.opts.timeout * 1000 : undefined,
47
+ });
48
+ },
49
+ openBrowser: () => this.openBrowserViewer(),
50
+ onThresholdFailure: ({ overallPassRate, threshold }) => {
51
+ const avg = overallPassRate;
52
+ const configuredThreshold = this.opts.ci?.threshold ?? threshold;
53
+ process.exitCode = 1;
54
+ console.log(`\n ${fmt.fail('CI THRESHOLD FAILED')} avg score ${fmt.bold(avg.toFixed(3))} < threshold ${fmt.bold(String(configuredThreshold))}\n`);
55
+ },
56
+ },
57
+ });
58
+ if (exitCode !== 0 && (process.exitCode === undefined || process.exitCode === 0)) {
59
+ process.exitCode = exitCode;
60
+ }
61
+ }
62
+ openBrowserViewer() {
63
+ const viewerPath = path.resolve(import.meta.dirname, '..', '..', 'viewer.html');
64
+ try {
65
+ const openCmd = process.platform === 'darwin' ? 'open'
66
+ : process.platform === 'win32' ? 'start'
67
+ : 'xdg-open';
68
+ execSync(`${openCmd} "${viewerPath}"`, { stdio: 'ignore' });
69
+ console.log(`\n ${fmt.dim('Opened viewer in browser')}\n`);
70
+ }
71
+ catch {
72
+ console.log(`\n ${fmt.dim('Open manually:')} ${viewerPath}\n`);
73
+ }
74
+ }
75
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ import { afterAll, afterEach, aroundEach } from 'vitest';
2
+ import { lifecycle } from './lifecycle.js';
3
+ lifecycle.install(afterEach, afterAll, aroundEach);
@@ -2,11 +2,12 @@ import { BaseAgent, getRuntimeEnv, getWorkspacePath, } from '../../types.js';
2
2
  import { mountMcpForCodexAppServer } from '../../providers/mcp-runtime-mounting.js';
3
3
  import { assertMcpSecretReferencesReady } from '../../providers/mcp-config.js';
4
4
  import { buildSummary, enrichSkillEvents, extractSkillNameFromPath, inferCodexExecAction, } from '../../tool-events.js';
5
- import { requireAskBusForLiveBatches, } from '../../sdk/ask-bus/bus.js';
5
+ import { requireAskBusForLiveBatches } from '../../sdk/ask-bus/bus.js';
6
6
  import { toAskUserToolEvent } from '../../sdk/ask-bus/projection.js';
7
7
  import { decideMcpToolCall, redactMcpSecrets, } from '../../sdk/mcp-safety.js';
8
8
  import { spawnAppServerTransport, } from './transport.js';
9
9
  import { normalizeUpstreamQuestion, toWireAnswerMap, } from './wire-translators.js';
10
+ import { extractTurnCompletionFailure } from './turn-completion.js';
10
11
  const DEFAULT_MODEL = 'gpt-5.4';
11
12
  const TURN_COMPLETED_METHOD = 'turn/completed';
12
13
  function recordFromUnknown(value) {
@@ -308,6 +309,9 @@ export class CodexAppServerAgent extends BaseAgent {
308
309
  settled = true;
309
310
  off();
310
311
  closeOff();
312
+ const failure = extractTurnCompletionFailure(n.params);
313
+ if (failure)
314
+ Object.assign(turn, { turnFailed: true, failureMessage: failure });
311
315
  resolve();
312
316
  }
313
317
  });
@@ -1,8 +1,8 @@
1
1
  // Curated re-exports for the pathgrade Codex app-server driver and fixture
2
- // tests. Every re-exported symbol is either vendored verbatim from
2
+ // tests. Every re-exported symbol is either a curated upstream shape from
3
3
  // openai/codex@rust-v0.124.0 (with an upstream-citation header in its file) or
4
- // a pathgrade-local composition (ClientRequest, ServerRequest, Op) whose
5
- // header spells out the composition rationale.
4
+ // a pathgrade-local composition (ClientRequest, ServerRequest, Op) whose header
5
+ // spells out the composition rationale.
6
6
  //
7
7
  // Refresh policy: to bump the vendored version, update each file's header
8
8
  // citation to the new tag, replace the `unknown` aliases with their vendored
@@ -0,0 +1 @@
1
+ export declare function extractTurnCompletionFailure(params: unknown): string | undefined;
@@ -0,0 +1,9 @@
1
+ export function extractTurnCompletionFailure(params) {
2
+ const turn = params?.turn;
3
+ const message = turn?.error?.message;
4
+ const details = turn?.error?.additionalDetails;
5
+ if (typeof message === 'string' && message.length > 0) {
6
+ return typeof details === 'string' && details.length > 0 ? `${message} ${details}` : message;
7
+ }
8
+ return turn?.status === 'failed' ? 'turn failed' : undefined;
9
+ }
@@ -12,19 +12,11 @@
12
12
  * and `--explain` / `--json` presentation.
13
13
  */
14
14
  import * as fs from 'fs';
15
- import { configDefaults } from 'vitest/config';
16
15
  import { selectAffected } from '../affected/select.js';
17
16
  import { resolveBaseRef, computeChangedFiles } from '../affected/git.js';
18
- import { loadAffectedConfig } from '../affected/config.js';
19
17
  import { formatExplain, formatJson } from '../affected/format.js';
20
18
  import { discoverPathgradeEvalFiles } from '../evals/discovery.js';
21
- const DEFAULT_EXCLUDE = [
22
- ...configDefaults.exclude,
23
- '.worktrees/**',
24
- 'worktrees/**',
25
- '**/node_modules/**',
26
- '**/fixtures/**',
27
- ];
19
+ import { DEFAULT_EVAL_EXCLUDE, DEFAULT_EVAL_INCLUDE, resolvePathgradeConfig, } from '../config/pathgrade.js';
28
20
  export async function runAffected(opts) {
29
21
  const { cwd } = opts;
30
22
  let changes;
@@ -44,9 +36,21 @@ export async function runAffected(opts) {
44
36
  if (changes.baseRefLine) {
45
37
  process.stderr.write(`${changes.baseRefLine}\n`);
46
38
  }
47
- const evalFiles = discoverEvalFiles(cwd);
48
- const config = await loadAffectedConfig(cwd, {
49
- onWarning: w => process.stderr.write(`${w}\n`),
39
+ let config;
40
+ try {
41
+ config = await resolvePathgradeConfig({
42
+ cwd,
43
+ warn: w => process.stderr.write(`${w}\n`),
44
+ });
45
+ }
46
+ catch (err) {
47
+ process.stderr.write(`pathgrade affected: ${errMsg(err)}\n`);
48
+ return 1;
49
+ }
50
+ const evalFiles = discoverPathgradeEvalFiles({
51
+ cwd,
52
+ include: config.evals.include,
53
+ exclude: config.evals.exclude,
50
54
  });
51
55
  let result;
52
56
  try {
@@ -55,7 +59,7 @@ export async function runAffected(opts) {
55
59
  changedFiles: changes.changedFiles,
56
60
  repoRoot: cwd,
57
61
  baseRef: changes.baseRef,
58
- global: config.global,
62
+ global: config.affected.global,
59
63
  });
60
64
  }
61
65
  catch (err) {
@@ -132,6 +136,9 @@ function readChangedFilesList(filePath) {
132
136
  .map(l => l.trim())
133
137
  .filter(l => l.length > 0);
134
138
  }
139
+ function errMsg(err) {
140
+ return err instanceof Error ? err.message : String(err);
141
+ }
135
142
  /**
136
143
  * Discover repo-relative `*.eval.ts` files under `cwd`, honoring the same
137
144
  * exclude defaults the vitest plugin uses.
@@ -139,8 +146,8 @@ function readChangedFilesList(filePath) {
139
146
  export function discoverEvalFiles(cwd) {
140
147
  return discoverPathgradeEvalFiles({
141
148
  cwd,
142
- include: ['**/*.eval.ts'],
143
- exclude: DEFAULT_EXCLUDE,
149
+ include: DEFAULT_EVAL_INCLUDE,
150
+ exclude: DEFAULT_EVAL_EXCLUDE,
144
151
  });
145
152
  }
146
153
  function printSelectedToStdout(result) {
@@ -2,11 +2,12 @@
2
2
  * Parse `pathgrade run` arguments.
3
3
  *
4
4
  * Recognized pathgrade-specific flags are consumed here; everything else
5
- * passes through to vitest. After a literal `--` separator, all remaining
6
- * args are forwarded verbatim (vitest uses `--` the same way).
5
+ * passes through to the selected runner. After a literal `--` separator,
6
+ * all remaining args are forwarded verbatim.
7
7
  */
8
8
  export interface PathgradeRunArgs {
9
- vitestArgs: string[];
9
+ runnerArgs: string[];
10
+ adapterName?: string;
10
11
  forceDiagnostics: boolean;
11
12
  forceVerbose: boolean;
12
13
  changed: boolean;
@@ -2,21 +2,22 @@
2
2
  * Parse `pathgrade run` arguments.
3
3
  *
4
4
  * Recognized pathgrade-specific flags are consumed here; everything else
5
- * passes through to vitest. After a literal `--` separator, all remaining
6
- * args are forwarded verbatim (vitest uses `--` the same way).
5
+ * passes through to the selected runner. After a literal `--` separator,
6
+ * all remaining args are forwarded verbatim.
7
7
  */
8
8
  export function parsePathgradeRunArgs(args) {
9
- const vitestArgs = [];
9
+ const runnerArgs = [];
10
10
  let forceDiagnostics = false;
11
11
  let forceVerbose = false;
12
12
  let changed = false;
13
13
  let quiet = false;
14
14
  let since;
15
15
  let changedFilesPath;
16
+ let adapterName;
16
17
  let passthrough = false;
17
18
  for (const arg of args) {
18
19
  if (passthrough) {
19
- vitestArgs.push(arg);
20
+ runnerArgs.push(arg);
20
21
  continue;
21
22
  }
22
23
  if (arg === '--') {
@@ -39,6 +40,10 @@ export function parsePathgradeRunArgs(args) {
39
40
  quiet = true;
40
41
  continue;
41
42
  }
43
+ if (arg.startsWith('--adapter=')) {
44
+ adapterName = arg.slice('--adapter='.length);
45
+ continue;
46
+ }
42
47
  if (arg.startsWith('--since=')) {
43
48
  since = arg.slice('--since='.length);
44
49
  continue;
@@ -47,7 +52,7 @@ export function parsePathgradeRunArgs(args) {
47
52
  changedFilesPath = arg.slice('--changed-files='.length);
48
53
  continue;
49
54
  }
50
- vitestArgs.push(arg);
55
+ runnerArgs.push(arg);
51
56
  }
52
57
  const warnings = [];
53
58
  if (!changed && since !== undefined) {
@@ -57,11 +62,12 @@ export function parsePathgradeRunArgs(args) {
57
62
  warnings.push('--changed-files has no effect without --changed; the flag is being ignored.');
58
63
  }
59
64
  const base = {
60
- vitestArgs,
65
+ runnerArgs,
61
66
  forceDiagnostics,
62
67
  forceVerbose,
63
68
  changed,
64
69
  quiet,
70
+ adapterName,
65
71
  since,
66
72
  changedFilesPath,
67
73
  };
@@ -12,15 +12,15 @@
12
12
  * Git-resolution failures exit non-zero. The selection pipeline is the
13
13
  * only producer of the file list here.
14
14
  */
15
+ import type { RunnerInvocationAdapter } from '../runners/invocation.js';
16
+ import type { SpawnVitest } from '../runners/vitest-invocation.js';
15
17
  import type { PathgradeRunArgs } from './run-args.js';
16
- export interface SpawnVitestRequest {
17
- argv: string[];
18
- }
19
- export type SpawnVitest = (req: SpawnVitestRequest) => Promise<number> | number;
18
+ export type { SpawnVitest };
20
19
  export interface RunChangedOptions {
21
20
  cwd: string;
22
21
  parsed: PathgradeRunArgs;
23
22
  /** Override for tests; default spawns `npx vitest` inheriting stdio. */
24
23
  spawnVitest?: SpawnVitest;
24
+ runnerInvocation?: RunnerInvocationAdapter;
25
25
  }
26
26
  export declare function runChanged(opts: RunChangedOptions): Promise<number>;
@@ -13,15 +13,42 @@
13
13
  * only producer of the file list here.
14
14
  */
15
15
  import * as fs from 'fs';
16
- import picomatch from 'picomatch';
17
16
  import { selectAffected } from '../affected/select.js';
18
17
  import { resolveBaseRef, computeChangedFiles } from '../affected/git.js';
19
- import { loadAffectedConfig } from '../affected/config.js';
20
18
  import { writeSidecar } from '../affected/sidecar.js';
21
- import { discoverEvalFiles } from './affected.js';
19
+ import { discoverPathgradeEvalFiles } from '../evals/discovery.js';
20
+ import { resolvePathgradeConfig } from '../config/pathgrade.js';
21
+ import { loadRunnerInvocationAdapter } from '../runners/adapter-loader.js';
22
22
  export async function runChanged(opts) {
23
23
  const { cwd, parsed } = opts;
24
- const spawnVitest = opts.spawnVitest ?? defaultSpawnVitest;
24
+ const runnerEnv = {
25
+ ...process.env,
26
+ ...(parsed.forceDiagnostics ? { PATHGRADE_DIAGNOSTICS: '1' } : {}),
27
+ ...(parsed.forceVerbose ? { PATHGRADE_VERBOSE: '1' } : {}),
28
+ };
29
+ const configPath = findVitestConfigArg(parsed.runnerArgs);
30
+ let config;
31
+ let runnerInvocation;
32
+ try {
33
+ config = await resolvePathgradeConfig({
34
+ cwd,
35
+ legacyVitestConfigPath: configPath,
36
+ warn: w => {
37
+ if (!parsed.quiet)
38
+ process.stderr.write(`${w}\n`);
39
+ },
40
+ });
41
+ runnerInvocation = opts.runnerInvocation ?? await loadRunnerInvocationAdapter({
42
+ adapterName: parsed.adapterName ?? config.runner.adapter,
43
+ cwd,
44
+ config,
45
+ spawnVitest: opts.spawnVitest,
46
+ });
47
+ }
48
+ catch (err) {
49
+ process.stderr.write(`${errMsg(err)}\n`);
50
+ return 1;
51
+ }
25
52
  // 1. Change-set
26
53
  let baseRef;
27
54
  let changedFiles;
@@ -53,22 +80,11 @@ export async function runChanged(opts) {
53
80
  baseRefLine = `pathgrade: base = ${baseRef} (merge-base with HEAD)`;
54
81
  }
55
82
  // 2. Selection
56
- const configPath = findVitestConfigArg(parsed.vitestArgs);
57
- let config;
58
- try {
59
- config = await loadAffectedConfig(cwd, {
60
- configPath,
61
- onWarning: w => {
62
- if (!parsed.quiet)
63
- process.stderr.write(`${w}\n`);
64
- },
65
- });
66
- }
67
- catch (err) {
68
- process.stderr.write(`${errMsg(err)}\n`);
69
- return 1;
70
- }
71
- const evalFiles = filterEvalFilesForConfig(discoverEvalFiles(cwd), config);
83
+ const evalFiles = discoverPathgradeEvalFiles({
84
+ cwd,
85
+ include: config.evals.include,
86
+ exclude: config.evals.exclude,
87
+ });
72
88
  let result;
73
89
  try {
74
90
  result = selectAffected({
@@ -76,7 +92,7 @@ export async function runChanged(opts) {
76
92
  changedFiles,
77
93
  repoRoot: cwd,
78
94
  baseRef,
79
- global: config.global,
95
+ global: config.affected.global,
80
96
  });
81
97
  }
82
98
  catch (err) {
@@ -103,16 +119,16 @@ export async function runChanged(opts) {
103
119
  return 0;
104
120
  }
105
121
  const selectedFiles = result.selected.map(s => s.file);
106
- if (hasPassWithNoTests(parsed.vitestArgs)) {
107
- process.stderr.write('pathgrade run: --passWithNoTests cannot be used with pathgrade run --changed. ' +
108
- 'The command already exits 0 when no evals are selected; if selected evals resolve to no Vitest files, CI must fail.\n');
109
- return 1;
110
- }
111
- const argv = ['run', ...selectedFiles, ...parsed.vitestArgs];
122
+ const runnerArgs = [...config.runner.args, ...parsed.runnerArgs];
112
123
  if (!parsed.quiet) {
113
- process.stderr.write(`→ vitest run ${selectedFiles.join(' ')}\n`);
124
+ process.stderr.write(`→ ${runnerInvocation.name} run ${selectedFiles.join(' ')}\n`);
114
125
  }
115
- return await spawnVitest({ argv });
126
+ return await runnerInvocation.run({
127
+ cwd,
128
+ runnerArgs,
129
+ selectedFiles,
130
+ env: runnerEnv,
131
+ });
116
132
  }
117
133
  function printRunStartSummary(input) {
118
134
  const { baseRefLine, totalEvals, changedCount, result } = input;
@@ -141,15 +157,6 @@ function readChangedFilesList(filePath) {
141
157
  function errMsg(err) {
142
158
  return err instanceof Error ? err.message : String(err);
143
159
  }
144
- function hasPassWithNoTests(args) {
145
- return args.some(arg => {
146
- if (arg === '--passWithNoTests')
147
- return true;
148
- if (!arg.startsWith('--passWithNoTests='))
149
- return false;
150
- return arg.slice('--passWithNoTests='.length).toLowerCase() !== 'false';
151
- });
152
- }
153
160
  function findVitestConfigArg(args) {
154
161
  for (let i = 0; i < args.length; i++) {
155
162
  const arg = args[i];
@@ -162,23 +169,3 @@ function findVitestConfigArg(args) {
162
169
  }
163
170
  return undefined;
164
171
  }
165
- function filterEvalFilesForConfig(evalFiles, config) {
166
- if (!config.include && !config.exclude)
167
- return evalFiles;
168
- const includeMatchers = config.include?.map(g => picomatch(g, { dot: true }));
169
- const excludeMatchers = config.exclude?.map(g => picomatch(g, { dot: true })) ?? [];
170
- return evalFiles.filter(file => {
171
- const included = includeMatchers ? includeMatchers.some(m => m(file)) : true;
172
- return included && !excludeMatchers.some(m => m(file));
173
- });
174
- }
175
- async function defaultSpawnVitest(req) {
176
- const { spawn } = await import('child_process');
177
- return await new Promise(resolve => {
178
- const child = spawn('npx', ['vitest', ...req.argv], {
179
- stdio: 'inherit',
180
- shell: true,
181
- });
182
- child.on('close', code => resolve(code ?? 0));
183
- });
184
- }
@@ -0,0 +1,48 @@
1
+ export interface PathgradeConfig {
2
+ runner?: {
3
+ adapter?: string;
4
+ args?: string[];
5
+ };
6
+ evals?: {
7
+ include?: string[];
8
+ exclude?: string[];
9
+ };
10
+ affected?: {
11
+ global?: string[];
12
+ };
13
+ reporter?: 'cli' | 'browser' | 'json';
14
+ diagnostics?: boolean;
15
+ verbose?: boolean;
16
+ ci?: {
17
+ threshold?: number;
18
+ };
19
+ }
20
+ export interface ResolvedPathgradeConfig {
21
+ runner: {
22
+ adapter: string;
23
+ args: string[];
24
+ };
25
+ evals: {
26
+ include: string[];
27
+ exclude: string[];
28
+ };
29
+ affected: {
30
+ global: string[];
31
+ };
32
+ reporter?: 'cli' | 'browser' | 'json';
33
+ diagnostics: boolean;
34
+ verbose: boolean;
35
+ ci: {
36
+ threshold?: number;
37
+ };
38
+ }
39
+ export declare const DEFAULT_EVAL_INCLUDE: string[];
40
+ export declare const DEFAULT_EVAL_EXCLUDE: string[];
41
+ export declare function defaultPathgradeConfig(): ResolvedPathgradeConfig;
42
+ export declare function resolvePathgradeConfig(input: {
43
+ cwd: string;
44
+ cli?: PathgradeConfig;
45
+ configPath?: string;
46
+ legacyVitestConfigPath?: string;
47
+ warn?: (message: string) => void;
48
+ }): Promise<ResolvedPathgradeConfig>;