pr-checkmate 1.22.2 → 1.22.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,6 +7,7 @@
7
7
  ![TypeScript](https://img.shields.io/badge/TypeScript-6-3178C6?logo=typescript&logoColor=white)
8
8
  ![cspell](https://img.shields.io/badge/cspell-10-4285F4)
9
9
  ![jscpd](https://img.shields.io/badge/jscpd-5-blue)
10
+ [![API Docs](https://img.shields.io/badge/API_docs-TypeDoc-9600FF?logo=readthedocs&logoColor=white)](https://pr-checkmate-docs.pages.dev)
10
11
 
11
12
  > **PR CheckMate** is a universal PR quality checker that runs as a single `npm install` and works across **TypeScript, JavaScript, Python, C++, Swift, and Android/Kotlin** repositories — no per-language toolchain setup required.
12
13
 
@@ -218,7 +219,7 @@ jobs:
218
219
  |---|---|---|
219
220
  | ESLint + SonarJS + JSDoc | blocking | Lint with complexity, import order, and doc coverage rules |
220
221
  | TypeScript | blocking | `tsc --noEmit` type check |
221
- | Dependency Check | blocking | Unused / missing deps via depcheck |
222
+ | Dependency Check | blocking | Unused / missing deps via depcheck; silence CLI-only false positives with `dependency.ignore` |
222
223
  | NPM Audit | blocking | Vulnerability scan (`moderate`+) |
223
224
  | Circular Deps | blocking | Circular dependency detection |
224
225
  | License Check | blocking | Blocks GPL / AGPL / LGPL packages |
@@ -226,7 +227,7 @@ jobs:
226
227
  | SwiftLint | informational | Swift linting |
227
228
  | ktlint | informational | Kotlin linting |
228
229
  | Dead Code | informational | Unused exports (`ts-unused-exports`) |
229
- | Duplicate Code | informational | `jscpd` — 5 lines / 50 tokens threshold |
230
+ | Duplicate Code | informational | `jscpd` — 5 lines / 50 tokens threshold; test files ignored by default (set `duplicate.ignore`); only source-code formats are scanned, so YAML/JSON/Markdown are skipped unless you override `duplicate.formats` |
230
231
  | Diff Smell | informational | `console.*`, `debugger`, `TODO/FIXME` in added lines |
231
232
  | Diff Security | informational | XSS, `eval`, SQLi, weak crypto in added lines |
232
233
  | Sensitive Files | informational | `.env`, private keys, CI configs touched in PR |
@@ -258,8 +259,17 @@ All checks can be tuned or disabled per-repo via `pr-checkmate.json`:
258
259
  "swift": { "enabled": true },
259
260
  "kotlin": { "enabled": true },
260
261
  "cpp": { "enabled": true, "clangFormat": { "style": "Google" } },
261
- "duplicate": { "enabled": true, "minLines": 10 },
262
+ "duplicate": {
263
+ "enabled": true,
264
+ "minLines": 10,
265
+ "formats": ["javascript", "typescript", "jsx", "tsx", "python", "kotlin", "swift", "c", "cpp"],
266
+ "ignore": ["**/__tests__/**", "**/*.test.*", "**/*.spec.*", "**/test/**", "**/tests/**"]
267
+ },
262
268
  "deadCode": { "enabled": false },
269
+ "dependency": {
270
+ "enabled": true,
271
+ "ignore": ["@types/*", "some-cli-tool"]
272
+ },
263
273
  "commitlint": {
264
274
  "enabled": true,
265
275
  "types": ["feat", "fix", "docs", "chore", "ci", "refactor", "test"]
@@ -272,6 +282,18 @@ All checks can be tuned or disabled per-repo via `pr-checkmate.json`:
272
282
  }
273
283
  ```
274
284
 
285
+ A note on `duplicate.ignore`:
286
+
287
+ - Setting it replaces the default test-file list rather than merging with it. If you add your own patterns, include any test globs you still want excluded.
288
+ - `node_modules/**`, `dist/**`, and `.git/**` are always excluded, regardless of config.
289
+ - `duplicate.formats` defaults to source-code formats only, so data files like YAML, JSON, and Markdown are not scanned. Setting it replaces the default list, so add `"yaml"` if you want duplication flagged in those files too.
290
+
291
+ A note on `dependency.ignore`:
292
+
293
+ - List dependency names or globs (such as `"@types/*"`) to exclude from the unused/missing check. The values are passed straight to depcheck's `--ignores`.
294
+ - Use it for packages that only run as CLI binaries or are referenced from config files. depcheck sees no `import` for them and would otherwise flag them as unused.
295
+ - When set, `dependency.ignore` takes precedence over any `.depcheckrc` file. Leave it empty or unset and your existing `.depcheckrc` still applies.
296
+
275
297
  ---
276
298
 
277
299
  ## Delta Mode
@@ -280,6 +302,60 @@ In GitHub Actions PR context, language checks automatically switch to **delta mo
280
302
 
281
303
  ---
282
304
 
305
+ ## Programmatic API
306
+
307
+ Besides the CLI, PR CheckMate ships a typed programmatic API. Install the package and import checks directly — useful for embedding the same quality gates inside your own tooling. The full reference is published at the [API docs site](https://pr-checkmate-docs.pages.dev).
308
+
309
+ Run the built-in checks:
310
+
311
+ ```ts
312
+ import { runChecks } from 'pr-checkmate';
313
+ import type { CheckReport } from 'pr-checkmate';
314
+
315
+ const report: CheckReport = await runChecks({
316
+ cwd: process.cwd(),
317
+ reporter: 'stdout', // 'none' | 'stdout' | 'github' | (report) => void
318
+ });
319
+
320
+ if (!report.ok) process.exit(1);
321
+ ```
322
+
323
+ `runChecks` never throws on violations and never calls `process.exit` — the caller decides. `report.ok` is `true` when nothing failed; warnings and skips do not affect it.
324
+
325
+ Author a custom check:
326
+
327
+ ```ts
328
+ import { runChecks, defineCheck, pass, fail } from 'pr-checkmate';
329
+
330
+ const noFixmes = defineCheck({
331
+ name: 'no-fixmes',
332
+ phase: 'lint',
333
+ applies: () => true,
334
+ run: async (ctx) => {
335
+ // inspect ctx (config, changed files, language, sha range) and return an outcome
336
+ return pass();
337
+ },
338
+ });
339
+
340
+ await runChecks({ checks: [noFixmes] }); // appended to the built-in registry by default
341
+ ```
342
+
343
+ ### Key exports
344
+
345
+ | Export | Purpose |
346
+ |---|---|
347
+ | `runChecks` | Run the check pipeline; returns a structured `CheckReport` |
348
+ | `buildContext` | Build the `CheckContext` (language, config, sha range) checks receive |
349
+ | `REGISTRY` | The array of built-in checks |
350
+ | `defineCheck` | Identity helper for authoring a `Check` with full type inference |
351
+ | `pass` / `warn` / `fail` / `skip` | Outcome constructors for check bodies |
352
+ | `getSourcePath` / `getPackageConfigPath` / `hasLocalConfig` | Config-path helpers |
353
+ | Types | `Check`, `CheckContext`, `CheckReport`, `CheckResult`, `CheckOutcome`, `CheckStatus`, `RunOptions`, `Language`, `Phase`, `ReporterKind`, `ReporterFn`, `PRCheckMateConfig` |
354
+
355
+ Regenerate the docs locally with `npm run docs` (output in `docs/api`).
356
+
357
+ ---
358
+
283
359
  ## PR Summary Comment
284
360
 
285
361
  After `npx pr-checkmate all`, a summary table is posted to the PR. On repeated pushes the comment is updated in place — no duplicate comments.
@@ -2,7 +2,21 @@
2
2
  * Common patterns to ignore in all checks
3
3
  */
4
4
  export declare const IGNORE_PATTERNS: string[];
5
+ /**
6
+ * Test files/folders excluded from duplicate-code detection by default.
7
+ * Test suites legitimately repeat setup/arrange/assert boilerplate, so counting
8
+ * them as clones is noise that drowns out real production duplication. Clients can
9
+ * override this list via `duplicate.ignore` in pr-checkmate.json.
10
+ */
11
+ export declare const DUPLICATE_IGNORE_DEFAULTS: string[];
5
12
  export declare const CONFIG_FILE_PATTERNS: string[];
13
+ /**
14
+ * jscpd formats the duplicate-CODE check scans. Restricting to code formats keeps
15
+ * config/CI YAML and JSON out of the report — they are data, not code, and their
16
+ * repetition is not meaningful duplication. This mirrors the code-only file set the
17
+ * check already uses when scanning a git diff, so full-tree and diff scans agree.
18
+ */
19
+ export declare const DUPLICATE_CODE_FORMATS: string[];
6
20
  /**
7
21
  * File extensions to check
8
22
  */
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.CONFIG_FILE = exports.documentation = exports.DEFAULT_ALLOW_PACKAGES = exports.DEFAULT_COMMANDS = exports.SOURCE_EXTENSIONS = exports.CONFIG_FILE_PATTERNS = exports.IGNORE_PATTERNS = void 0;
6
+ exports.CONFIG_FILE = exports.documentation = exports.DEFAULT_ALLOW_PACKAGES = exports.DEFAULT_COMMANDS = exports.SOURCE_EXTENSIONS = exports.DUPLICATE_CODE_FORMATS = exports.CONFIG_FILE_PATTERNS = exports.DUPLICATE_IGNORE_DEFAULTS = exports.IGNORE_PATTERNS = void 0;
7
7
  const node_path_1 = __importDefault(require("node:path"));
8
8
  /**
9
9
  * Common patterns to ignore in all checks
@@ -57,14 +57,49 @@ exports.IGNORE_PATTERNS = [
57
57
  '**/lint-report/**',
58
58
  '**/audit-report/**',
59
59
  ];
60
+ /**
61
+ * Test files/folders excluded from duplicate-code detection by default.
62
+ * Test suites legitimately repeat setup/arrange/assert boilerplate, so counting
63
+ * them as clones is noise that drowns out real production duplication. Clients can
64
+ * override this list via `duplicate.ignore` in pr-checkmate.json.
65
+ */
66
+ exports.DUPLICATE_IGNORE_DEFAULTS = [
67
+ '**/__tests__/**',
68
+ '**/__mocks__/**',
69
+ '**/*.test.*',
70
+ '**/*.spec.*',
71
+ '**/test/**',
72
+ '**/tests/**',
73
+ ];
60
74
  exports.CONFIG_FILE_PATTERNS = [
61
75
  '**/*.config.js',
76
+ '**/*.config.cjs',
77
+ '**/*.config.mjs',
62
78
  '**/*.config.ts',
79
+ '**/.eslintrc.*',
80
+ '**/eslint.config.*',
63
81
  '**/jest.config.*',
64
82
  '**/wdio.config.*',
65
83
  '**/playwright.config.*',
66
84
  '**/cypress.config.*',
67
85
  ];
86
+ /**
87
+ * jscpd formats the duplicate-CODE check scans. Restricting to code formats keeps
88
+ * config/CI YAML and JSON out of the report — they are data, not code, and their
89
+ * repetition is not meaningful duplication. This mirrors the code-only file set the
90
+ * check already uses when scanning a git diff, so full-tree and diff scans agree.
91
+ */
92
+ exports.DUPLICATE_CODE_FORMATS = [
93
+ 'javascript',
94
+ 'typescript',
95
+ 'jsx',
96
+ 'tsx',
97
+ 'python',
98
+ 'kotlin',
99
+ 'swift',
100
+ 'c',
101
+ 'cpp',
102
+ ];
68
103
  /**
69
104
  * File extensions to check
70
105
  */
@@ -13,11 +13,3 @@ export declare function getPackageConfigPath(fileName: string): string;
13
13
  * Check if project has local config
14
14
  */
15
15
  export declare function hasLocalConfig(configNames: string[]): boolean;
16
- /**
17
- * For backward compatibility, also export as constant
18
- * But this will be evaluated at module load time
19
- */
20
- export declare const config: {
21
- sourcePath: string;
22
- };
23
- export declare const SOURCE_PATH: string;
@@ -17,7 +17,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
17
17
  return (mod && mod.__esModule) ? mod : { "default": mod };
18
18
  };
19
19
  Object.defineProperty(exports, "__esModule", { value: true });
20
- exports.SOURCE_PATH = exports.config = exports.loadProjectConfig = void 0;
20
+ exports.loadProjectConfig = void 0;
21
21
  exports.getSourcePath = getSourcePath;
22
22
  exports.getPackageConfigPath = getPackageConfigPath;
23
23
  exports.hasLocalConfig = hasLocalConfig;
@@ -65,9 +65,3 @@ function hasLocalConfig(configNames) {
65
65
  const projectRoot = process.cwd();
66
66
  return configNames.some(name => node_fs_1.default.existsSync(node_path_1.default.join(projectRoot, name)));
67
67
  }
68
- /**
69
- * For backward compatibility, also export as constant
70
- * But this will be evaluated at module load time
71
- */
72
- exports.config = loadConfig();
73
- exports.SOURCE_PATH = exports.config.sourcePath;
@@ -4,11 +4,3 @@ import { PRCheckMateConfig } from '../interfaces';
4
4
  * Falls back to defaults if not found
5
5
  */
6
6
  export declare function loadProjectConfig(): PRCheckMateConfig;
7
- /**
8
- * Get default configuration
9
- */
10
- export declare function getDefaultConfig(): PRCheckMateConfig;
11
- /**
12
- * Export config for external use
13
- */
14
- export declare const DEFAULT_CONFIG: PRCheckMateConfig;
@@ -3,9 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.DEFAULT_CONFIG = void 0;
7
6
  exports.loadProjectConfig = loadProjectConfig;
8
- exports.getDefaultConfig = getDefaultConfig;
9
7
  const node_fs_1 = __importDefault(require("node:fs"));
10
8
  const node_path_1 = __importDefault(require("node:path"));
11
9
  const utils_1 = require("../utils");
@@ -71,6 +69,7 @@ function mergeWithDefaults(userConfig) {
71
69
  diffSmell: { ...defaults.diffSmell, ...userConfig.diffSmell },
72
70
  prBody: { ...defaults.prBody, ...userConfig.prBody },
73
71
  duplicate: { ...defaults.duplicate, ...userConfig.duplicate },
72
+ dependency: { ...defaults.dependency, ...userConfig.dependency },
74
73
  commitlint: { ...defaults.commitlint, ...userConfig.commitlint },
75
74
  deadCode: { ...defaults.deadCode, ...userConfig.deadCode },
76
75
  licenseCheck: {
@@ -146,8 +145,15 @@ function getDefaultConfig() {
146
145
  kotlin: { enabled: true },
147
146
  diffSmell: { enabled: true },
148
147
  prBody: { enabled: true, minLength: 20, requireTicket: true },
149
- duplicate: { enabled: true, minLines: 5, minTokens: 50 },
150
- deadCode: { enabled: true },
148
+ duplicate: {
149
+ enabled: true,
150
+ minLines: 5,
151
+ minTokens: 50,
152
+ ignore: constants_1.DUPLICATE_IGNORE_DEFAULTS,
153
+ formats: constants_1.DUPLICATE_CODE_FORMATS,
154
+ },
155
+ dependency: { enabled: true, ignore: [] },
156
+ deadCode: { enabled: true, ignoreFiles: [] },
151
157
  commitlint: {
152
158
  enabled: true,
153
159
  types: [
@@ -174,7 +180,3 @@ function getDefaultConfig() {
174
180
  },
175
181
  };
176
182
  }
177
- /**
178
- * Export config for external use
179
- */
180
- exports.DEFAULT_CONFIG = getDefaultConfig();
@@ -24,6 +24,12 @@ exports.deadCodeCheck = {
24
24
  }
25
25
  const bin = (0, utils_1.resolvePackageBin)('ts-unused-exports', 'bin/ts-unused-exports');
26
26
  try {
27
+ // Run the full analysis unfiltered: ignore patterns are applied to the
28
+ // *report* below, not passed to ts-unused-exports' own `--ignoreFiles`.
29
+ // That flag drops files from analysis entirely, so a file's imports stop
30
+ // counting as usage — ignoring a barrel or entry point would then cascade
31
+ // and flag everything it re-exports. Filtering the output instead keeps
32
+ // every usage edge intact and only hides the exempted files' own reports.
27
33
  const result = await (0, execa_1.execa)(process.execPath, [bin, tsConfigPath], {
28
34
  cwd: ctx.cwd,
29
35
  reject: false,
@@ -33,14 +39,31 @@ exports.deadCodeCheck = {
33
39
  utils_1.logger.info('[Dead Code]: ✅ No unused exports found');
34
40
  return (0, types_1.pass)();
35
41
  }
36
- const lines = output.split('\n').filter(l => l.trim());
37
- const fileCount = lines.length;
38
- const exportCount = lines.reduce((sum, line) => {
39
- const match = line.match(/\[(\d+)\]:/);
40
- return sum + (match ? parseInt(match[1], 10) : 1);
41
- }, 0);
42
+ const ignore = compileIgnorePatterns(ctx.config.deadCode?.ignoreFiles);
43
+ // ts-unused-exports prints one `path: sym1, sym2` line per file, preceded
44
+ // by a `N modules with unused exports` summary line we skip.
45
+ const entries = output
46
+ .split('\n')
47
+ .map(line => line.trim().match(/^(.+\.[cm]?tsx?): (.+)$/))
48
+ .filter((match) => match !== null)
49
+ .map(match => ({
50
+ file: match[1],
51
+ symbols: match[2]
52
+ .split(',')
53
+ .map(sym => sym.trim())
54
+ .filter(Boolean),
55
+ }))
56
+ .filter(entry => !ignore.some(re => re.test(entry.file)));
57
+ if (entries.length === 0) {
58
+ utils_1.logger.info('[Dead Code]: ✅ No unused exports found');
59
+ return (0, types_1.pass)();
60
+ }
61
+ const fileCount = entries.length;
62
+ const exportCount = entries.reduce((sum, entry) => sum + entry.symbols.length, 0);
42
63
  utils_1.logger.warn(`[Dead Code]: ⚠️ ${exportCount} unused export(s) across ${fileCount} file(s):`);
43
- utils_1.logger.warn(output);
64
+ for (const entry of entries) {
65
+ utils_1.logger.warn(`${entry.file}: ${entry.symbols.join(', ')}`);
66
+ }
44
67
  return (0, types_1.warn)(`${exportCount} unused export(s) in ${fileCount} file(s)`);
45
68
  }
46
69
  catch (err) {
@@ -49,3 +72,18 @@ exports.deadCodeCheck = {
49
72
  }
50
73
  },
51
74
  };
75
+ /**
76
+ * Compile `deadCode.ignoreFiles` regex sources, skipping any that fail to parse
77
+ * (a bad pattern warns rather than aborting the whole check).
78
+ */
79
+ function compileIgnorePatterns(patterns) {
80
+ return (patterns ?? []).filter(Boolean).flatMap(pattern => {
81
+ try {
82
+ return [new RegExp(pattern)];
83
+ }
84
+ catch {
85
+ utils_1.logger.warn(`[Dead Code]: ⚠️ Ignoring invalid ignoreFiles pattern: ${pattern}`);
86
+ return [];
87
+ }
88
+ });
89
+ }
@@ -13,6 +13,8 @@ exports.dependencyCheck = {
13
13
  name: 'Dependencies',
14
14
  phase: 'blocking',
15
15
  applies(ctx) {
16
+ if (ctx.config.dependency?.enabled === false)
17
+ return false;
16
18
  return node_fs_1.default.existsSync(node_path_1.default.join(ctx.cwd, 'package.json'));
17
19
  },
18
20
  async run(ctx) {
@@ -25,8 +27,16 @@ exports.dependencyCheck = {
25
27
  catch {
26
28
  return (0, types_1.skip)('depcheck unavailable');
27
29
  }
30
+ // Dependency names/globs to skip, configured in pr-checkmate.json. Passed via
31
+ // --ignores so a single pr-checkmate.json is the source of truth; depcheck's
32
+ // CLI --ignores overrides any .depcheckrc, so we only pass it when non-empty
33
+ // (letting a client's own .depcheckrc still apply when nothing is configured).
34
+ const ignore = ctx.config.dependency?.ignore ?? [];
35
+ const args = [depcheckBin, '--json'];
36
+ if (ignore.length > 0)
37
+ args.push(`--ignores=${ignore.join(',')}`);
28
38
  // reject:false — depcheck exits non-zero when it finds issues but still emits JSON on stdout.
29
- const { stdout } = await (0, execa_1.execa)(process.execPath, [depcheckBin, '--json'], {
39
+ const { stdout } = await (0, execa_1.execa)(process.execPath, args, {
30
40
  cwd: ctx.cwd,
31
41
  reject: false,
32
42
  });
@@ -7,9 +7,15 @@ exports.duplicateCheck = void 0;
7
7
  const node_fs_1 = __importDefault(require("node:fs"));
8
8
  const node_path_1 = __importDefault(require("node:path"));
9
9
  const execa_1 = require("execa");
10
+ const constants_1 = require("../../config/constants");
10
11
  const utils_1 = require("../../utils");
11
12
  const delta_1 = require("../../utils/delta");
12
13
  const types_1 = require("../types");
14
+ // Always excluded regardless of client config — scanning these never yields
15
+ // meaningful production duplication. Config files (eslint/*.config.*) are data,
16
+ // not logic, and are commonly shipped in two formats (e.g. .eslintrc + flat
17
+ // config), so their repetition is expected rather than a code smell.
18
+ const BASE_IGNORE = ['node_modules/**', 'dist/**', '.git/**', ...constants_1.CONFIG_FILE_PATTERNS];
13
19
  // jscpd v5 appends promotional/funding lines after its report. Strip them so
14
20
  // they never reach the client's terminal when running our package.
15
21
  const JSCPD_PROMO_MARKERS = [
@@ -44,6 +50,14 @@ exports.duplicateCheck = {
44
50
  const srcPath = node_path_1.default.join(ctx.cwd, 'src');
45
51
  const targetPaths = changedFiles ?? (node_fs_1.default.existsSync(srcPath) ? [srcPath] : [ctx.cwd]);
46
52
  const jscpdBin = (0, utils_1.resolvePackageBin)('jscpd', 'run-jscpd.js');
53
+ // Client-configurable exclusions (pr-checkmate.json `duplicate.ignore`),
54
+ // defaulting to test files/folders, appended to the always-on base ignores.
55
+ const configuredIgnore = ctx.config.duplicate?.ignore ?? constants_1.DUPLICATE_IGNORE_DEFAULTS;
56
+ const ignore = [...BASE_IGNORE, ...configuredIgnore].join(',');
57
+ // Client-configurable formats (pr-checkmate.json `duplicate.formats`),
58
+ // defaulting to source-code formats so data files (yaml/json/markdown) are
59
+ // not scanned. Clients can add them back explicitly.
60
+ const formats = (ctx.config.duplicate?.formats ?? constants_1.DUPLICATE_CODE_FORMATS).join(',');
47
61
  try {
48
62
  const result = await (0, execa_1.execa)(process.execPath, [
49
63
  jscpdBin,
@@ -53,8 +67,10 @@ exports.duplicateCheck = {
53
67
  String(minTokens),
54
68
  '--reporters',
55
69
  'console',
70
+ '--format',
71
+ formats,
56
72
  '--ignore',
57
- 'node_modules/**,dist/**,.git/**',
73
+ ignore,
58
74
  ...targetPaths,
59
75
  ], { cwd: ctx.cwd, reject: false });
60
76
  const output = stripJscpdPromo([result.stdout, result.stderr].filter(Boolean).join('\n'));
@@ -9,7 +9,7 @@ const node_path_1 = __importDefault(require("node:path"));
9
9
  const utils_1 = require("../../utils");
10
10
  const targets_1 = require("../targets");
11
11
  const types_1 = require("../types");
12
- const PY_GLOBS = ['*.py'];
12
+ const ruff_shared_1 = require("./ruff-shared");
13
13
  /**
14
14
  * Formats changed Python files with Ruff's formatter (WASM, no Python/pip needed).
15
15
  * Read-only in check mode (`warn` when files differ from formatted output); rewrites
@@ -24,13 +24,12 @@ exports.pythonFormatCheck = {
24
24
  ctx.languages.includes('python'));
25
25
  },
26
26
  async run(ctx) {
27
- const targets = await (0, targets_1.resolveTargetFiles)(ctx, PY_GLOBS);
27
+ const targets = await (0, targets_1.resolveTargetFiles)(ctx, ruff_shared_1.PY_GLOBS);
28
28
  if (targets === null)
29
29
  return (0, types_1.skip)('git unavailable');
30
30
  if (targets.length === 0)
31
31
  return (0, types_1.pass)('no Python files changed');
32
- const { Workspace, PositionEncoding } = require('@astral-sh/ruff-wasm-nodejs');
33
- const workspace = new Workspace(Workspace.defaultSettings(), PositionEncoding.Utf16);
32
+ const workspace = (0, ruff_shared_1.createRuffWorkspace)();
34
33
  let unformatted = 0;
35
34
  for (const rel of targets) {
36
35
  const abs = node_path_1.default.join(ctx.cwd, rel);
@@ -9,7 +9,7 @@ const node_path_1 = __importDefault(require("node:path"));
9
9
  const utils_1 = require("../../utils");
10
10
  const targets_1 = require("../targets");
11
11
  const types_1 = require("../types");
12
- const PY_GLOBS = ['*.py'];
12
+ const ruff_shared_1 = require("./ruff-shared");
13
13
  /**
14
14
  * Lints changed Python files with Ruff, bundled as a self-contained WASM module
15
15
  * (`@astral-sh/ruff-wasm-nodejs`) so it runs on a bare Node image with no Python
@@ -26,13 +26,12 @@ exports.pythonLintCheck = {
26
26
  ctx.languages.includes('python'));
27
27
  },
28
28
  async run(ctx) {
29
- const targets = await (0, targets_1.resolveTargetFiles)(ctx, PY_GLOBS);
29
+ const targets = await (0, targets_1.resolveTargetFiles)(ctx, ruff_shared_1.PY_GLOBS);
30
30
  if (targets === null)
31
31
  return (0, types_1.skip)('git unavailable');
32
32
  if (targets.length === 0)
33
33
  return (0, types_1.pass)('no Python files changed');
34
- const { Workspace, PositionEncoding } = require('@astral-sh/ruff-wasm-nodejs');
35
- const workspace = new Workspace(Workspace.defaultSettings(), PositionEncoding.Utf16);
34
+ const workspace = (0, ruff_shared_1.createRuffWorkspace)();
36
35
  let totalIssues = 0;
37
36
  let filesWithIssues = 0;
38
37
  for (const rel of targets) {
@@ -0,0 +1,10 @@
1
+ type RuffModule = typeof import('@astral-sh/ruff-wasm-nodejs');
2
+ type RuffWorkspace = InstanceType<RuffModule['Workspace']>;
3
+ export declare const PY_GLOBS: string[];
4
+ /**
5
+ * Instantiate a Ruff WASM workspace with default settings. Shared by the Ruff
6
+ * lint and format checks so their setup stays identical, and kept as a function
7
+ * (not a module-level singleton) so the WASM module loads only when a check runs.
8
+ */
9
+ export declare function createRuffWorkspace(): RuffWorkspace;
10
+ export {};
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PY_GLOBS = void 0;
4
+ exports.createRuffWorkspace = createRuffWorkspace;
5
+ exports.PY_GLOBS = ['*.py'];
6
+ /**
7
+ * Instantiate a Ruff WASM workspace with default settings. Shared by the Ruff
8
+ * lint and format checks so their setup stays identical, and kept as a function
9
+ * (not a module-level singleton) so the WASM module loads only when a check runs.
10
+ */
11
+ function createRuffWorkspace() {
12
+ const { Workspace, PositionEncoding } = require('@astral-sh/ruff-wasm-nodejs');
13
+ return new Workspace(Workspace.defaultSettings(), PositionEncoding.Utf16);
14
+ }
@@ -1,7 +1,6 @@
1
- import type { CheckReport, CheckResult } from './types';
1
+ import type { CheckReport } from './types';
2
2
  /**
3
3
  * Emit the report through the selected channel.
4
4
  * none: nothing · stdout: console table · github: table + upsert PR comment.
5
5
  */
6
6
  export declare function report(kind: 'none' | 'stdout' | 'github', result: CheckReport): Promise<void>;
7
- export type { CheckResult };
@@ -54,7 +54,12 @@ function errMessage(err) {
54
54
  async function runOne(check, ctx, collector) {
55
55
  try {
56
56
  if (!(await check.applies(ctx))) {
57
- collector.register(check.name, 'skip', 'not applicable');
57
+ // A check that doesn't apply is irrelevant to this project/run — e.g. a
58
+ // language-specific check (SwiftLint, ktlint, Ruff, C++) in a repo of a
59
+ // different language, or a check disabled in config. Omit it from the report
60
+ // entirely rather than listing it as "not applicable": in a single-language
61
+ // repo those rows are pure noise and inflate the skipped count. Runtime skips
62
+ // from run() (e.g. "no relevant files changed") are still reported.
58
63
  return;
59
64
  }
60
65
  const outcome = await check.run(ctx);
package/dist/index.d.ts CHANGED
@@ -7,3 +7,4 @@ export { runChecks, buildContext, REGISTRY } from './core';
7
7
  export { pass, warn, fail, skip, defineCheck } from './core';
8
8
  export type { Check, CheckContext, CheckOutcome, CheckReport, CheckResult, CheckStatus, Language, Phase, ReporterFn, ReporterKind, RunOptions, } from './core';
9
9
  export { getSourcePath, getPackageConfigPath, hasLocalConfig } from './config';
10
+ export type { PRCheckMateConfig } from './interfaces';
package/dist/init.d.ts CHANGED
@@ -1,4 +1,3 @@
1
- export declare const CONFIG_FILE: string;
2
1
  /**
3
2
  * Initialize (or refresh) pr-checkmate configuration in the current project.
4
3
  */
package/dist/init.js CHANGED
@@ -3,14 +3,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.CONFIG_FILE = void 0;
7
6
  exports.init = init;
8
7
  const fs_1 = __importDefault(require("fs"));
9
8
  const path_1 = __importDefault(require("path"));
10
9
  const inquirer_1 = __importDefault(require("inquirer"));
11
10
  const config_1 = require("./config");
12
11
  const utils_1 = require("./utils");
13
- exports.CONFIG_FILE = path_1.default.resolve(process.cwd(), 'pr-checkmate.json');
12
+ const CONFIG_FILE = path_1.default.resolve(process.cwd(), 'pr-checkmate.json');
14
13
  /**
15
14
  * Default configuration structure with customization options
16
15
  */
@@ -60,6 +59,18 @@ const DEFAULT_CONFIG = {
60
59
  enabled: true,
61
60
  minLines: 5,
62
61
  minTokens: 50,
62
+ // Test files/folders are excluded by default; edit to customise.
63
+ ignore: config_1.DUPLICATE_IGNORE_DEFAULTS,
64
+ // Only source-code formats are scanned. Data formats (yaml, json, markdown)
65
+ // are intentionally absent — add them here to detect duplication in them too.
66
+ formats: config_1.DUPLICATE_CODE_FORMATS,
67
+ },
68
+ // Dependency usage check (depcheck)
69
+ dependency: {
70
+ enabled: true,
71
+ // Dependency names/globs to skip (e.g. CLI-only tools invoked as binaries,
72
+ // which depcheck can't see as imports and would report as unused).
73
+ ignore: [],
63
74
  },
64
75
  documentation: config_1.documentation,
65
76
  };
@@ -135,7 +146,7 @@ async function createConfig(root) {
135
146
  sourcePath,
136
147
  languages,
137
148
  };
138
- fs_1.default.writeFileSync(exports.CONFIG_FILE, JSON.stringify(config, null, 2));
149
+ fs_1.default.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
139
150
  utils_1.logger.info(`[init]: ✅ Created pr-checkmate.json with sourcePath: ${sourcePath}`);
140
151
  utils_1.logger.info(`[init]: 💡 You can now customize ESLint rules, Prettier settings,
141
152
  and cspell words in pr-checkmate.json`);
@@ -146,7 +157,7 @@ async function createConfig(root) {
146
157
  async function updateConfig(root) {
147
158
  utils_1.logger.info('[init]: pr-checkmate.json already exists — updating it.');
148
159
  try {
149
- const raw = fs_1.default.readFileSync(exports.CONFIG_FILE, 'utf-8');
160
+ const raw = fs_1.default.readFileSync(CONFIG_FILE, 'utf-8');
150
161
  const json = JSON.parse(raw);
151
162
  const languages = await (0, utils_1.detectLanguages)(root);
152
163
  utils_1.logger.info(`[init]: 🔍 Detected languages: ${languages.length ? languages.join(', ') : 'none'}`);
@@ -164,7 +175,7 @@ async function updateConfig(root) {
164
175
  prBody: { ...DEFAULT_CONFIG.prBody, ...(json.prBody || {}) },
165
176
  duplicate: { ...DEFAULT_CONFIG.duplicate, ...(json.duplicate || {}) },
166
177
  };
167
- fs_1.default.writeFileSync(exports.CONFIG_FILE, JSON.stringify(updated, null, 2));
178
+ fs_1.default.writeFileSync(CONFIG_FILE, JSON.stringify(updated, null, 2));
168
179
  utils_1.logger.info('[init]: 🔧 Updated pr-checkmate.json with customization options.');
169
180
  utils_1.logger.info('[init]: 💡 You can now add custom ESLint rules, cspell words, and more!');
170
181
  }
@@ -177,7 +188,7 @@ async function updateConfig(root) {
177
188
  */
178
189
  async function init() {
179
190
  const root = process.cwd();
180
- if (fs_1.default.existsSync(exports.CONFIG_FILE)) {
191
+ if (fs_1.default.existsSync(CONFIG_FILE)) {
181
192
  await updateConfig(root);
182
193
  }
183
194
  else {
@@ -72,6 +72,31 @@ export interface PRCheckMateConfig {
72
72
  enabled?: boolean;
73
73
  minLines?: number;
74
74
  minTokens?: number;
75
+ /**
76
+ * Glob patterns excluded from duplicate detection, in addition to the always-on
77
+ * node_modules/dist/.git ignores. Defaults to test files/folders
78
+ * (see DUPLICATE_IGNORE_DEFAULTS). Setting this replaces the default list.
79
+ */
80
+ ignore?: string[];
81
+ /**
82
+ * jscpd formats to scan. Defaults to source-code formats only
83
+ * (see DUPLICATE_CODE_FORMATS) — data formats like `yaml`, `json` and
84
+ * `markdown` are intentionally NOT scanned, since their repetition is rarely
85
+ * meaningful duplication. Add them back (e.g. `["typescript", "yaml"]`) if you
86
+ * do want to detect duplication in those files. Setting this replaces the list.
87
+ */
88
+ formats?: string[];
89
+ };
90
+ dependency?: {
91
+ enabled?: boolean;
92
+ /**
93
+ * Dependency names to exclude from the unused/missing check (depcheck).
94
+ * Supports globs, e.g. "@types/*". Use for packages that are invoked only as
95
+ * CLI binaries or referenced from config — depcheck sees no `import` for them
96
+ * and would otherwise report them as unused. Passed to depcheck's `--ignores`,
97
+ * which takes precedence over any `.depcheckrc` file.
98
+ */
99
+ ignore?: string[];
75
100
  };
76
101
  commitlint?: {
77
102
  enabled?: boolean;
@@ -97,33 +122,20 @@ export interface PRCheckMateConfig {
97
122
  };
98
123
  deadCode?: {
99
124
  enabled?: boolean;
125
+ /**
126
+ * Regex sources matched against reported file paths; matching files are
127
+ * dropped from the dead-code report. Use to exempt intentional public
128
+ * surface — a package entry point (`src/index.ts`) or re-export barrels
129
+ * whose exports are consumed by external installers, not within the repo.
130
+ * Applied to the report (not ts-unused-exports' own `--ignoreFiles`), so it
131
+ * never removes a file from analysis and cannot cascade onto other files.
132
+ */
133
+ ignoreFiles?: string[];
100
134
  };
101
135
  licenseCheck?: {
102
136
  allowPackages?: string[];
103
137
  allowLicenses?: string[];
104
138
  };
105
139
  }
106
- export interface CheckmateConfig {
107
- skipLint?: boolean;
108
- skipDeps?: boolean;
109
- skipFormat?: boolean;
110
- skipSpell?: boolean;
111
- skipSecurity?: boolean;
112
- skipNpmAudit?: boolean;
113
- skipCircularDeps?: boolean;
114
- skipLicense?: boolean;
115
- skipPrSize?: boolean;
116
- skipPrTitle?: boolean;
117
- skipOutdated?: boolean;
118
- skipMarkdown?: boolean;
119
- skipTypecheck?: boolean;
120
- skipDiffSmell?: boolean;
121
- skipPrBody?: boolean;
122
- skipDuplicate?: boolean;
123
- skipDiffSecurity?: boolean;
124
- skipSensitiveFiles?: boolean;
125
- skipDeadCode?: boolean;
126
- autoCommit?: boolean;
127
- }
128
140
  export type { CheckStatus, CheckResult } from '../utils/result-collector';
129
141
  export type { Language } from '../utils/detect-language';
@@ -1,5 +1 @@
1
- /**
2
- *
3
- */
4
- declare function copyConfigs(): void;
5
- export { copyConfigs };
1
+ export {};
@@ -3,7 +3,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.copyConfigs = copyConfigs;
7
6
  const node_fs_1 = __importDefault(require("node:fs"));
8
7
  const node_path_1 = __importDefault(require("node:path"));
9
8
  const utils_1 = require("../utils");
@@ -19,15 +19,19 @@ function readDependencyNames(pkgDir) {
19
19
  return [];
20
20
  }
21
21
  }
22
- /** Walk up from this module until we find pr-checkmate's own package root. */
23
- function findOwnPackageRoot() {
24
- let dir = __dirname;
22
+ /**
23
+ * Walk up from `startDir` (inclusive) toward the filesystem root, returning the
24
+ * first directory whose package.json declares `name === expectedName`, or null.
25
+ * Broken/missing package.json files are skipped rather than thrown.
26
+ */
27
+ function findPackageRootByName(startDir, expectedName) {
28
+ let dir = startDir;
25
29
  while (dir !== node_path_1.default.dirname(dir)) {
26
30
  const pkgJsonPath = node_path_1.default.join(dir, PACKAGE_JSON);
27
31
  if (node_fs_1.default.existsSync(pkgJsonPath)) {
28
32
  try {
29
33
  const pkg = JSON.parse(node_fs_1.default.readFileSync(pkgJsonPath, 'utf-8'));
30
- if (pkg.name === OWN_PACKAGE_NAME)
34
+ if (pkg.name === expectedName)
31
35
  return dir;
32
36
  }
33
37
  catch { }
@@ -36,6 +40,10 @@ function findOwnPackageRoot() {
36
40
  }
37
41
  return null;
38
42
  }
43
+ /** Walk up from this module until we find pr-checkmate's own package root. */
44
+ function findOwnPackageRoot() {
45
+ return findPackageRootByName(__dirname, OWN_PACKAGE_NAME);
46
+ }
39
47
  /**
40
48
  * Resolve a dependency's install directory by name, starting the search from
41
49
  * `fromDir`. Relies on Node's upward node_modules walk so hoisted packages are
@@ -50,19 +58,8 @@ function resolvePackageDir(name, fromDir) {
50
58
  // Fallback for packages whose exports map hides ./package.json: resolve the
51
59
  // entry point and walk up to the package's own root.
52
60
  try {
53
- let dir = node_path_1.default.dirname(require.resolve(name, { paths: [fromDir] }));
54
- while (dir !== node_path_1.default.dirname(dir)) {
55
- const pkgJsonPath = node_path_1.default.join(dir, PACKAGE_JSON);
56
- if (node_fs_1.default.existsSync(pkgJsonPath)) {
57
- try {
58
- const pkg = JSON.parse(node_fs_1.default.readFileSync(pkgJsonPath, 'utf-8'));
59
- if (pkg.name === name)
60
- return dir;
61
- }
62
- catch { }
63
- }
64
- dir = node_path_1.default.dirname(dir);
65
- }
61
+ const entryDir = node_path_1.default.dirname(require.resolve(name, { paths: [fromDir] }));
62
+ return findPackageRootByName(entryDir, name);
66
63
  }
67
64
  catch { }
68
65
  return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pr-checkmate",
3
- "version": "1.22.2",
3
+ "version": "1.22.4",
4
4
  "description": "Automated PR quality checks: linting, formatting, dependency analysis, and spellcheck",
5
5
  "keywords": [
6
6
  "github-actions",
@@ -46,6 +46,7 @@
46
46
  "lint": "eslint \"src/**/*.{ts,tsx}\" --fix",
47
47
  "prettier": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,md}\"",
48
48
  "spellcheck": "cspell '**/*.{ts,tsx,js,jsx,md,json}' --no-progress",
49
+ "docs": "typedoc",
49
50
  "scan:secrets": "gitleaks-secret-scanner --diff-mode ci --html-report gitleaks-report.html",
50
51
  "test": "echo 'Tests coming soon'",
51
52
  "security-scan": "npx pr-checkmate security",
@@ -99,6 +100,7 @@
99
100
  "typescript": "^6.0.3"
100
101
  },
101
102
  "devDependencies": {
102
- "standard-version": "^9.5.0"
103
+ "standard-version": "^9.5.0",
104
+ "typedoc": "^0.28.20"
103
105
  }
104
106
  }
@@ -1,2 +0,0 @@
1
- export declare const config: any;
2
- export declare const SOURCE_PATH: any;
@@ -1,12 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.SOURCE_PATH = exports.config = void 0;
7
- const node_fs_1 = __importDefault(require("node:fs"));
8
- const constants_1 = require("./constants");
9
- exports.config = node_fs_1.default.existsSync(constants_1.CONFIG_FILE)
10
- ? JSON.parse(node_fs_1.default.readFileSync(constants_1.CONFIG_FILE, 'utf-8'))
11
- : { sourcePath: 'src' };
12
- exports.SOURCE_PATH = exports.config.sourcePath;