pr-checkmate 1.23.23 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/README.md +68 -15
  2. package/cspell.json +3 -8
  3. package/dist/cli.js +37 -62
  4. package/dist/config/constants.d.ts +21 -3
  5. package/dist/config/constants.js +33 -6
  6. package/dist/config/pr-checkmate-workflow.yml +33 -327
  7. package/dist/core/checks/dependencies/banned-imports.js +23 -24
  8. package/dist/core/checks/dependencies/circular-deps.js +1 -0
  9. package/dist/core/checks/dependencies/dependency-check.js +1 -0
  10. package/dist/core/checks/dependencies/grype-scan.js +1 -0
  11. package/dist/core/checks/dependencies/npm-audit.js +6 -1
  12. package/dist/core/checks/dependencies/vuln-scan.js +1 -0
  13. package/dist/core/checks/findings.d.ts +198 -19
  14. package/dist/core/checks/findings.js +474 -22
  15. package/dist/core/checks/git/custom-rules.js +21 -16
  16. package/dist/core/checks/git/diff-smell.d.ts +17 -0
  17. package/dist/core/checks/git/diff-smell.js +43 -24
  18. package/dist/core/checks/git/large-files.js +14 -2
  19. package/dist/core/checks/git/leftover-debug.js +23 -14
  20. package/dist/core/checks/git/lockfile-drift.js +2 -1
  21. package/dist/core/checks/git/merge-conflict.js +19 -8
  22. package/dist/core/checks/git/missing-tests.js +22 -6
  23. package/dist/core/checks/git/todo-fixme.js +10 -9
  24. package/dist/core/checks/languages/shellcheck.d.ts +8 -0
  25. package/dist/core/checks/languages/shellcheck.js +55 -0
  26. package/dist/core/checks/languages/typecheck.js +1 -0
  27. package/dist/core/checks/pr/pr-size-check.js +2 -1
  28. package/dist/core/checks/quality/config-validation.d.ts +14 -4
  29. package/dist/core/checks/quality/config-validation.js +119 -17
  30. package/dist/core/checks/quality/dead-code.js +1 -0
  31. package/dist/core/checks/quality/duplicate-check.js +1 -0
  32. package/dist/core/checks/quality/license-header.js +12 -2
  33. package/dist/core/checks/quality/spellcheck.d.ts +50 -0
  34. package/dist/core/checks/quality/spellcheck.js +141 -13
  35. package/dist/core/checks/quality/symlinks.d.ts +16 -0
  36. package/dist/core/checks/quality/symlinks.js +62 -0
  37. package/dist/core/checks/quality/yaml-lint.js +22 -9
  38. package/dist/core/checks/security/diff-security.js +111 -28
  39. package/dist/core/checks/security/dockerfile-security.js +23 -20
  40. package/dist/core/checks/security/migration-safety.js +19 -17
  41. package/dist/core/checks/security/sensitive-file-guard.js +37 -5
  42. package/dist/core/checks/security/workflow-security.d.ts +10 -0
  43. package/dist/core/checks/security/workflow-security.js +48 -26
  44. package/dist/core/commands.d.ts +27 -0
  45. package/dist/core/commands.js +91 -0
  46. package/dist/core/context.d.ts +12 -3
  47. package/dist/core/context.js +118 -16
  48. package/dist/core/install/cache.d.ts +39 -0
  49. package/dist/core/install/cache.js +99 -0
  50. package/dist/core/install/doctor.d.ts +35 -0
  51. package/dist/core/install/doctor.js +145 -0
  52. package/dist/core/install/ensure.d.ts +60 -0
  53. package/dist/core/install/ensure.js +169 -0
  54. package/dist/core/install/lock.d.ts +33 -0
  55. package/dist/core/install/lock.js +99 -0
  56. package/dist/core/install/profiles.d.ts +64 -0
  57. package/dist/core/install/profiles.js +170 -0
  58. package/dist/core/registry.js +4 -0
  59. package/dist/core/reporters.d.ts +13 -2
  60. package/dist/core/reporters.js +145 -104
  61. package/dist/core/run-checks.js +91 -14
  62. package/dist/core/targets.js +26 -4
  63. package/dist/core/types.d.ts +49 -1
  64. package/dist/init.d.ts +29 -14
  65. package/dist/init.js +204 -262
  66. package/dist/interfaces/index.d.ts +130 -0
  67. package/dist/scripts/generate-install-manifests.d.ts +39 -0
  68. package/dist/scripts/generate-install-manifests.js +121 -0
  69. package/dist/scripts/publish-github.js +18 -4
  70. package/dist/utils/detect-language.d.ts +37 -4
  71. package/dist/utils/detect-language.js +113 -19
  72. package/dist/utils/http.d.ts +59 -0
  73. package/dist/utils/http.js +119 -0
  74. package/dist/utils/logger.d.ts +52 -6
  75. package/dist/utils/logger.js +125 -33
  76. package/dist/utils/pool.d.ts +49 -0
  77. package/dist/utils/pool.js +124 -0
  78. package/dist/utils/update-notifier.d.ts +9 -0
  79. package/dist/utils/update-notifier.js +57 -3
  80. package/eslint.config.mjs +14 -2
  81. package/install-manifests/commitlint/package-lock.json +1028 -0
  82. package/install-manifests/commitlint/package.json +9 -0
  83. package/install-manifests/core/package-lock.json +1335 -0
  84. package/install-manifests/core/package.json +11 -0
  85. package/install-manifests/cpp/package-lock.json +29 -0
  86. package/install-manifests/cpp/package.json +8 -0
  87. package/install-manifests/duplicate/package-lock.json +113 -0
  88. package/install-manifests/duplicate/package.json +8 -0
  89. package/install-manifests/js/package-lock.json +5099 -0
  90. package/install-manifests/js/package.json +26 -0
  91. package/install-manifests/markdown/package-lock.json +1281 -0
  92. package/install-manifests/markdown/package.json +8 -0
  93. package/install-manifests/python/package-lock.json +21 -0
  94. package/install-manifests/python/package.json +8 -0
  95. package/install-manifests/spellcheck/package-lock.json +1047 -0
  96. package/install-manifests/spellcheck/package.json +8 -0
  97. package/package.json +13 -28
  98. package/pr-checkmate.schema.json +1070 -96
  99. package/src/config/cspell.json +3 -8
  100. package/src/config/pr-checkmate-workflow.yml +33 -327
  101. package/src/config/pr-checkmate.schema.json +1070 -96
@@ -45,26 +45,13 @@ const DOCKER_PATTERNS = [
45
45
  severity: 'warn',
46
46
  },
47
47
  ];
48
- function scanDiff(stdout) {
49
- const errors = {};
50
- const warnings = {};
51
- for (const line of stdout.split('\n')) {
52
- if (line.includes('pr-checkmate-ignore'))
53
- continue;
54
- for (const { pattern, label, severity } of DOCKER_PATTERNS) {
55
- if (pattern.test(line)) {
56
- const bucket = severity === 'error' ? errors : warnings;
57
- (bucket[label] ?? (bucket[label] = [])).push(line.slice(1).trim().substring(0, 120));
58
- }
59
- }
60
- }
61
- return { errors, warnings };
62
- }
63
48
  exports.dockerfileSecurityCheck = {
64
49
  name: 'Dockerfile Security',
65
50
  phase: 'informational',
51
+ // No `requiresDiff`: a secret baked into an ENV line is baked in whether the
52
+ // line is new or inherited (ticket 067).
66
53
  applies(ctx) {
67
- return ctx.config.dockerfileSecurity?.enabled !== false && Boolean(ctx.baseSha);
54
+ return ctx.config.dockerfileSecurity?.enabled !== false;
68
55
  },
69
56
  async run(ctx) {
70
57
  const files = await (0, targets_1.resolveTargetFiles)(ctx, ['*']);
@@ -74,14 +61,30 @@ exports.dockerfileSecurityCheck = {
74
61
  if (dockerfiles.length === 0)
75
62
  return (0, types_1.pass)();
76
63
  try {
77
- const { stdout } = await (0, execa_1.execa)('git', ['diff', '--unified=0', ctx.baseSha, ctx.headSha ?? 'HEAD', '--', ...dockerfiles], { cwd: ctx.cwd });
78
- const { errors, warnings } = scanDiff(stdout);
64
+ const cfg = ctx.config.dockerfileSecurity ?? {};
65
+ let stdout = '';
66
+ if (ctx.baseSha) {
67
+ stdout = (await (0, execa_1.execa)('git', ['diff', '--unified=0', ctx.baseSha, ctx.headSha ?? 'HEAD', '--', ...dockerfiles], { cwd: ctx.cwd })).stdout;
68
+ }
69
+ else if (ctx.config.run?.fullScanBehaviour !== 'skip') {
70
+ await (0, findings_1.scanTrackedFiles)(ctx, dockerfiles, asDiff => {
71
+ stdout += `${asDiff}\n`;
72
+ });
73
+ }
74
+ // This check runs `git diff` itself rather than going through
75
+ // `diffAddedLines`, so the `pr-checkmate-ignore` opt-out is applied here.
76
+ const { errors, warnings } = (0, findings_1.scanDiffPatterns)(stdout, DOCKER_PATTERNS, {
77
+ skipIgnored: true,
78
+ // Dockerfiles have no extension to key off, and `#` is their comment
79
+ // marker — the `hash` reading, not the language-detected one.
80
+ comments: 'legacy',
81
+ ignorePaths: cfg.ignorePaths,
82
+ });
79
83
  return (0, findings_1.reportSeverityBuckets)(errors, warnings, {
80
84
  prefix: 'Dockerfile Security',
81
85
  errorSummary: n => `${n} issue(s)`,
82
86
  warningSummary: n => `${n} warning(s)`,
83
- sampleLimit: 3,
84
- ignore: ctx.config.dockerfileSecurity?.ignore,
87
+ ...(0, findings_1.clientFindingOptions)(ctx, cfg),
85
88
  });
86
89
  }
87
90
  catch (err) {
@@ -65,8 +65,11 @@ function scan(stdout, findings) {
65
65
  exports.migrationSafetyCheck = {
66
66
  name: 'Migration Safety',
67
67
  phase: 'informational',
68
+ // No `requiresDiff`: a destructive migration is destructive whenever it was
69
+ // written (ticket 067). Over a whole repository this becomes an inventory of
70
+ // the irreversible migrations already in the history, which is worth having.
68
71
  applies(ctx) {
69
- return ctx.config.migrationSafety?.enabled !== false && Boolean(ctx.baseSha);
72
+ return ctx.config.migrationSafety?.enabled !== false;
70
73
  },
71
74
  async run(ctx) {
72
75
  // Not sourcePath-scoped: migrations commonly live outside `src` (db/, alembic/).
@@ -79,28 +82,27 @@ exports.migrationSafetyCheck = {
79
82
  if (migrationFiles.length === 0)
80
83
  return (0, types_1.pass)();
81
84
  try {
82
- const { stdout } = await (0, execa_1.execa)('git', [
83
- 'diff',
84
- '--unified=0',
85
- ctx.baseSha,
86
- ctx.headSha ?? 'HEAD',
87
- '--',
88
- ...migrationFiles,
89
- ], { cwd: ctx.cwd });
85
+ let stdout = '';
86
+ if (ctx.baseSha) {
87
+ stdout = (await (0, execa_1.execa)('git', ['diff', '--unified=0', ctx.baseSha, ctx.headSha ?? 'HEAD', '--', ...migrationFiles], { cwd: ctx.cwd })).stdout;
88
+ }
89
+ else if (ctx.config.run?.fullScanBehaviour !== 'skip') {
90
+ await (0, findings_1.scanTrackedFiles)(ctx, migrationFiles, asDiff => {
91
+ stdout += `${asDiff}\n`;
92
+ });
93
+ }
90
94
  const findings = {};
91
95
  scan(stdout, findings);
92
96
  if ((0, findings_1.bucketTotal)(findings) === 0)
93
97
  return (0, types_1.pass)();
94
- const sev = (0, findings_1.severityEmitter)(ctx.config.migrationSafety?.severity, 'warn');
95
- (0, findings_1.logFindings)(findings, {
98
+ return (0, findings_1.reportCountedFindings)(findings, ctx, {
96
99
  prefix: 'Migration Safety',
97
- sampleLimit: 2,
98
- isError: sev.icon === '❌',
100
+ severity: ctx.config.migrationSafety?.severity,
101
+ sampleLimit: ctx.config.log?.sampleLimit ?? 2,
102
+ // The labels carry an explanatory tail ("… — rewrites every row"); the
103
+ // summary is a one-line tally, so only the name of the risk fits.
104
+ summaryLabel: label => label.split(' — ')[0],
99
105
  });
100
- const summary = Object.entries(findings)
101
- .map(([label, arr]) => `${arr.length}× ${label.split(' — ')[0]}`)
102
- .join(', ');
103
- return sev.emit(summary);
104
106
  }
105
107
  catch (err) {
106
108
  utils_1.logger.warn(`[Migration Safety]: ⚠️ Could not analyze diff\n${err}`);
@@ -72,21 +72,53 @@ function categorizeFiles(changedFiles) {
72
72
  exports.sensitiveFileGuardCheck = {
73
73
  name: 'Sensitive Files',
74
74
  phase: 'informational',
75
+ // No `requiresDiff`: a committed `.env` or private key is committed whether
76
+ // this PR touched it or not (ticket 067). In a full scan the question becomes
77
+ // "what sensitive files does this repository contain", which is the more
78
+ // useful one when auditing a repo rather than reviewing a change.
75
79
  applies(ctx) {
76
- return ctx.config.sensitiveFileGuard?.enabled !== false && Boolean(ctx.baseSha);
80
+ return ctx.config.sensitiveFileGuard?.enabled !== false;
77
81
  },
78
82
  async run(ctx) {
79
83
  try {
80
- const { stdout } = await (0, execa_1.execa)('git', ['diff', '--name-only', '--diff-filter=ACMRD', ctx.baseSha, 'HEAD'], { cwd: ctx.cwd });
81
- const changedFiles = stdout.split('\n').filter(Boolean);
84
+ const args = ctx.baseSha
85
+ ? ['diff', '--name-only', '--diff-filter=ACMRD', ctx.baseSha, 'HEAD']
86
+ : ['ls-files'];
87
+ if (!ctx.baseSha && ctx.config.run?.fullScanBehaviour === 'skip')
88
+ return (0, types_1.pass)();
89
+ // `reject: false` so a directory that is not a repository is an ordinary
90
+ // outcome rather than a thrown error whose message carries git's raw
91
+ // `fatal: not a git repository` into the log. The check still skips — but
92
+ // a skip with a reason reads as a decision, while a bare `fatal:` reads as
93
+ // a crash, and someone has to spend time proving it was not one.
94
+ const result = await (0, execa_1.execa)('git', args, { cwd: ctx.cwd, reject: false });
95
+ if (result.exitCode !== 0)
96
+ return (0, types_1.skip)('git unavailable');
97
+ const cfg = ctx.config.sensitiveFileGuard ?? {};
98
+ // Path exclusions are applied before categorising, so one false positive —
99
+ // a `.pem` test fixture, a committed `.env.example` — is excused by name.
100
+ // Without this the only exclusion a client has is `ignore`, which suppresses
101
+ // the whole CATEGORY: muting "Private keys and certificates" to excuse one
102
+ // fixture would also excuse the next real key someone commits.
103
+ const changedFiles = result.stdout
104
+ .split('\n')
105
+ .filter(Boolean)
106
+ .filter(file => !(0, utils_1.matchesAnyGlob)(file, cfg.ignorePaths ?? []));
82
107
  if (changedFiles.length === 0)
83
108
  return (0, types_1.pass)();
84
109
  const { errors, warnings } = categorizeFiles(changedFiles);
85
110
  return (0, findings_1.reportSeverityBuckets)(errors, warnings, {
86
111
  prefix: 'Sensitive Files',
87
- errorSummary: n => `${n} sensitive file(s) changed`,
88
- warningSummary: n => `${n} notable file(s) changed`,
112
+ // "changed" is only true of a diff; a full scan is answering a different
113
+ // question — what the repository contains — and saying otherwise would
114
+ // send someone looking for a change that never happened.
115
+ errorSummary: n => `${n} sensitive file(s) ${ctx.baseSha ? 'changed' : 'in the repo'}`,
116
+ warningSummary: n => `${n} notable file(s) ${ctx.baseSha ? 'changed' : 'in the repo'}`,
89
117
  showCount: false,
118
+ ...(0, findings_1.clientFindingOptions)(ctx, cfg),
119
+ // Every sensitive path is listed rather than sampled: knowing that eight
120
+ // files are in play but only which three is not an answer anyone can act on.
121
+ sampleLimit: undefined,
90
122
  });
91
123
  }
92
124
  catch (err) {
@@ -1,2 +1,12 @@
1
1
  import { Check } from '../../types';
2
+ import { DiffPattern } from '../findings';
3
+ /**
4
+ * GitHub Actions workflow risks, matched on added diff lines of workflow files.
5
+ * These are the high-frequency supply-chain / injection footguns.
6
+ *
7
+ * Exported for the corpus in `workflow-security.test.ts`: these rules turn on a
8
+ * context distinction subtle enough that only worked examples can hold it in
9
+ * place.
10
+ */
11
+ export declare const WORKFLOW_PATTERNS: DiffPattern[];
2
12
  export declare const workflowSecurityCheck: Check;
@@ -1,23 +1,46 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.workflowSecurityCheck = void 0;
3
+ exports.workflowSecurityCheck = exports.WORKFLOW_PATTERNS = void 0;
4
4
  const utils_1 = require("../../../utils");
5
+ const targets_1 = require("../../targets");
5
6
  const types_1 = require("../../types");
6
7
  const findings_1 = require("../findings");
8
+ /**
9
+ * A YAML key binding — `HEAD_REF: ${{ … }}`, `ref: ${{ … }}` — rather than an
10
+ * interpolation inside a script body.
11
+ *
12
+ * This is the distinction the rules below turn on. Binding an untrusted value to
13
+ * an `env:` entry or an action input is *the recommended fix*: the value reaches
14
+ * the shell as data, expanded after parsing. Flagging it made the check punish
15
+ * the exact remediation it recommends, which is worse than not checking at all —
16
+ * a warning on correct code teaches people to ignore the warnings.
17
+ *
18
+ * The risky shape is the same expression inside a `run:` block, where GitHub
19
+ * substitutes it into the script *before* the shell parses it.
20
+ */
21
+ const YAML_KEY_BINDING = String.raw `(?!\s*[\w.-]+:\s*\$\{\{)`;
7
22
  /**
8
23
  * GitHub Actions workflow risks, matched on added diff lines of workflow files.
9
24
  * These are the high-frequency supply-chain / injection footguns.
25
+ *
26
+ * Exported for the corpus in `workflow-security.test.ts`: these rules turn on a
27
+ * context distinction subtle enough that only worked examples can hold it in
28
+ * place.
10
29
  */
11
- const WORKFLOW_PATTERNS = [
30
+ exports.WORKFLOW_PATTERNS = [
12
31
  {
13
32
  // Untrusted PR-controlled input interpolated into an expression — the classic
14
33
  // `run:` script-injection vector when it reaches a shell.
15
- pattern: /^\+(?!\+).*\$\{\{\s*github\.event\.(?:issue|pull_request|comment|review|discussion|head_commit|commits)[^}]*\.(?:title|body|head_ref|ref|label|name|email|message|url)/,
34
+ pattern: new RegExp([
35
+ String.raw `^\+(?!\+)${YAML_KEY_BINDING}.*\$\{\{\s*github\.event\.`,
36
+ String.raw `(?:issue|pull_request|comment|review|discussion|head_commit|commits)[^}]*`,
37
+ String.raw `\.(?:title|body|head_ref|ref|label|name|email|message|url)`,
38
+ ].join('')),
16
39
  label: 'untrusted github.event input in expression (script injection risk)',
17
40
  severity: 'error',
18
41
  },
19
42
  {
20
- pattern: /^\+(?!\+).*\$\{\{\s*github\.head_ref\s*\}\}/,
43
+ pattern: new RegExp(String.raw `^\+(?!\+)${YAML_KEY_BINDING}.*\$\{\{\s*github\.head_ref\s*\}\}`),
21
44
  label: 'github.head_ref interpolation (injection risk — use an env var)',
22
45
  severity: 'warn',
23
46
  },
@@ -33,38 +56,37 @@ const WORKFLOW_PATTERNS = [
33
56
  severity: 'warn',
34
57
  },
35
58
  ];
36
- function scanDiff(stdout) {
37
- const errors = {};
38
- const warnings = {};
39
- for (const line of stdout.split('\n')) {
40
- for (const { pattern, label, severity } of WORKFLOW_PATTERNS) {
41
- if (pattern.test(line)) {
42
- const bucket = severity === 'error' ? errors : warnings;
43
- (bucket[label] ?? (bucket[label] = [])).push(line.slice(1).trim().substring(0, 120));
44
- }
45
- }
46
- }
47
- return { errors, warnings };
48
- }
49
59
  exports.workflowSecurityCheck = {
50
60
  name: 'Workflow Security',
51
61
  phase: 'informational',
62
+ // No `requiresDiff`: an injectable workflow is injectable whether the line
63
+ // arrived in this PR or long before it (ticket 067). Ours had lived there for
64
+ // months when ticket 060 found it — by hand, because no mode looked.
52
65
  applies(ctx) {
53
- return ctx.config.workflowSecurity?.enabled !== false && Boolean(ctx.baseSha);
66
+ return ctx.config.workflowSecurity?.enabled !== false;
54
67
  },
55
68
  async run(ctx) {
56
69
  try {
57
- const stdout = await (0, findings_1.diffAddedLines)(ctx, [
58
- '.github/workflows/*.yml',
59
- '.github/workflows/*.yaml',
60
- ]);
61
- const { errors, warnings } = scanDiff(stdout);
62
- return (0, findings_1.reportSeverityBuckets)(errors, warnings, {
70
+ const globs = ['.github/workflows/*.yml', '.github/workflows/*.yaml'];
71
+ const cfg = ctx.config.workflowSecurity ?? {};
72
+ const found = (0, findings_1.emptyBuckets)();
73
+ const scanInto = (stdout) => {
74
+ // `diffAddedLines` already dropped `pr-checkmate-ignore` lines, and
75
+ // `scanTrackedFiles` does the same for the whole-file path.
76
+ (0, findings_1.mergeSeverityBuckets)(found, (0, findings_1.scanDiffPatterns)(stdout, exports.WORKFLOW_PATTERNS, { ignorePaths: cfg.ignorePaths }));
77
+ };
78
+ if (ctx.baseSha) {
79
+ scanInto(await (0, findings_1.diffAddedLines)(ctx, globs));
80
+ }
81
+ else if (ctx.config.run?.fullScanBehaviour !== 'skip') {
82
+ const files = await (0, targets_1.listTrackedFiles)(ctx, globs, ['.']);
83
+ await (0, findings_1.scanTrackedFiles)(ctx, files, scanInto);
84
+ }
85
+ return (0, findings_1.reportSeverityBuckets)(found.errors, found.warnings, {
63
86
  prefix: 'Workflow Security',
64
87
  errorSummary: n => `${n} issue(s)`,
65
88
  warningSummary: n => `${n} warning(s)`,
66
- sampleLimit: 3,
67
- ignore: ctx.config.workflowSecurity?.ignore,
89
+ ...(0, findings_1.clientFindingOptions)(ctx, cfg),
68
90
  });
69
91
  }
70
92
  catch (err) {
@@ -0,0 +1,27 @@
1
+ /**
2
+ * CLI vocabulary, kept out of `cli.ts` so it can be imported without side
3
+ * effects: that module is an executable entry point — it parses `process.argv`
4
+ * and calls `process.exit` at import time, so anything importing it to read
5
+ * these tables would run the CLI instead.
6
+ *
7
+ * Separating them is what lets `commands.test.ts` assert the invariant that
8
+ * matters here: every command must resolve to a check that actually exists.
9
+ * A command pointing at a name no longer in the registry does not error — the
10
+ * `only` filter simply matches nothing, every check is filtered out, and the run
11
+ * reports success having verified nothing at all.
12
+ */
13
+ /** Maps short CLI command names to their registry check names. */
14
+ export declare const COMMAND_TO_CHECK: Record<string, string>;
15
+ /**
16
+ * Commands that run the whole pipeline (or no pipeline at all) and take no siblings.
17
+ *
18
+ * The three install commands are here rather than in {@link COMMAND_TO_CHECK}
19
+ * because they run no checks at all: they manage the tooling the checks need.
20
+ * `doctor` in particular is the answer to "why did my typecheck not run" once
21
+ * tooling is fetched on demand — a question that must have a command behind it
22
+ * and not a reading of our source.
23
+ */
24
+ export declare const TOOLING_COMMANDS: readonly string[];
25
+ export declare const STANDALONE_COMMANDS: readonly string[];
26
+ /** Flags accepted anywhere on the command line, with what each one means. */
27
+ export declare const FLAGS: Record<string, string>;
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+ /**
3
+ * CLI vocabulary, kept out of `cli.ts` so it can be imported without side
4
+ * effects: that module is an executable entry point — it parses `process.argv`
5
+ * and calls `process.exit` at import time, so anything importing it to read
6
+ * these tables would run the CLI instead.
7
+ *
8
+ * Separating them is what lets `commands.test.ts` assert the invariant that
9
+ * matters here: every command must resolve to a check that actually exists.
10
+ * A command pointing at a name no longer in the registry does not error — the
11
+ * `only` filter simply matches nothing, every check is filtered out, and the run
12
+ * reports success having verified nothing at all.
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.FLAGS = exports.STANDALONE_COMMANDS = exports.TOOLING_COMMANDS = exports.COMMAND_TO_CHECK = void 0;
16
+ /** Maps short CLI command names to their registry check names. */
17
+ exports.COMMAND_TO_CHECK = {
18
+ // Every check that can fail a run is reachable on its own, so "why does this
19
+ // block me" is answered by running that one check instead of the whole suite.
20
+ 'merge-conflict': 'Merge Conflict',
21
+ lint: 'ESLint',
22
+ deps: 'Dependencies',
23
+ dependencies: 'Dependencies',
24
+ 'npm-audit': 'NPM Audit',
25
+ prettier: 'Prettier',
26
+ spellcheck: 'Spellcheck',
27
+ security: 'Security Scan',
28
+ circular: 'Circular Deps',
29
+ license: 'License Check',
30
+ 'pr-size': 'PR Size',
31
+ 'pr-title': 'PR Title',
32
+ outdated: 'Outdated Deps',
33
+ markdown: 'Markdown',
34
+ 'yaml-lint': 'YAML Lint',
35
+ symlinks: 'Broken Symlinks',
36
+ shellcheck: 'ShellCheck',
37
+ typecheck: 'TypeScript',
38
+ 'diff-smell': 'Diff Smells',
39
+ 'diff-security': 'Diff Security',
40
+ 'workflow-security': 'Workflow Security',
41
+ 'custom-rules': 'Custom Rules',
42
+ 'banned-imports': 'Banned Imports',
43
+ 'license-header': 'License Header',
44
+ 'config-validation': 'Config Validation',
45
+ 'dockerfile-security': 'Dockerfile Security',
46
+ 'case-collision': 'Case Collision',
47
+ 'vuln-scan': 'Vuln Scan',
48
+ 'leftover-debug': 'Leftover Debug',
49
+ 'lockfile-drift': 'Lockfile Drift',
50
+ 'large-files': 'Large Files',
51
+ 'todo-fixme': 'TODO/FIXME',
52
+ 'missing-tests': 'Missing Tests',
53
+ coverage: 'Coverage',
54
+ 'migration-safety': 'Migration Safety',
55
+ 'sensitive-files': 'Sensitive Files',
56
+ 'pr-body': 'PR Body',
57
+ duplicate: 'Duplicate Code',
58
+ commitlint: 'Commitlint',
59
+ 'cpp-format': 'C++ Format',
60
+ 'python-lint': 'Ruff Lint',
61
+ 'python-format': 'Ruff Format',
62
+ 'python-typecheck': 'Python Types',
63
+ 'go-lint': 'Go Vet',
64
+ 'go-format': 'Go Format',
65
+ 'rust-lint': 'Clippy',
66
+ 'rust-format': 'Rustfmt',
67
+ 'csharp-format': 'C# Format',
68
+ 'ruby-lint': 'RuboCop',
69
+ 'php-format': 'PHP CS Fixer',
70
+ };
71
+ /**
72
+ * Commands that run the whole pipeline (or no pipeline at all) and take no siblings.
73
+ *
74
+ * The three install commands are here rather than in {@link COMMAND_TO_CHECK}
75
+ * because they run no checks at all: they manage the tooling the checks need.
76
+ * `doctor` in particular is the answer to "why did my typecheck not run" once
77
+ * tooling is fetched on demand — a question that must have a command behind it
78
+ * and not a reading of our source.
79
+ */
80
+ exports.TOOLING_COMMANDS = ['install', 'doctor', 'cache-prune'];
81
+ exports.STANDALONE_COMMANDS = [
82
+ 'init',
83
+ 'all',
84
+ 'precommit',
85
+ ...exports.TOOLING_COMMANDS,
86
+ ];
87
+ /** Flags accepted anywhere on the command line, with what each one means. */
88
+ exports.FLAGS = {
89
+ '--full': 'review every tracked file instead of the diff (the default outside CI)',
90
+ '--all': 'with "install": fetch every profile, not only the ones this repository needs',
91
+ };
@@ -22,6 +22,8 @@ type RangePlan = {
22
22
  headSha?: string;
23
23
  } | {
24
24
  kind: 'github-event';
25
+ } | {
26
+ kind: 'branch';
25
27
  } | {
26
28
  kind: 'ref';
27
29
  ref: string;
@@ -37,9 +39,16 @@ type RangePlan = {
37
39
  * branch) — a gate exists to judge the diff.
38
40
  * 4. A CI build with no PR context still diffs `HEAD^`: it is a push/branch build,
39
41
  * 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.
42
+ * 5. Anything else is a human at a terminal, working on a branch → review what the
43
+ * branch changed, i.e. everything since it diverged from the default branch.
44
+ * That is the same set of changes a pull request would show, which is what
45
+ * someone running the tool before opening one is asking about.
46
+ *
47
+ * `HEAD^` was tried here once and was wrong for a different reason: it judged
48
+ * only the last commit, so a `debugger` committed earlier on the branch passed.
49
+ * The merge base has neither problem — it covers every commit on the branch —
50
+ * and on the default branch itself, where there is nothing to diverge from,
51
+ * {@link resolveDiffRange} falls back to a full scan.
43
52
  *
44
53
  * @param opts Run options as passed to `runChecks`.
45
54
  * @param env Environment to read CI markers from. Defaults to the current process.