pr-checkmate 1.23.20 → 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.
package/.eslintrc.js CHANGED
@@ -70,13 +70,15 @@ module.exports = {
70
70
  'space-before-function-paren': 'off',
71
71
  'operator-linebreak': 'off',
72
72
  'max-len': ['warn', { code: 100 }],
73
- semi: ['error', 'always'],
74
- quotes: ['error', 'single', { avoidEscape: true }],
75
- 'object-curly-spacing': ['error', 'always'],
76
- 'space-infix-ops': 'error',
77
- 'keyword-spacing': ['error', { before: true, after: true }],
73
+ // Formatting is advisory — see the note above `rules` in eslint-flat-config.mjs.
74
+ // Keep these in sync with src/config/eslint-default-rules.json.
75
+ semi: ['warn', 'always'],
76
+ quotes: ['warn', 'single', { avoidEscape: true }],
77
+ 'object-curly-spacing': ['warn', 'always'],
78
+ 'space-infix-ops': 'warn',
79
+ 'keyword-spacing': ['warn', { before: true, after: true }],
78
80
  'padding-line-between-statements': [
79
- 'error',
81
+ 'warn',
80
82
  { blankLine: 'always', prev: 'block', next: '*' },
81
83
  { blankLine: 'always', prev: '*', next: 'return' },
82
84
  ],
@@ -108,7 +110,7 @@ module.exports = {
108
110
  'jsdoc/check-tag-names': 'warn',
109
111
  'jsdoc/check-types': 'warn',
110
112
  'prettier/prettier': [
111
- 'error',
113
+ 'warn',
112
114
  {
113
115
  semi: true,
114
116
  singleQuote: true,
package/README.md CHANGED
@@ -29,8 +29,8 @@ every PR.
29
29
  - One package, any stack. Language is auto-detected from the repo.
30
30
  - Bundled tooling. ESLint, Prettier, Ruff, clang-format, and gitleaks run inside the
31
31
  package (WASM/JS), so clients install nothing for those languages.
32
- - Delta mode. In a PR only the changed files are checked; a full-project scan is the
33
- fallback outside PR context.
32
+ - Delta mode. A PR run checks only the changed files; outside CI the default is every tracked
33
+ file. `--full` forces that anywhere, and a run that cannot resolve its base falls back to it.
34
34
  - Parallel phases. Checks run concurrently within each phase: blocking, then
35
35
  informational, then format.
36
36
  - Reports in the PR. Results post as a single comment that updates in place, and
@@ -79,6 +79,24 @@ Run all checks:
79
79
  npx pr-checkmate all
80
80
  ```
81
81
 
82
+ Scope depends on where the command runs. In CI it reviews the pull request diff, taking the
83
+ base SHA from the GitHub `pull_request` event or Bitbucket's destination branch; a CI build
84
+ with no PR attached falls back to `HEAD^`. Locally, with no CI environment detected, it
85
+ reviews every tracked file. It used to fall back to `HEAD^` there too, so a local run saw
86
+ only the last commit and would report a clean project with a `debugger` committed the day
87
+ before. `--full` forces the whole-repo review anywhere, including inside a CI build, and it
88
+ composes with any command:
89
+
90
+ ```bash
91
+ npx pr-checkmate all --full
92
+ npx pr-checkmate lint --full
93
+ ```
94
+
95
+ The 16 checks that only make sense on a diff (PR Size, Diff Security, Merge Conflict, Custom
96
+ Rules, Banned Imports, the git-diff family) drop out of the report on a full scan. The
97
+ [per-check reference](https://github.com/Mybono/pr_checkmate_public/blob/main/documentation/checks/INDEX.md)
98
+ spells out the rules under "Review scope".
99
+
82
100
  ### Running in CI without installing
83
101
 
84
102
  Plenty of repos never add pr-checkmate to `package.json` and call it on the fly in CI
package/cspell.json CHANGED
@@ -903,6 +903,7 @@
903
903
  "riskfolio",
904
904
  "robolectric",
905
905
  "rowwise",
906
+ "rseidelsohn",
906
907
  "Rshum",
907
908
  "rtol",
908
909
  "rtype",
@@ -1010,6 +1011,7 @@
1010
1011
  "taxinfo",
1011
1012
  "Tayebeh",
1012
1013
  "tcwf",
1014
+ "teamcity",
1013
1015
  "telaviv",
1014
1016
  "temurin",
1015
1017
  "Tenés",
package/dist/cli.js CHANGED
@@ -56,8 +56,51 @@ const COMMAND_TO_CHECK = {
56
56
  };
57
57
  /** Commands that run the whole pipeline (or no pipeline at all) and take no siblings. */
58
58
  const STANDALONE_COMMANDS = ['init', 'all', 'precommit'];
59
- const args = process.argv.slice(2);
59
+ /** Flags accepted anywhere on the command line, with what each one means. */
60
+ const FLAGS = {
61
+ '--full': 'review every tracked file instead of the diff (the default outside CI)',
62
+ };
63
+ const argv = process.argv.slice(2);
64
+ /**
65
+ * Flags are separated from positional arguments so they compose with every command
66
+ * — scope is orthogonal to which checks run, hence `pr-checkmate lint --full` as
67
+ * well as `pr-checkmate all --full`.
68
+ */
69
+ const flags = argv.filter(arg => arg.startsWith('-'));
70
+ const args = argv.filter(arg => !arg.startsWith('-'));
60
71
  const cmd = args[0];
72
+ /**
73
+ * An unrecognised flag aborts the run. Ignoring it would report success for a scope
74
+ * the user did not ask for: a misspelling of `--full` must not quietly review the
75
+ * diff and print a green summary.
76
+ */
77
+ function rejectUnknownFlags() {
78
+ const unknown = flags.filter(flag => !FLAGS[flag]);
79
+ if (unknown.length === 0)
80
+ return;
81
+ const known = Object.entries(FLAGS)
82
+ .map(([flag, help]) => `${flag} (${help})`)
83
+ .join(', ');
84
+ utils_1.logger.error(`[cli]: Unknown flag(s) "${unknown.join('", "')}". Available: ${known}`);
85
+ process.exit(1);
86
+ }
87
+ /**
88
+ * `--full` and `precommit` ask for different things — every tracked file versus the
89
+ * staged changes only. `planDiffRange` resolves the conflict in favour of `--full`,
90
+ * which would silently turn a pre-commit hook into a whole-repo scan, so the
91
+ * contradiction is refused here instead.
92
+ */
93
+ function rejectConflictingScope() {
94
+ if (cmd === 'precommit' && flags.includes('--full')) {
95
+ utils_1.logger.error('[cli]: "precommit --full" is contradictory — precommit reviews the staged changes. Run "pr-checkmate all --full" for a whole-repo review.');
96
+ process.exit(1);
97
+ }
98
+ if (cmd === 'init' && flags.length > 0) {
99
+ utils_1.logger.error(`[cli]: "init" takes no flags, got: ${flags.join(', ')}.`);
100
+ process.exit(1);
101
+ }
102
+ }
103
+ const full = flags.includes('--full');
61
104
  /**
62
105
  * Reject extra arguments after a standalone command. Silently ignoring them would
63
106
  * report success for work that never ran — `pr-checkmate init lint` must not look
@@ -75,6 +118,8 @@ function rejectExtraArgs() {
75
118
  }
76
119
  void (async () => {
77
120
  try {
121
+ rejectUnknownFlags();
122
+ rejectConflictingScope();
78
123
  rejectExtraArgs();
79
124
  if (cmd === 'init') {
80
125
  await (0, init_1.init)();
@@ -86,7 +131,7 @@ void (async () => {
86
131
  // Fetch the update notice concurrently (best-effort, never blocks/affects
87
132
  // the exit code), but print it LAST — after the summary — so it's noticeable.
88
133
  const noticePromise = (0, utils_1.getUpdateNotice)();
89
- const report = await (0, core_1.runChecks)({ reporter: 'auto', write: true });
134
+ const report = await (0, core_1.runChecks)({ reporter: 'auto', write: true, full });
90
135
  const notice = await noticePromise.catch(() => null);
91
136
  if (notice) {
92
137
  utils_1.logger.info('');
@@ -109,7 +154,7 @@ void (async () => {
109
154
  process.exit(1);
110
155
  }
111
156
  const checkNames = [...new Set(args.map(arg => COMMAND_TO_CHECK[arg]))];
112
- const report = await (0, core_1.runChecks)({ only: checkNames, reporter: 'stdout', write: true });
157
+ const report = await (0, core_1.runChecks)({ only: checkNames, reporter: 'stdout', write: true, full });
113
158
  process.exit(report.ok ? 0 : 1);
114
159
  }
115
160
  catch (err) {
@@ -55,6 +55,7 @@ export declare const SOURCE_EXTENSIONS: {
55
55
  export declare const DEFAULT_COMMANDS: {
56
56
  'npx pr-checkmate init': string;
57
57
  'npx pr-checkmate all': string;
58
+ 'npx pr-checkmate all --full': string;
58
59
  'npx pr-checkmate lint': string;
59
60
  'npx pr-checkmate prettier': string;
60
61
  'npx pr-checkmate deps': string;
@@ -181,7 +181,8 @@ exports.SOURCE_EXTENSIONS = {
181
181
  };
182
182
  exports.DEFAULT_COMMANDS = {
183
183
  'npx pr-checkmate init': 'Update pr-checkmate.json',
184
- 'npx pr-checkmate all': 'Run all checks',
184
+ 'npx pr-checkmate all': 'Run all checks (the PR diff in CI, every tracked file locally)',
185
+ 'npx pr-checkmate all --full': 'Run all checks over every tracked file, CI included',
185
186
  'npx pr-checkmate lint': 'Lint code using ESLint',
186
187
  'npx pr-checkmate prettier': 'Format code using Prettier',
187
188
  'npx pr-checkmate deps': 'Check project dependencies',
@@ -57,6 +57,13 @@ exports.lintCheck = {
57
57
  // parser, breaking TypeScript linting. Only pass it on the legacy path.
58
58
  if (version < 9)
59
59
  args.push('--ext', '.ts,.tsx,.js,.jsx');
60
+ // Files are named explicitly, so any of them that the config ignores produces a
61
+ // "File ignored because of a matching ignore pattern" warning. That is a fact
62
+ // about the config, not about the client's code, and a whole-repo run surfaces
63
+ // one per ignored-but-tracked file (`.eslintrc.js`, generated sources). The flag
64
+ // suppresses the warning only — nothing changes about which files are linted.
65
+ if (version >= 9)
66
+ args.push('--no-warn-ignored');
60
67
  // Only auto-fix when the caller allows file mutations.
61
68
  if (ctx.write)
62
69
  args.push('--fix');
@@ -63,9 +63,20 @@ exports.commitlintCheck = {
63
63
  const commitlintBin = (0, utils_1.resolvePackageBin)('@commitlint/cli', 'cli.js');
64
64
  const hasLocal = (0, config_1.hasLocalConfig)(LOCAL_CONFIG_FILES);
65
65
  let tempConfigFile = null;
66
- const from = ctx.baseSha ?? 'HEAD~1';
67
- const to = ctx.headSha ?? 'HEAD';
68
- const args = ['--from', from, '--to', to, '--verbose'];
66
+ // With no diff range (a full-repo review, or `--full`) the check still has a
67
+ // meaningful subject: the newest commit message. `HEAD~1` cannot be assumed to
68
+ // exist, though — it is absent on a repository's first commit and in a shallow
69
+ // clone (`actions/checkout` fetches depth 1 by default). Passing it blind made
70
+ // commitlint die with an unhandled "ambiguous argument 'HEAD~1..HEAD'" stack
71
+ // trace in the client's log, and the check then reported "commit message
72
+ // violations found" — a verdict about code quality for what was a crash.
73
+ // `--last` lints exactly the newest commit and needs no ancestor.
74
+ const range = ctx.baseSha
75
+ ? ['--from', ctx.baseSha, '--to', ctx.headSha ?? 'HEAD']
76
+ : (await (0, utils_1.resolveGitRef)(ctx.cwd, 'HEAD~1'))
77
+ ? ['--from', 'HEAD~1', '--to', ctx.headSha ?? 'HEAD']
78
+ : ['--last'];
79
+ const args = [...range, '--verbose'];
69
80
  try {
70
81
  if (hasLocal) {
71
82
  utils_1.logger.info('[Commitlint]: Using local config');
@@ -1,4 +1,11 @@
1
1
  import { Check } from '../../types';
2
+ /**
3
+ * True when a parser problem describes something actually wrong with the file —
4
+ * a syntax error, a duplicate key, a tab used for indentation.
5
+ */
6
+ export declare function isFileDefect(problem: {
7
+ code?: string;
8
+ }): boolean;
2
9
  /**
3
10
  * Bundled YAML linter (the `yaml` package — no runner dependency, always
4
11
  * available like Markdown/Spellcheck). Parses every changed `.yml`/`.yaml` file
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.yamlLintCheck = void 0;
7
+ exports.isFileDefect = isFileDefect;
7
8
  const node_fs_1 = __importDefault(require("node:fs"));
8
9
  const node_path_1 = __importDefault(require("node:path"));
9
10
  const yaml_1 = require("yaml");
@@ -15,6 +16,28 @@ const findings_1 = require("../findings");
15
16
  function shortMessage(message) {
16
17
  return message.split('\n')[0].trim();
17
18
  }
19
+ /**
20
+ * Parser problems that are not defects in the file.
21
+ *
22
+ * `TAG_RESOLVE_FAILED` means the parser met an application-specific tag it has no
23
+ * schema for. YAML is explicitly extensible there, so the document is valid and the
24
+ * parser simply is not the application that defines the tag: `!Ref`, `!Sub`,
25
+ * `!GetAtt`, `!Equals` in CloudFormation and SAM templates, `!include` in Home
26
+ * Assistant, `!vault` in Ansible. Reporting those calls a whole ecosystem's valid
27
+ * files broken — one CloudFormation template produced **557 blocking findings** for
28
+ * a client, none of which was a real problem.
29
+ *
30
+ * Matched on the parser's `code`, not on message text, so a wording change upstream
31
+ * cannot silently re-enable it.
32
+ */
33
+ const NON_DEFECT_CODES = new Set(['TAG_RESOLVE_FAILED']);
34
+ /**
35
+ * True when a parser problem describes something actually wrong with the file —
36
+ * a syntax error, a duplicate key, a tab used for indentation.
37
+ */
38
+ function isFileDefect(problem) {
39
+ return problem.code === undefined || !NON_DEFECT_CODES.has(problem.code);
40
+ }
18
41
  /**
19
42
  * Bundled YAML linter (the `yaml` package — no runner dependency, always
20
43
  * available like Markdown/Spellcheck). Parses every changed `.yml`/`.yaml` file
@@ -60,7 +83,7 @@ exports.yamlLintCheck = {
60
83
  // so a single malformed file cannot abort the whole check.
61
84
  const docs = (0, yaml_1.parseAllDocuments)(source, { prettyErrors: true });
62
85
  for (const doc of docs) {
63
- for (const problem of [...doc.errors, ...doc.warnings]) {
86
+ for (const problem of [...doc.errors, ...doc.warnings].filter(isFileDefect)) {
64
87
  const pos = problem.linePos?.[0];
65
88
  issues.push({
66
89
  file,
@@ -39,10 +39,30 @@ exports.securityCheck = {
39
39
  // times out in locked-down/offline CI (the scanner already falls back to
40
40
  // this exact version). Overridable via `security.gitleaks.version`.
41
41
  const gitleaksVersion = ctx.config.security?.gitleaks?.version ?? '8.30.0';
42
- const args = ['--diff-mode', 'ci', '--gitleaks-version', gitleaksVersion];
42
+ // The scan scope has to follow the run's scope. `--diff-mode ci` REQUIRES
43
+ // HEAD_SHA and aborts without it, so on a full-scan run (no diff range — the
44
+ // local default) it exited 2 and the secret scan silently became a warning
45
+ // about the scanner instead of a review of the code. `history` is the only
46
+ // whole-repository mode the scanner offers; it walks the full commit history,
47
+ // which took ~1s over 700 commits here but is worth bounding on a large
48
+ // monorepo via `security.gitleaks.depth`.
49
+ const isDelta = Boolean(ctx.baseSha);
50
+ const depth = ctx.config.security?.gitleaks?.depth;
51
+ // History mode needs a repository to walk. Without one the scanner exits 2
52
+ // after printing `fatal: not a git repository` and "An unexpected error
53
+ // occurred" — alarming output about the tool, for a directory it was never
54
+ // able to review. Every other check skips when git is unusable; so does this.
55
+ if (!isDelta && !(await (0, utils_1.resolveGitRef)(ctx.cwd, 'HEAD'))) {
56
+ return (0, types_1.skip)('git history unavailable');
57
+ }
58
+ const args = isDelta
59
+ ? ['--diff-mode', 'ci']
60
+ : ['--diff-mode', 'history', ...(depth ? ['--depth', String(depth)] : [])];
61
+ args.push('--gitleaks-version', gitleaksVersion);
43
62
  if (!hasClientConfig) {
44
63
  args.push('--config', (0, config_1.getPackageConfigPath)('gitleaks.toml'));
45
64
  }
65
+ utils_1.logger.info(`[Security Scan]: scanning ${isDelta ? 'the diff range' : `full history${depth ? ` (last ${depth} commits)` : ''}`}`);
46
66
  const env = {
47
67
  ...process.env,
48
68
  ...(ctx.headSha ? { HEAD_SHA: ctx.headSha } : {}),
@@ -1,6 +1,53 @@
1
1
  import type { CheckContext, RunOptions } from './types';
2
+ /**
3
+ * What a run should look at, before any git call is made. Separated from
4
+ * {@link resolveDiffRange} so the precedence rules are a pure function of the
5
+ * options and the environment — the part worth pinning with tests.
6
+ *
7
+ * - `full` — every tracked file. Checks fall back to `git ls-files` when the range
8
+ * is undefined, so this is expressed by resolving to no SHAs at all.
9
+ * - `staged` — index vs HEAD (pre-commit).
10
+ * - `explicit` — SHAs the caller supplied.
11
+ * - `github-event` — base SHA from the pull_request event payload.
12
+ * - `ref` — diff against a named ref (a PR's destination branch, or `HEAD^`).
13
+ */
14
+ type RangePlan = {
15
+ kind: 'full';
16
+ reason: string;
17
+ } | {
18
+ kind: 'staged';
19
+ } | {
20
+ kind: 'explicit';
21
+ baseSha?: string;
22
+ headSha?: string;
23
+ } | {
24
+ kind: 'github-event';
25
+ } | {
26
+ kind: 'ref';
27
+ ref: string;
28
+ reason: string;
29
+ };
30
+ /**
31
+ * Decide what to look at. Order matters and is the contract:
32
+ *
33
+ * 1. `full` wins over everything — an explicit "check all of it" must not be
34
+ * quietly narrowed by a CI variable that happens to be set.
35
+ * 2. `staged`, then caller-supplied SHAs.
36
+ * 3. The CI platform's PR context (GitHub event payload, Bitbucket destination
37
+ * branch) — a gate exists to judge the diff.
38
+ * 4. A CI build with no PR context still diffs `HEAD^`: it is a push/branch build,
39
+ * and delta is why CI runs are fast.
40
+ * 5. Anything else is a human at a terminal → full scan. `HEAD^` used to be the
41
+ * fallback here, which meant a local `pr-checkmate all` judged only the last
42
+ * commit and could report a clean project with a `debugger` committed earlier.
43
+ *
44
+ * @param opts Run options as passed to `runChecks`.
45
+ * @param env Environment to read CI markers from. Defaults to the current process.
46
+ */
47
+ export declare function planDiffRange(opts: RunOptions, env?: NodeJS.ProcessEnv): RangePlan;
2
48
  /**
3
49
  * Assemble the shared CheckContext once per run. Language detection is skipped
4
50
  * when the caller supplies `languages` (e.g. PRValidator passes its ProjectType).
5
51
  */
6
52
  export declare function buildContext(opts?: RunOptions): Promise<CheckContext>;
53
+ export {};
@@ -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,10 +34,22 @@ 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
@@ -61,6 +61,7 @@ export interface PRCheckMateConfig {
61
61
  gitleaks?: {
62
62
  enabled?: boolean;
63
63
  version?: string;
64
+ depth?: number;
64
65
  };
65
66
  'npm-audit'?: {
66
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.20",
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"
@@ -108,7 +108,12 @@
108
108
  "type": "object",
109
109
  "properties": {
110
110
  "enabled": { "type": "boolean" },
111
- "version": { "type": "string" }
111
+ "version": { "type": "string" },
112
+ "depth": {
113
+ "description": "Caps the whole-repository history scan used when there is no diff range (a local run or --full) to the last N commits. Ignored in a PR run, which scans the diff range.",
114
+ "type": "integer",
115
+ "minimum": 1
116
+ }
112
117
  }
113
118
  },
114
119
  "npm-audit": {
@@ -70,13 +70,15 @@ module.exports = {
70
70
  'space-before-function-paren': 'off',
71
71
  'operator-linebreak': 'off',
72
72
  'max-len': ['warn', { code: 100 }],
73
- semi: ['error', 'always'],
74
- quotes: ['error', 'single', { avoidEscape: true }],
75
- 'object-curly-spacing': ['error', 'always'],
76
- 'space-infix-ops': 'error',
77
- 'keyword-spacing': ['error', { before: true, after: true }],
73
+ // Formatting is advisory — see the note above `rules` in eslint-flat-config.mjs.
74
+ // Keep these in sync with src/config/eslint-default-rules.json.
75
+ semi: ['warn', 'always'],
76
+ quotes: ['warn', 'single', { avoidEscape: true }],
77
+ 'object-curly-spacing': ['warn', 'always'],
78
+ 'space-infix-ops': 'warn',
79
+ 'keyword-spacing': ['warn', { before: true, after: true }],
78
80
  'padding-line-between-statements': [
79
- 'error',
81
+ 'warn',
80
82
  { blankLine: 'always', prev: 'block', next: '*' },
81
83
  { blankLine: 'always', prev: '*', next: 'return' },
82
84
  ],
@@ -108,7 +110,7 @@ module.exports = {
108
110
  'jsdoc/check-tag-names': 'warn',
109
111
  'jsdoc/check-types': 'warn',
110
112
  'prettier/prettier': [
111
- 'error',
113
+ 'warn',
112
114
  {
113
115
  semi: true,
114
116
  singleQuote: true,
@@ -903,6 +903,7 @@
903
903
  "riskfolio",
904
904
  "robolectric",
905
905
  "rowwise",
906
+ "rseidelsohn",
906
907
  "Rshum",
907
908
  "rtol",
908
909
  "rtype",
@@ -1010,6 +1011,7 @@
1010
1011
  "taxinfo",
1011
1012
  "Tayebeh",
1012
1013
  "tcwf",
1014
+ "teamcity",
1013
1015
  "telaviv",
1014
1016
  "temurin",
1015
1017
  "Tenés",
@@ -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,
@@ -108,7 +108,12 @@
108
108
  "type": "object",
109
109
  "properties": {
110
110
  "enabled": { "type": "boolean" },
111
- "version": { "type": "string" }
111
+ "version": { "type": "string" },
112
+ "depth": {
113
+ "description": "Caps the whole-repository history scan used when there is no diff range (a local run or --full) to the last N commits. Ignored in a PR run, which scans the diff range.",
114
+ "type": "integer",
115
+ "minimum": 1
116
+ }
112
117
  }
113
118
  },
114
119
  "npm-audit": {