pr-checkmate 1.23.19 → 1.23.21

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.
@@ -3,6 +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.planDiffRange = planDiffRange;
6
7
  exports.buildContext = buildContext;
7
8
  const node_fs_1 = __importDefault(require("node:fs"));
8
9
  const loader_1 = require("../config/loader");
@@ -10,41 +11,71 @@ const utils_1 = require("../utils");
10
11
  const detect_language_1 = require("../utils/detect-language");
11
12
  const execa_1 = require("../utils/execa");
12
13
  /**
13
- * Resolve the diff range for delta-aware checks.
14
- * Prefers explicit opts, then GitHub PR event, then Bitbucket PR env, then git HEAD^.
15
- * Returns undefined SHAs when nothing usable is found → checks fall back to full scan.
14
+ * Decide what to look at. Order matters and is the contract:
15
+ *
16
+ * 1. `full` wins over everything — an explicit "check all of it" must not be
17
+ * quietly narrowed by a CI variable that happens to be set.
18
+ * 2. `staged`, then caller-supplied SHAs.
19
+ * 3. The CI platform's PR context (GitHub event payload, Bitbucket destination
20
+ * branch) — a gate exists to judge the diff.
21
+ * 4. A CI build with no PR context still diffs `HEAD^`: it is a push/branch build,
22
+ * and delta is why CI runs are fast.
23
+ * 5. Anything else is a human at a terminal → full scan. `HEAD^` used to be the
24
+ * fallback here, which meant a local `pr-checkmate all` judged only the last
25
+ * commit and could report a clean project with a `debugger` committed earlier.
26
+ *
27
+ * @param opts Run options as passed to `runChecks`.
28
+ * @param env Environment to read CI markers from. Defaults to the current process.
16
29
  */
30
+ function planDiffRange(opts, env = process.env) {
31
+ if (opts.full)
32
+ return { kind: 'full', reason: 'requested' };
33
+ if (opts.staged)
34
+ return { kind: 'staged' };
35
+ if (opts.baseSha || opts.headSha) {
36
+ return { kind: 'explicit', baseSha: opts.baseSha, headSha: opts.headSha };
37
+ }
38
+ if (env.GITHUB_EVENT_NAME === 'pull_request' && env.GITHUB_EVENT_PATH) {
39
+ return { kind: 'github-event' };
40
+ }
41
+ if (env.BITBUCKET_PR_DESTINATION_BRANCH) {
42
+ return {
43
+ kind: 'ref',
44
+ ref: `origin/${env.BITBUCKET_PR_DESTINATION_BRANCH}`,
45
+ reason: 'PR build',
46
+ };
47
+ }
48
+ if ((0, utils_1.isCiEnvironment)(env))
49
+ return { kind: 'ref', ref: 'HEAD^', reason: 'CI build, no PR context' };
50
+ return { kind: 'full', reason: 'no CI detected' };
51
+ }
17
52
  /**
18
- * Resolve a git ref to a concrete commit SHA, or undefined when it can't be
19
- * resolved in this clone. `--verify --quiet` emits nothing (instead of echoing
20
- * the literal ref) and exits non-zero for a missing ref — e.g. `HEAD^` on a
21
- * single-commit repo, or a base branch not fetched in a shallow CI clone. That
22
- * way an unresolvable base yields `undefined` and callers fall back to a full
23
- * scan, rather than diffing against a garbage ref and silently checking nothing.
53
+ * Carry out a {@link RangePlan}. Every path that cannot produce a usable base
54
+ * commit returns no SHAs, which the checks read as "scan everything" — degrading
55
+ * to a full scan is always safer than diffing against a ref that isn't there and
56
+ * silently reviewing nothing.
57
+ *
58
+ * @returns The resolved range, plus a label for the log line.
24
59
  */
25
- async function resolveRef(cwd, ref) {
26
- const { stdout } = await (0, execa_1.execa)('git', ['rev-parse', '--verify', '--quiet', `${ref}^{commit}`], {
27
- cwd,
28
- reject: false,
29
- });
30
- return stdout.trim() || undefined;
31
- }
32
60
  async function resolveDiffRange(cwd, opts) {
61
+ const plan = planDiffRange(opts, process.env);
62
+ if (plan.kind === 'full')
63
+ return { scope: `full (${plan.reason})` };
33
64
  // Pre-commit mode: diff HEAD against the current index. `git write-tree`
34
65
  // materialises the staged index as a real tree object, so every check that
35
66
  // diffs baseSha..headSha sees exactly the staged changes — no per-check code.
36
- if (opts.staged) {
67
+ if (plan.kind === 'staged') {
37
68
  try {
38
69
  const head = (await (0, execa_1.execa)('git', ['rev-parse', 'HEAD'], { cwd })).stdout.trim();
39
70
  const tree = (await (0, execa_1.execa)('git', ['write-tree'], { cwd })).stdout.trim();
40
- return { baseSha: head, headSha: tree };
71
+ return { baseSha: head, headSha: tree, scope: 'staged' };
41
72
  }
42
73
  catch {
43
- return {};
74
+ return { scope: 'full (no staged range)' };
44
75
  }
45
76
  }
46
- if (opts.baseSha || opts.headSha) {
47
- return { baseSha: opts.baseSha, headSha: opts.headSha };
77
+ if (plan.kind === 'explicit') {
78
+ return { baseSha: plan.baseSha, headSha: plan.headSha, scope: 'delta (explicit)' };
48
79
  }
49
80
  let headSha;
50
81
  try {
@@ -52,30 +83,32 @@ async function resolveDiffRange(cwd, opts) {
52
83
  headSha = stdout.trim();
53
84
  }
54
85
  catch {
55
- return {};
86
+ return { scope: 'full (no HEAD)' };
56
87
  }
57
88
  // GitHub Actions: the PR event payload carries the base branch SHA. Read it
58
89
  // directly so delta works without relying on any client-set variable
59
90
  // (GITHUB_EVENT_* are injected by the runner).
60
- if (process.env.GITHUB_EVENT_NAME === 'pull_request' && process.env.GITHUB_EVENT_PATH) {
91
+ if (plan.kind === 'github-event') {
61
92
  try {
62
93
  const event = JSON.parse(node_fs_1.default.readFileSync(process.env.GITHUB_EVENT_PATH, 'utf-8'));
63
94
  const eventBase = event?.pull_request?.base?.sha;
64
95
  // Only use it if the commit is actually present in this clone; otherwise
65
96
  // fall through so a shallow clone degrades to a full scan, not 0 files.
66
- const baseSha = eventBase ? await resolveRef(cwd, eventBase) : undefined;
97
+ const baseSha = eventBase ? await (0, utils_1.resolveGitRef)(cwd, eventBase) : undefined;
67
98
  if (baseSha)
68
- return { baseSha, headSha };
99
+ return { baseSha, headSha, scope: 'delta (PR event)' };
69
100
  }
70
101
  catch {
71
- // Malformed/unreadable event — fall through to the git-based resolution.
102
+ // Malformed/unreadable event — fall through to a full scan below.
72
103
  }
104
+ return { headSha, scope: 'full (PR base unavailable)' };
73
105
  }
74
- // Bitbucket exposes the PR destination branch as an env var (also runner-set).
75
- const bitbucketTarget = process.env.BITBUCKET_PR_DESTINATION_BRANCH;
76
- const ref = bitbucketTarget ? `origin/${bitbucketTarget}` : 'HEAD^';
77
- const baseSha = await resolveRef(cwd, ref);
78
- return { baseSha, headSha };
106
+ const baseSha = await (0, utils_1.resolveGitRef)(cwd, plan.ref);
107
+ return {
108
+ baseSha,
109
+ headSha,
110
+ scope: baseSha ? `delta (${plan.ref}, ${plan.reason})` : `full (${plan.ref} unresolvable)`,
111
+ };
79
112
  }
80
113
  /**
81
114
  * Assemble the shared CheckContext once per run. Language detection is skipped
@@ -84,9 +117,15 @@ async function resolveDiffRange(cwd, opts) {
84
117
  async function buildContext(opts = {}) {
85
118
  const cwd = opts.cwd ?? process.cwd();
86
119
  const config = opts.config ?? (0, loader_1.loadProjectConfig)();
87
- const languages = opts.languages ?? (await (0, detect_language_1.detectLanguages)(cwd));
88
- const { baseSha, headSha } = await resolveDiffRange(cwd, opts);
89
- utils_1.logger.info(`[buildContext]: cwd=${cwd} languages=[${languages.join(', ') || 'none'}] write=${opts.write ?? false}`);
120
+ // Detection reads the client's `ignoreDirs` from the config already loaded here,
121
+ // rather than calling getIgnoredDirs() — that one resolves against process.cwd(),
122
+ // which is not necessarily the `cwd` under review.
123
+ const languages = opts.languages ?? (await (0, detect_language_1.detectLanguages)(cwd, config.ignoreDirs));
124
+ const { baseSha, headSha, scope } = await resolveDiffRange(cwd, opts);
125
+ // `scope` is logged because it decides what the whole run looks at, and getting
126
+ // it wrong is invisible in the results: a run that reviewed one commit and one
127
+ // that reviewed the repository both just print passes.
128
+ utils_1.logger.info(`[buildContext]: cwd=${cwd} languages=[${languages.join(', ') || 'none'}] write=${opts.write ?? false} scope=${scope}`);
90
129
  return {
91
130
  cwd,
92
131
  languages,
@@ -27,7 +27,7 @@ function excludeIgnoredDirs(files, keepDirs = []) {
27
27
  ignored.delete(dir);
28
28
  if (ignored.size === 0)
29
29
  return files;
30
- return files.filter(f => !f.split('/').some(segment => ignored.has(segment)));
30
+ return files.filter(f => !(0, utils_1.isUnderIgnoredDir)(f, ignored));
31
31
  }
32
32
  /**
33
33
  * Lists the files a language check should operate on, matched by pathspec `globs`
@@ -90,6 +90,15 @@ export interface RunOptions {
90
90
  * (`git write-tree`), so every delta-aware check works unchanged.
91
91
  */
92
92
  staged?: boolean;
93
+ /**
94
+ * Review every tracked file instead of a diff (`--full` on the CLI, and the
95
+ * default when no CI environment is detected). Takes precedence over every other
96
+ * range source, including CI variables — see `planDiffRange`. The 16 checks that
97
+ * are about a diff by definition (PR Size, Diff Security, Merge Conflict, …) gate
98
+ * their own `applies` on `baseSha`, so they drop out of the report rather than
99
+ * misreporting.
100
+ */
101
+ full?: boolean;
93
102
  }
94
103
  export interface CheckReport {
95
104
  results: CheckResult[];
package/dist/init.js CHANGED
@@ -11,20 +11,6 @@ const inquirer_1 = __importDefault(require("inquirer"));
11
11
  const config_1 = require("./config");
12
12
  const utils_1 = require("./utils");
13
13
  const CONFIG_FILE = path_1.default.resolve(process.cwd(), 'pr-checkmate.json');
14
- /**
15
- * Load the canonical default ESLint rules (the same set eslint.config.mjs
16
- * applies) so the generated pr-checkmate.json lists every rule for the user to
17
- * change or disable. Falls back to an empty object if the file is unavailable.
18
- */
19
- function loadDefaultEslintRules() {
20
- try {
21
- return JSON.parse(fs_1.default.readFileSync((0, config_1.getPackageConfigPath)('eslint-default-rules.json'), 'utf-8'));
22
- }
23
- catch (err) {
24
- utils_1.logger.warn(`[init]: ⚠️ Could not load default ESLint rules: ${err}`);
25
- return {};
26
- }
27
- }
28
14
  /**
29
15
  * Default configuration structure with customization options
30
16
  */
@@ -48,18 +34,36 @@ const DEFAULT_CONFIG = {
48
34
  // Directories excluded from every check (matched on any path segment). Edit
49
35
  // freely — add your own generated/vendored dirs or remove ones you want scanned.
50
36
  ignoreDirs: config_1.IGNORED_DIRS,
51
- // ESLint — the full default rule set is written out so every rule is visible
52
- // and editable. Change any value, or set a rule to "off" to disable it.
37
+ // ESLint — `rules` starts EMPTY on purpose and holds overrides only, each merged
38
+ // over the bundled default for that one rule.
39
+ //
40
+ // It used to be written out in full (all 27 rules with their severities) so every
41
+ // rule was visible and editable in this file. That copy then won over the bundled
42
+ // defaults forever: `eslint.rules` is in REPLACED_OBJECT_PATHS, so a later `init`
43
+ // keeps the user's block wholesale, and improvements to the defaults never reached
44
+ // a repo that had ever run `init`. Downgrading `prettier/prettier` from error to
45
+ // warn — so formatting stops failing pipelines — is exactly such an improvement,
46
+ // and it stopped at the config boundary for every existing client.
47
+ //
48
+ // The full list with defaults lives in the ESLint check documentation and in the
49
+ // package's own eslint-default-rules.json; discoverability there costs nothing,
50
+ // un-updatable defaults cost every client an edit.
53
51
  eslint: {
54
- rules: loadDefaultEslintRules(),
52
+ rules: {},
55
53
  ignorePatterns: [],
56
54
  },
57
55
  // Prettier customization
58
56
  prettier: {
59
57
  rules: {},
60
58
  },
61
- // Cspell customization
59
+ // Spellcheck (cspell). `enabled: false` turns the check off — that part always
60
+ // applies. The lists feed the cspell config pr-checkmate generates, so they are
61
+ // ignored when the repo has a cspell.json of its own; all of them are appended to
62
+ // the bundled defaults. `ignorePaths` takes globs to skip (e.g. "CHANGELOG.md",
63
+ // "generated/**"), anchored to the repo root so a pattern never matches a
64
+ // directory above the checkout.
62
65
  cspell: {
66
+ enabled: true,
63
67
  words: [],
64
68
  ignorePaths: [],
65
69
  ignoreRegExpList: [],
@@ -19,6 +19,8 @@ export interface CspellConfig {
19
19
  ignorePaths: string[];
20
20
  enableFiletypes: string[];
21
21
  ignoreRegExpList: string[];
22
+ dictionaries?: string[];
23
+ enabled?: boolean;
22
24
  }
23
25
  export interface PRCheckMateConfig {
24
26
  sourcePath: string | string[];
@@ -42,14 +44,24 @@ export interface PRCheckMateConfig {
42
44
  config?: Record<string, any>;
43
45
  };
44
46
  spellcheck?: {
47
+ enabled?: boolean;
45
48
  words?: string[];
46
49
  ignoreWords?: string[];
47
50
  ignorePaths?: string[];
48
51
  };
52
+ cspell?: {
53
+ enabled?: boolean;
54
+ words?: string[];
55
+ ignorePaths?: string[];
56
+ ignoreRegExpList?: string[];
57
+ dictionaries?: string[];
58
+ language?: string;
59
+ };
49
60
  security?: {
50
61
  gitleaks?: {
51
62
  enabled?: boolean;
52
63
  version?: string;
64
+ depth?: number;
53
65
  };
54
66
  'npm-audit'?: {
55
67
  enabled?: boolean;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * True when running inside a CI/CD build.
3
+ *
4
+ * Decides the default review scope: a build looks at the diff (that is the point
5
+ * of a PR gate), a human at a terminal looks at the whole project — the local
6
+ * `HEAD^` default used to check only the last commit, so a repo could report a
7
+ * clean run with a `debugger` sitting in it.
8
+ *
9
+ * @param env Environment to inspect. Defaults to the current process.
10
+ */
11
+ export declare function isCiEnvironment(env?: NodeJS.ProcessEnv): boolean;
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isCiEnvironment = isCiEnvironment;
4
+ /**
5
+ * Environment variables that mean "this is an automated build". `CI` alone covers
6
+ * GitHub Actions, GitLab, Bitbucket, CircleCI, Buildkite, Travis and most others;
7
+ * the rest are providers that either predate the convention or set only their own
8
+ * marker (Jenkins, TeamCity, Azure Pipelines, AWS CodeBuild).
9
+ *
10
+ * This answers a different question from `detectPlatform()` in reporters.ts, which
11
+ * picks WHERE to post a PR comment and therefore only knows the three platforms we
12
+ * can comment on. A Jenkins build is CI but has nowhere to post.
13
+ */
14
+ const CI_ENV_VARS = [
15
+ 'CI',
16
+ 'GITHUB_ACTIONS',
17
+ 'GITLAB_CI',
18
+ 'BITBUCKET_BUILD_NUMBER',
19
+ 'JENKINS_URL',
20
+ 'TEAMCITY_VERSION',
21
+ 'TF_BUILD',
22
+ 'CIRCLECI',
23
+ 'BUILDKITE',
24
+ 'CODEBUILD_BUILD_ID',
25
+ ];
26
+ /** Values that are present but mean "no" — some tooling exports `CI=false`. */
27
+ const FALSY = new Set(['', '0', 'false', 'no', 'off']);
28
+ /**
29
+ * True when running inside a CI/CD build.
30
+ *
31
+ * Decides the default review scope: a build looks at the diff (that is the point
32
+ * of a PR gate), a human at a terminal looks at the whole project — the local
33
+ * `HEAD^` default used to check only the last commit, so a repo could report a
34
+ * clean run with a `debugger` sitting in it.
35
+ *
36
+ * @param env Environment to inspect. Defaults to the current process.
37
+ */
38
+ function isCiEnvironment(env = process.env) {
39
+ return CI_ENV_VARS.some(name => {
40
+ const value = env[name];
41
+ return value !== undefined && !FALSY.has(value.trim().toLowerCase());
42
+ });
43
+ }
@@ -4,7 +4,12 @@ export type Language = 'typescript' | 'javascript' | 'python' | 'cpp' | 'swift'
4
4
  * with tracked source-file counts (breadth). A language is reported when it has a
5
5
  * marker file OR at least one tracked source file.
6
6
  *
7
+ * @param root Repository root.
8
+ * @param ignoreDirs Directory names whose contents do not count as the repo's own
9
+ * code. Defaults to the curated list; `buildContext` passes the client's
10
+ * `ignoreDirs` so a repo that vendors dependencies is described by its own
11
+ * sources. Marker files are unaffected — they are only ever read from `root`.
7
12
  * @returns languages ordered by tracked file count (most-used first). `.h` headers
8
13
  * are intentionally excluded from the C++ count since they are ambiguous with C.
9
14
  */
10
- export declare function detectLanguages(root?: string): Promise<Language[]>;
15
+ export declare function detectLanguages(root?: string, ignoreDirs?: string[]): Promise<Language[]>;
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.detectLanguages = detectLanguages;
7
7
  const node_fs_1 = __importDefault(require("node:fs"));
8
8
  const node_path_1 = __importDefault(require("node:path"));
9
+ const constants_1 = require("../config/constants");
9
10
  const execa_1 = require("./execa");
10
11
  const glob_1 = require("./glob");
11
12
  const logger_1 = require("./logger");
@@ -70,7 +71,7 @@ function hasMarker(root, lang) {
70
71
  * when git is unavailable or the directory is not a repo — callers then fall
71
72
  * back to marker files alone.
72
73
  */
73
- async function countTrackedFiles(root) {
74
+ async function countTrackedFiles(root, ignoreDirs) {
74
75
  const counts = {
75
76
  typescript: 0,
76
77
  javascript: 0,
@@ -87,9 +88,18 @@ async function countTrackedFiles(root) {
87
88
  try {
88
89
  const { stdout } = await (0, execa_1.execa)('git', ['ls-files'], { cwd: root });
89
90
  const files = stdout.split('\n').map(f => f.trim());
91
+ const ignored = new Set(ignoreDirs);
90
92
  for (const file of files) {
91
93
  if (!file)
92
94
  continue;
95
+ // A repository that commits its dependencies (`node_modules`, `vendor`,
96
+ // `third_party`) would otherwise be detected as a Python/Go/PHP project off
97
+ // the back of files nobody in it wrote, switching on whole toolchains for
98
+ // languages absent from the source. Detection has to describe the repo's own
99
+ // code, since it decides which checks run at all — the same exclusion every
100
+ // check's file list already applies via `excludeIgnoredDirs`.
101
+ if ((0, glob_1.isUnderIgnoredDir)(file, ignored))
102
+ continue;
93
103
  const ext = node_path_1.default.extname(file).toLowerCase();
94
104
  for (const lang of Object.keys(EXTENSIONS)) {
95
105
  if (EXTENSIONS[lang].includes(ext)) {
@@ -109,11 +119,16 @@ async function countTrackedFiles(root) {
109
119
  * with tracked source-file counts (breadth). A language is reported when it has a
110
120
  * marker file OR at least one tracked source file.
111
121
  *
122
+ * @param root Repository root.
123
+ * @param ignoreDirs Directory names whose contents do not count as the repo's own
124
+ * code. Defaults to the curated list; `buildContext` passes the client's
125
+ * `ignoreDirs` so a repo that vendors dependencies is described by its own
126
+ * sources. Marker files are unaffected — they are only ever read from `root`.
112
127
  * @returns languages ordered by tracked file count (most-used first). `.h` headers
113
128
  * are intentionally excluded from the C++ count since they are ambiguous with C.
114
129
  */
115
- async function detectLanguages(root = process.cwd()) {
116
- const counts = await countTrackedFiles(root);
130
+ async function detectLanguages(root = process.cwd(), ignoreDirs = constants_1.IGNORED_DIRS) {
131
+ const counts = await countTrackedFiles(root, ignoreDirs);
117
132
  const detected = Object.keys(EXTENSIONS).filter(lang => hasMarker(root, lang) || counts[lang] > 0);
118
133
  detected.sort((a, b) => counts[b] - counts[a]);
119
134
  const countStr = Object.keys(counts).map(l => `${l}=${counts[l]}`).join(', ');
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Resolve a git ref to a concrete commit SHA, or `undefined` when this clone
3
+ * cannot resolve it.
4
+ *
5
+ * `--verify --quiet` emits nothing (rather than echoing the literal ref) and exits
6
+ * non-zero for a missing ref, so the caller gets a clear "not here" instead of a
7
+ * string that looks like a ref but resolves to nothing. Refs go missing more often
8
+ * than it seems: `HEAD^` on a repository's first commit, and any ancestor in a
9
+ * shallow CI clone — `actions/checkout` fetches depth 1 by default.
10
+ *
11
+ * Every caller must degrade rather than fail when this returns `undefined`: a
12
+ * missing base means "review everything", never "review nothing".
13
+ */
14
+ export declare function resolveGitRef(cwd: string, ref: string): Promise<string | undefined>;
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveGitRef = resolveGitRef;
4
+ const execa_1 = require("./execa");
5
+ /**
6
+ * Resolve a git ref to a concrete commit SHA, or `undefined` when this clone
7
+ * cannot resolve it.
8
+ *
9
+ * `--verify --quiet` emits nothing (rather than echoing the literal ref) and exits
10
+ * non-zero for a missing ref, so the caller gets a clear "not here" instead of a
11
+ * string that looks like a ref but resolves to nothing. Refs go missing more often
12
+ * than it seems: `HEAD^` on a repository's first commit, and any ancestor in a
13
+ * shallow CI clone — `actions/checkout` fetches depth 1 by default.
14
+ *
15
+ * Every caller must degrade rather than fail when this returns `undefined`: a
16
+ * missing base means "review everything", never "review nothing".
17
+ */
18
+ async function resolveGitRef(cwd, ref) {
19
+ const { stdout } = await (0, execa_1.execa)('git', ['rev-parse', '--verify', '--quiet', `${ref}^{commit}`], {
20
+ cwd,
21
+ reject: false,
22
+ });
23
+ return stdout.trim() || undefined;
24
+ }
@@ -8,3 +8,17 @@
8
8
  export declare function globToRegExp(glob: string): RegExp;
9
9
  /** True when `file` matches any of the glob `patterns`. */
10
10
  export declare function matchesAnyGlob(file: string, patterns: string[]): boolean;
11
+ /**
12
+ * True when any segment of a repo-relative path names an excluded directory —
13
+ * `node_modules/x/y.py` for `node_modules`, at any nesting depth.
14
+ *
15
+ * Shared so the two consumers cannot drift: the file list every check operates on
16
+ * (`excludeIgnoredDirs` in core/targets.ts) and language detection, which counts
17
+ * tracked source files. They disagreed once — detection counted a repository that
18
+ * commits its dependencies as a Python/Go/PHP project and switched on the checks
19
+ * for languages nobody had written a line of.
20
+ *
21
+ * @param file Repo-relative path, forward slashes.
22
+ * @param ignoredDirs Directory NAMES (not globs) to exclude.
23
+ */
24
+ export declare function isUnderIgnoredDir(file: string, ignoredDirs: Iterable<string>): boolean;
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.globToRegExp = globToRegExp;
4
4
  exports.matchesAnyGlob = matchesAnyGlob;
5
+ exports.isUnderIgnoredDir = isUnderIgnoredDir;
5
6
  /**
6
7
  * Minimal glob → RegExp for path matching against config patterns. Supports `*`
7
8
  * (matches within a path segment), `**` (across segments), and a leading `** /`
@@ -23,3 +24,22 @@ function globToRegExp(glob) {
23
24
  function matchesAnyGlob(file, patterns) {
24
25
  return patterns.some(p => globToRegExp(p).test(file));
25
26
  }
27
+ /**
28
+ * True when any segment of a repo-relative path names an excluded directory —
29
+ * `node_modules/x/y.py` for `node_modules`, at any nesting depth.
30
+ *
31
+ * Shared so the two consumers cannot drift: the file list every check operates on
32
+ * (`excludeIgnoredDirs` in core/targets.ts) and language detection, which counts
33
+ * tracked source files. They disagreed once — detection counted a repository that
34
+ * commits its dependencies as a Python/Go/PHP project and switched on the checks
35
+ * for languages nobody had written a line of.
36
+ *
37
+ * @param file Repo-relative path, forward slashes.
38
+ * @param ignoredDirs Directory NAMES (not globs) to exclude.
39
+ */
40
+ function isUnderIgnoredDir(file, ignoredDirs) {
41
+ const ignored = ignoredDirs instanceof Set ? ignoredDirs : new Set(ignoredDirs);
42
+ if (ignored.size === 0)
43
+ return false;
44
+ return file.split('/').some(segment => ignored.has(segment));
45
+ }
@@ -6,6 +6,8 @@ export * from './direct-deps';
6
6
  export * from './detect-language';
7
7
  export * from './glob';
8
8
  export * from './update-notifier';
9
+ export * from './ci';
10
+ export * from './git';
9
11
  /**
10
12
  *
11
13
  */
@@ -28,6 +28,8 @@ __exportStar(require("./direct-deps"), exports);
28
28
  __exportStar(require("./detect-language"), exports);
29
29
  __exportStar(require("./glob"), exports);
30
30
  __exportStar(require("./update-notifier"), exports);
31
+ __exportStar(require("./ci"), exports);
32
+ __exportStar(require("./git"), exports);
31
33
  /**
32
34
  *
33
35
  */
@@ -13,13 +13,13 @@
13
13
  "space-before-function-paren": "off",
14
14
  "operator-linebreak": "off",
15
15
  "max-len": ["warn", { "code": 100 }],
16
- "semi": ["error", "always"],
17
- "quotes": ["error", "single", { "avoidEscape": true }],
18
- "object-curly-spacing": ["error", "always"],
19
- "space-infix-ops": "error",
20
- "keyword-spacing": ["error", { "before": true, "after": true }],
16
+ "semi": ["warn", "always"],
17
+ "quotes": ["warn", "single", { "avoidEscape": true }],
18
+ "object-curly-spacing": ["warn", "always"],
19
+ "space-infix-ops": "warn",
20
+ "keyword-spacing": ["warn", { "before": true, "after": true }],
21
21
  "padding-line-between-statements": [
22
- "error",
22
+ "warn",
23
23
  { "blankLine": "always", "prev": "block", "next": "*" },
24
24
  { "blankLine": "always", "prev": "*", "next": "return" }
25
25
  ],
@@ -50,7 +50,7 @@
50
50
  "jsdoc/check-tag-names": "warn",
51
51
  "jsdoc/check-types": "warn",
52
52
  "prettier/prettier": [
53
- "error",
53
+ "warn",
54
54
  {
55
55
  "semi": true,
56
56
  "singleQuote": true,
package/eslint.config.mjs CHANGED
@@ -12,6 +12,22 @@ import jsdocPlugin from 'eslint-plugin-jsdoc';
12
12
  * Default ESLint rules — single source of truth, shared with init.ts so the
13
13
  * pr-checkmate.json it generates lists exactly the rules applied here. Edit the
14
14
  * rules in `eslint-default-rules.json` (shipped alongside this file).
15
+ *
16
+ * SEVERITY POLICY: `error` is reserved for defects a human has to resolve —
17
+ * `no-debugger`, `eqeqeq`, `prefer-const`, `@typescript-eslint/no-unused-vars`,
18
+ * `@typescript-eslint/no-floating-promises`, `import/no-duplicates`. Everything
19
+ * that is purely formatting is `warn`, even
20
+ * though it is auto-fixable, precisely BECAUSE it is auto-fixable: ESLint exits 1
21
+ * on errors only, and the ESLint check maps exit 1 to a blocking failure, so an
22
+ * `error`-level whitespace rule turns `npx prettier --write` worth of work into a
23
+ * red gate on someone's PR. Formatting still gets reported — as a warning here and
24
+ * by the dedicated Prettier check, which has always been advisory. A repo that
25
+ * wants formatting to gate promotes it back with
26
+ * `eslint.rules: { "prettier/prettier": "error" }` or
27
+ * `severity: { "Prettier": "error" }` in pr-checkmate.json.
28
+ *
29
+ * A JSON file cannot carry this note, hence it lives here; `src/config/.eslintrc.js`
30
+ * holds the same rule set for the legacy ESLint <9 path and must stay in sync.
15
31
  */
16
32
  const require = createRequire(import.meta.url);
17
33
  export const defaultRules = require('./eslint-default-rules.json');
@@ -34,7 +50,7 @@ const defaultIgnores = [
34
50
  function loadUserConfig() {
35
51
  let currentDir = process.cwd();
36
52
  let configPath = path.join(currentDir, 'pr-checkmate.json');
37
-
53
+
38
54
  // If running from node_modules, find project root
39
55
  if (currentDir.includes('node_modules')) {
40
56
  const parts = currentDir.split(path.sep);
@@ -44,7 +60,7 @@ function loadUserConfig() {
44
60
  configPath = path.join(currentDir, 'pr-checkmate.json');
45
61
  }
46
62
  }
47
-
63
+
48
64
  if (fs.existsSync(configPath)) {
49
65
  try {
50
66
  const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
@@ -56,7 +72,7 @@ function loadUserConfig() {
56
72
  } else {
57
73
  console.error(`[loadUserConfig]: No pr-checkmate.json found, using defaults`);
58
74
  }
59
-
75
+
60
76
  return { sourcePath: 'src', eslint: {} };
61
77
  }
62
78
 
@@ -117,4 +133,4 @@ export default [
117
133
 
118
134
  rules: mergedRules,
119
135
  },
120
- ];
136
+ ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pr-checkmate",
3
- "version": "1.23.19",
3
+ "version": "1.23.21",
4
4
  "description": "Automated PR quality checks: linting, formatting, dependency analysis, and spellcheck",
5
5
  "keywords": [
6
6
  "github-actions",
@@ -51,6 +51,7 @@
51
51
  "docs": "typedoc",
52
52
  "scan:secrets": "gitleaks-secret-scanner --diff-mode ci --html-report gitleaks-report.html",
53
53
  "test": "node --require ts-node/register --test \"src/**/*.test.ts\"",
54
+ "verify:tarball": "bash scripts/verify-tarball.sh",
54
55
  "security-scan": "npx pr-checkmate security",
55
56
  "clean": "rm -rf dist",
56
57
  "release": "standard-version"