@wix/pathgrade 0.35.0 → 0.36.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.
@@ -18,8 +18,11 @@
18
18
  */
19
19
  export interface AffectedConfig {
20
20
  global: string[];
21
+ include?: string[];
22
+ exclude?: string[];
21
23
  }
22
24
  export interface LoadOptions {
25
+ configPath?: string;
23
26
  onWarning?: (message: string) => void;
24
27
  }
25
28
  export declare function loadAffectedConfig(repoRoot: string, options?: LoadOptions): Promise<AffectedConfig>;
@@ -41,9 +41,11 @@ export async function loadAffectedConfig(repoRoot, options = {}) {
41
41
  return { global: [] };
42
42
  }
43
43
  }
44
- const configPath = VITEST_CONFIG_CANDIDATES
45
- .map(c => path.join(repoRoot, c))
46
- .find(p => fs.existsSync(p));
44
+ const configPath = options.configPath
45
+ ? path.resolve(repoRoot, options.configPath)
46
+ : VITEST_CONFIG_CANDIDATES
47
+ .map(c => path.join(repoRoot, c))
48
+ .find(p => fs.existsSync(p));
47
49
  if (!configPath)
48
50
  return { global: [] };
49
51
  let loaded;
@@ -52,7 +54,11 @@ export async function loadAffectedConfig(repoRoot, options = {}) {
52
54
  loaded = await jiti.import(configPath, { default: true });
53
55
  }
54
56
  catch (err) {
55
- warn(`pathgrade: failed to load ${path.relative(repoRoot, configPath)}: ${errMsg(err)}`);
57
+ const message = `pathgrade: failed to load ${path.relative(repoRoot, configPath)}: ${errMsg(err)}`;
58
+ if (options.configPath) {
59
+ throw new Error(message);
60
+ }
61
+ warn(message);
56
62
  return { global: [] };
57
63
  }
58
64
  const plugins = findPluginsList(loaded);
@@ -68,7 +74,13 @@ export async function loadAffectedConfig(repoRoot, options = {}) {
68
74
  }
69
75
  const opts = pathgradePlugin.__pathgradeOptions ?? {};
70
76
  const global = opts.affected?.global;
71
- return { global: Array.isArray(global) ? global : [] };
77
+ const include = opts.include;
78
+ const exclude = opts.exclude;
79
+ return {
80
+ global: Array.isArray(global) ? global : [],
81
+ ...(Array.isArray(include) ? { include } : {}),
82
+ ...(Array.isArray(exclude) ? { exclude } : {}),
83
+ };
72
84
  }
73
85
  /**
74
86
  * Given a loaded vitest config (either the raw export or a `defineConfig()`
@@ -13,6 +13,7 @@
13
13
  * only producer of the file list here.
14
14
  */
15
15
  import * as fs from 'fs';
16
+ import picomatch from 'picomatch';
16
17
  import { selectAffected } from '../affected/select.js';
17
18
  import { resolveBaseRef, computeChangedFiles } from '../affected/git.js';
18
19
  import { loadAffectedConfig } from '../affected/config.js';
@@ -52,13 +53,22 @@ export async function runChanged(opts) {
52
53
  baseRefLine = `pathgrade: base = ${baseRef} (merge-base with HEAD)`;
53
54
  }
54
55
  // 2. Selection
55
- const evalFiles = discoverEvalFiles(cwd);
56
- const config = await loadAffectedConfig(cwd, {
57
- onWarning: w => {
58
- if (!parsed.quiet)
59
- process.stderr.write(`${w}\n`);
60
- },
61
- });
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);
62
72
  let result;
63
73
  try {
64
74
  result = selectAffected({
@@ -93,6 +103,11 @@ export async function runChanged(opts) {
93
103
  return 0;
94
104
  }
95
105
  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
+ }
96
111
  const argv = ['run', ...selectedFiles, ...parsed.vitestArgs];
97
112
  if (!parsed.quiet) {
98
113
  process.stderr.write(`→ vitest run ${selectedFiles.join(' ')}\n`);
@@ -126,6 +141,37 @@ function readChangedFilesList(filePath) {
126
141
  function errMsg(err) {
127
142
  return err instanceof Error ? err.message : String(err);
128
143
  }
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
+ function findVitestConfigArg(args) {
154
+ for (let i = 0; i < args.length; i++) {
155
+ const arg = args[i];
156
+ if (arg === '--config' || arg === '-c')
157
+ return args[i + 1];
158
+ if (arg.startsWith('--config='))
159
+ return arg.slice('--config='.length);
160
+ if (arg.startsWith('-c='))
161
+ return arg.slice('-c='.length);
162
+ }
163
+ return undefined;
164
+ }
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
+ }
129
175
  async function defaultSpawnVitest(req) {
130
176
  const { spawn } = await import('child_process');
131
177
  return await new Promise(resolve => {
@@ -15,6 +15,7 @@ export declare class PathgradeReporter implements Reporter {
15
15
  */
16
16
  private getGroupKey;
17
17
  private toTestEntry;
18
+ private isReportableEntry;
18
19
  private printCliSummary;
19
20
  private writeJsonResults;
20
21
  private buildEvalReport;
@@ -45,6 +45,8 @@ export class PathgradeReporter {
45
45
  for (const testCase of mod.children.allTests()) {
46
46
  const groupKey = this.getGroupKey(testCase);
47
47
  const entry = this.toTestEntry(testCase);
48
+ if (!this.isReportableEntry(entry))
49
+ continue;
48
50
  if (!groupMap.has(groupKey)) {
49
51
  groupMap.set(groupKey, []);
50
52
  }
@@ -100,6 +102,9 @@ export class PathgradeReporter {
100
102
  diagnostics,
101
103
  };
102
104
  }
105
+ isReportableEntry(entry) {
106
+ return entry.state !== 'skipped' && entry.state !== 'pending';
107
+ }
103
108
  printCliSummary(groups) {
104
109
  console.log(`\n${fmt.bold('── pathgrade summary ')}${fmt.dim('─'.repeat(40))}\n`);
105
110
  const forceVerbose = this.opts.diagnostics === true || process.env.PATHGRADE_DIAGNOSTICS === '1';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/pathgrade",
3
- "version": "0.35.0",
3
+ "version": "0.36.0",
4
4
  "description": "Evaluate whether AI agents discover and use your skills correctly",
5
5
  "main": "./dist/sdk/index.js",
6
6
  "types": "./dist/sdk/index.d.ts",
@@ -97,5 +97,5 @@
97
97
  "typescript": "^5.9.3",
98
98
  "zod": "4.3.6"
99
99
  },
100
- "falconPackageHash": "7d7c65385d2ddc29c1b518b76ca8c6c57fd24241c74c182d1fd6fb56"
100
+ "falconPackageHash": "85d2875d7e92c76ffad2391c7756cbca44f843842a3d1b700b3e11f5"
101
101
  }