pr-checkmate 1.22.8 → 1.23.1

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 (131) hide show
  1. package/README.md +101 -333
  2. package/cspell.json +24 -1
  3. package/dist/cli.js +40 -2
  4. package/dist/config/constants.d.ts +9 -0
  5. package/dist/config/constants.js +68 -1
  6. package/dist/core/checks/dependencies/banned-imports.d.ts +2 -0
  7. package/dist/core/checks/dependencies/banned-imports.js +71 -0
  8. package/dist/core/checks/{circular-deps.d.ts → dependencies/circular-deps.d.ts} +1 -1
  9. package/dist/core/checks/{circular-deps.js → dependencies/circular-deps.js} +5 -5
  10. package/dist/core/checks/{dependency-check.d.ts → dependencies/dependency-check.d.ts} +1 -1
  11. package/dist/core/checks/{dependency-check.js → dependencies/dependency-check.js} +3 -3
  12. package/dist/core/checks/{license-check.d.ts → dependencies/license-check.d.ts} +1 -1
  13. package/dist/core/checks/{license-check.js → dependencies/license-check.js} +3 -3
  14. package/dist/core/checks/{npm-audit.d.ts → dependencies/npm-audit.d.ts} +1 -1
  15. package/dist/core/checks/{npm-audit.js → dependencies/npm-audit.js} +3 -3
  16. package/dist/core/checks/{outdated-deps.d.ts → dependencies/outdated-deps.d.ts} +1 -1
  17. package/dist/core/checks/{outdated-deps.js → dependencies/outdated-deps.js} +17 -4
  18. package/dist/core/checks/dependencies/vuln-scan.d.ts +9 -0
  19. package/dist/core/checks/dependencies/vuln-scan.js +41 -0
  20. package/dist/core/checks/findings.d.ts +37 -1
  21. package/dist/core/checks/findings.js +80 -3
  22. package/dist/core/checks/git/custom-rules.d.ts +2 -0
  23. package/dist/core/checks/git/custom-rules.js +102 -0
  24. package/dist/core/checks/{diff-smell.d.ts → git/diff-smell.d.ts} +1 -1
  25. package/dist/core/checks/{diff-smell.js → git/diff-smell.js} +3 -3
  26. package/dist/core/checks/git/large-files.d.ts +2 -0
  27. package/dist/core/checks/git/large-files.js +106 -0
  28. package/dist/core/checks/git/leftover-debug.d.ts +2 -0
  29. package/dist/core/checks/git/leftover-debug.js +77 -0
  30. package/dist/core/checks/git/lockfile-drift.d.ts +2 -0
  31. package/dist/core/checks/git/lockfile-drift.js +114 -0
  32. package/dist/core/checks/git/merge-conflict.d.ts +2 -0
  33. package/dist/core/checks/git/merge-conflict.js +109 -0
  34. package/dist/core/checks/git/missing-tests.d.ts +2 -0
  35. package/dist/core/checks/git/missing-tests.js +92 -0
  36. package/dist/core/checks/git/todo-fixme.d.ts +7 -0
  37. package/dist/core/checks/git/todo-fixme.js +74 -0
  38. package/dist/core/checks/{cpp-format.d.ts → languages/cpp-format.d.ts} +1 -1
  39. package/dist/core/checks/{cpp-format.js → languages/cpp-format.js} +3 -3
  40. package/dist/core/checks/languages/csharp-format.d.ts +13 -0
  41. package/dist/core/checks/languages/csharp-format.js +52 -0
  42. package/dist/core/checks/languages/go-format.d.ts +8 -0
  43. package/dist/core/checks/languages/go-format.js +48 -0
  44. package/dist/core/checks/languages/go-lint.d.ts +8 -0
  45. package/dist/core/checks/languages/go-lint.js +48 -0
  46. package/dist/core/checks/{kotlin-lint.d.ts → languages/kotlin-lint.d.ts} +1 -1
  47. package/dist/core/checks/{kotlin-lint.js → languages/kotlin-lint.js} +13 -17
  48. package/dist/core/checks/{lint.d.ts → languages/lint.d.ts} +1 -1
  49. package/dist/core/checks/{lint.js → languages/lint.js} +5 -5
  50. package/dist/core/checks/languages/php-format.d.ts +9 -0
  51. package/dist/core/checks/languages/php-format.js +49 -0
  52. package/dist/core/checks/{prettier.d.ts → languages/prettier.d.ts} +1 -1
  53. package/dist/core/checks/{prettier.js → languages/prettier.js} +5 -5
  54. package/dist/core/checks/{python-format.d.ts → languages/python-format.d.ts} +1 -1
  55. package/dist/core/checks/{python-format.js → languages/python-format.js} +3 -3
  56. package/dist/core/checks/{python-lint.d.ts → languages/python-lint.d.ts} +1 -1
  57. package/dist/core/checks/{python-lint.js → languages/python-lint.js} +3 -3
  58. package/dist/core/checks/languages/python-typecheck.d.ts +9 -0
  59. package/dist/core/checks/languages/python-typecheck.js +66 -0
  60. package/dist/core/checks/languages/ruby-lint.d.ts +8 -0
  61. package/dist/core/checks/languages/ruby-lint.js +50 -0
  62. package/dist/core/checks/languages/rust-format.d.ts +9 -0
  63. package/dist/core/checks/languages/rust-format.js +45 -0
  64. package/dist/core/checks/languages/rust-lint.d.ts +8 -0
  65. package/dist/core/checks/languages/rust-lint.js +50 -0
  66. package/dist/core/checks/{swift-lint.d.ts → languages/swift-lint.d.ts} +1 -1
  67. package/dist/core/checks/{swift-lint.js → languages/swift-lint.js} +13 -17
  68. package/dist/core/checks/{typecheck.d.ts → languages/typecheck.d.ts} +1 -1
  69. package/dist/core/checks/{typecheck.js → languages/typecheck.js} +3 -3
  70. package/dist/core/checks/{commitlint.d.ts → pr/commitlint.d.ts} +1 -1
  71. package/dist/core/checks/{commitlint.js → pr/commitlint.js} +4 -4
  72. package/dist/core/checks/{pr-body.d.ts → pr/pr-body.d.ts} +1 -1
  73. package/dist/core/checks/{pr-body.js → pr/pr-body.js} +2 -2
  74. package/dist/core/checks/{pr-size-check.d.ts → pr/pr-size-check.d.ts} +1 -1
  75. package/dist/core/checks/{pr-size-check.js → pr/pr-size-check.js} +13 -11
  76. package/dist/core/checks/{pr-title-lint.d.ts → pr/pr-title-lint.d.ts} +1 -1
  77. package/dist/core/checks/{pr-title-lint.js → pr/pr-title-lint.js} +2 -2
  78. package/dist/core/checks/quality/case-collision.d.ts +8 -0
  79. package/dist/core/checks/quality/case-collision.js +40 -0
  80. package/dist/core/checks/quality/config-validation.d.ts +8 -0
  81. package/dist/core/checks/quality/config-validation.js +63 -0
  82. package/dist/core/checks/quality/coverage.d.ts +2 -0
  83. package/dist/core/checks/quality/coverage.js +63 -0
  84. package/dist/core/checks/{dead-code.d.ts → quality/dead-code.d.ts} +1 -1
  85. package/dist/core/checks/{dead-code.js → quality/dead-code.js} +4 -4
  86. package/dist/core/checks/{duplicate-check.d.ts → quality/duplicate-check.d.ts} +1 -1
  87. package/dist/core/checks/{duplicate-check.js → quality/duplicate-check.js} +6 -6
  88. package/dist/core/checks/quality/license-header.d.ts +7 -0
  89. package/dist/core/checks/quality/license-header.js +74 -0
  90. package/dist/core/checks/{markdown-lint.d.ts → quality/markdown-lint.d.ts} +1 -1
  91. package/dist/core/checks/{markdown-lint.js → quality/markdown-lint.js} +5 -5
  92. package/dist/core/checks/{spellcheck.d.ts → quality/spellcheck.d.ts} +1 -1
  93. package/dist/core/checks/{spellcheck.js → quality/spellcheck.js} +5 -5
  94. package/dist/core/checks/{diff-security.d.ts → security/diff-security.d.ts} +1 -1
  95. package/dist/core/checks/security/diff-security.js +262 -0
  96. package/dist/core/checks/security/dockerfile-security.d.ts +2 -0
  97. package/dist/core/checks/security/dockerfile-security.js +92 -0
  98. package/dist/core/checks/security/migration-safety.d.ts +2 -0
  99. package/dist/core/checks/security/migration-safety.js +110 -0
  100. package/dist/core/checks/{security.d.ts → security/security.d.ts} +1 -1
  101. package/dist/core/checks/{security.js → security/security.js} +10 -5
  102. package/dist/core/checks/{sensitive-file-guard.d.ts → security/sensitive-file-guard.d.ts} +1 -1
  103. package/dist/core/checks/{sensitive-file-guard.js → security/sensitive-file-guard.js} +4 -4
  104. package/dist/core/checks/security/workflow-security.d.ts +2 -0
  105. package/dist/core/checks/security/workflow-security.js +75 -0
  106. package/dist/core/context.js +14 -0
  107. package/dist/core/registry.js +73 -28
  108. package/dist/core/reporters.d.ts +4 -3
  109. package/dist/core/reporters.js +106 -4
  110. package/dist/core/run-checks.js +21 -3
  111. package/dist/core/targets.d.ts +4 -1
  112. package/dist/core/targets.js +16 -3
  113. package/dist/core/types.d.ts +13 -1
  114. package/dist/init.js +82 -0
  115. package/dist/interfaces/index.d.ts +101 -0
  116. package/dist/scripts/copy-configs.js +1 -0
  117. package/dist/utils/detect-language.d.ts +1 -1
  118. package/dist/utils/detect-language.js +32 -3
  119. package/dist/utils/glob.d.ts +10 -0
  120. package/dist/utils/glob.js +25 -0
  121. package/dist/utils/index.d.ts +2 -0
  122. package/dist/utils/index.js +2 -0
  123. package/dist/utils/update-notifier.d.ts +10 -0
  124. package/dist/utils/update-notifier.js +60 -0
  125. package/package.json +2 -1
  126. package/pr-checkmate.schema.json +166 -0
  127. package/src/config/cspell.json +24 -1
  128. package/src/config/pr-checkmate.schema.json +166 -0
  129. package/dist/core/checks/diff-security.js +0 -145
  130. /package/dist/core/checks/{ruff-shared.d.ts → languages/ruff-shared.d.ts} +0 -0
  131. /package/dist/core/checks/{ruff-shared.js → languages/ruff-shared.js} +0 -0
@@ -1,40 +1,62 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.REGISTRY = void 0;
4
- // blocking — a failure here makes the whole run fail (report.ok === false)
5
- const circular_deps_1 = require("./checks/circular-deps");
6
- const commitlint_1 = require("./checks/commitlint");
7
- const cpp_format_1 = require("./checks/cpp-format");
8
- const dead_code_1 = require("./checks/dead-code");
9
- const dependency_check_1 = require("./checks/dependency-check");
10
- const diff_security_1 = require("./checks/diff-security");
11
- const diff_smell_1 = require("./checks/diff-smell");
12
- const duplicate_check_1 = require("./checks/duplicate-check");
13
- const kotlin_lint_1 = require("./checks/kotlin-lint");
14
- const license_check_1 = require("./checks/license-check");
15
- const lint_1 = require("./checks/lint");
16
- const markdown_lint_1 = require("./checks/markdown-lint");
17
- const npm_audit_1 = require("./checks/npm-audit");
18
- // informational — warn/skip only, never fail the run
19
- const outdated_deps_1 = require("./checks/outdated-deps");
20
- const pr_body_1 = require("./checks/pr-body");
21
- const pr_size_check_1 = require("./checks/pr-size-check");
22
- const pr_title_lint_1 = require("./checks/pr-title-lint");
23
- const prettier_1 = require("./checks/prettier");
24
- const python_format_1 = require("./checks/python-format");
25
- const python_lint_1 = require("./checks/python-lint");
26
- const security_1 = require("./checks/security");
27
- const sensitive_file_guard_1 = require("./checks/sensitive-file-guard");
28
- const spellcheck_1 = require("./checks/spellcheck");
29
- const swift_lint_1 = require("./checks/swift-lint");
30
- // format — runs last, may write files
31
- const typecheck_1 = require("./checks/typecheck");
4
+ const banned_imports_1 = require("./checks/dependencies/banned-imports");
5
+ const circular_deps_1 = require("./checks/dependencies/circular-deps");
6
+ const dependency_check_1 = require("./checks/dependencies/dependency-check");
7
+ const license_check_1 = require("./checks/dependencies/license-check");
8
+ const npm_audit_1 = require("./checks/dependencies/npm-audit");
9
+ const outdated_deps_1 = require("./checks/dependencies/outdated-deps");
10
+ const vuln_scan_1 = require("./checks/dependencies/vuln-scan");
11
+ const custom_rules_1 = require("./checks/git/custom-rules");
12
+ const diff_smell_1 = require("./checks/git/diff-smell");
13
+ const large_files_1 = require("./checks/git/large-files");
14
+ const leftover_debug_1 = require("./checks/git/leftover-debug");
15
+ const lockfile_drift_1 = require("./checks/git/lockfile-drift");
16
+ const merge_conflict_1 = require("./checks/git/merge-conflict");
17
+ const missing_tests_1 = require("./checks/git/missing-tests");
18
+ const todo_fixme_1 = require("./checks/git/todo-fixme");
19
+ const cpp_format_1 = require("./checks/languages/cpp-format");
20
+ const csharp_format_1 = require("./checks/languages/csharp-format");
21
+ const go_format_1 = require("./checks/languages/go-format");
22
+ const go_lint_1 = require("./checks/languages/go-lint");
23
+ const kotlin_lint_1 = require("./checks/languages/kotlin-lint");
24
+ const lint_1 = require("./checks/languages/lint");
25
+ const php_format_1 = require("./checks/languages/php-format");
26
+ const prettier_1 = require("./checks/languages/prettier");
27
+ const python_format_1 = require("./checks/languages/python-format");
28
+ const python_lint_1 = require("./checks/languages/python-lint");
29
+ const python_typecheck_1 = require("./checks/languages/python-typecheck");
30
+ const ruby_lint_1 = require("./checks/languages/ruby-lint");
31
+ const rust_format_1 = require("./checks/languages/rust-format");
32
+ const rust_lint_1 = require("./checks/languages/rust-lint");
33
+ const swift_lint_1 = require("./checks/languages/swift-lint");
34
+ const typecheck_1 = require("./checks/languages/typecheck");
35
+ const commitlint_1 = require("./checks/pr/commitlint");
36
+ const pr_body_1 = require("./checks/pr/pr-body");
37
+ const pr_size_check_1 = require("./checks/pr/pr-size-check");
38
+ const pr_title_lint_1 = require("./checks/pr/pr-title-lint");
39
+ const case_collision_1 = require("./checks/quality/case-collision");
40
+ const config_validation_1 = require("./checks/quality/config-validation");
41
+ const coverage_1 = require("./checks/quality/coverage");
42
+ const dead_code_1 = require("./checks/quality/dead-code");
43
+ const duplicate_check_1 = require("./checks/quality/duplicate-check");
44
+ const license_header_1 = require("./checks/quality/license-header");
45
+ const markdown_lint_1 = require("./checks/quality/markdown-lint");
46
+ const spellcheck_1 = require("./checks/quality/spellcheck");
47
+ const diff_security_1 = require("./checks/security/diff-security");
48
+ const dockerfile_security_1 = require("./checks/security/dockerfile-security");
49
+ const migration_safety_1 = require("./checks/security/migration-safety");
50
+ const security_1 = require("./checks/security/security");
51
+ const sensitive_file_guard_1 = require("./checks/security/sensitive-file-guard");
52
+ const workflow_security_1 = require("./checks/security/workflow-security");
32
53
  /**
33
54
  * Every check, in declaration order. The orchestrator groups them by `phase`
34
55
  * (blocking → informational → format) and runs each phase's checks in parallel.
35
56
  */
36
57
  exports.REGISTRY = [
37
58
  // blocking
59
+ merge_conflict_1.mergeConflictCheck,
38
60
  security_1.securityCheck,
39
61
  typecheck_1.typecheckCheck,
40
62
  lint_1.lintCheck,
@@ -45,6 +67,21 @@ exports.REGISTRY = [
45
67
  // informational
46
68
  diff_smell_1.diffSmellCheck,
47
69
  diff_security_1.diffSecurityCheck,
70
+ workflow_security_1.workflowSecurityCheck,
71
+ dockerfile_security_1.dockerfileSecurityCheck,
72
+ case_collision_1.caseCollisionCheck,
73
+ vuln_scan_1.vulnScanCheck,
74
+ custom_rules_1.customRulesCheck,
75
+ banned_imports_1.bannedImportsCheck,
76
+ license_header_1.licenseHeaderCheck,
77
+ config_validation_1.configValidationCheck,
78
+ leftover_debug_1.leftoverDebugCheck,
79
+ lockfile_drift_1.lockfileDriftCheck,
80
+ large_files_1.largeFilesCheck,
81
+ todo_fixme_1.todoFixmeCheck,
82
+ missing_tests_1.missingTestsCheck,
83
+ coverage_1.coverageCheck,
84
+ migration_safety_1.migrationSafetyCheck,
48
85
  sensitive_file_guard_1.sensitiveFileGuardCheck,
49
86
  pr_body_1.prBodyCheck,
50
87
  duplicate_check_1.duplicateCheck,
@@ -55,11 +92,19 @@ exports.REGISTRY = [
55
92
  markdown_lint_1.markdownLintCheck,
56
93
  spellcheck_1.spellcheckCheck,
57
94
  python_lint_1.pythonLintCheck,
95
+ python_typecheck_1.pythonTypecheckCheck,
58
96
  dead_code_1.deadCodeCheck,
59
97
  swift_lint_1.swiftLintCheck,
60
98
  kotlin_lint_1.kotlinLintCheck,
99
+ go_lint_1.goLintCheck,
100
+ rust_lint_1.rustLintCheck,
101
+ ruby_lint_1.rubyLintCheck,
61
102
  // format
62
103
  prettier_1.prettierCheck,
63
104
  cpp_format_1.cppFormatCheck,
64
105
  python_format_1.pythonFormatCheck,
106
+ go_format_1.goFormatCheck,
107
+ rust_format_1.rustFormatCheck,
108
+ csharp_format_1.csharpFormatCheck,
109
+ php_format_1.phpFormatCheck,
65
110
  ];
@@ -1,6 +1,7 @@
1
- import type { CheckReport } from './types';
1
+ import type { CheckReport, ReporterKind } from './types';
2
2
  /**
3
3
  * Emit the report through the selected channel.
4
- * none: nothing · stdout: console table · github: table + upsert PR comment.
4
+ * none: nothing · stdout: console table · github/gitlab/bitbucket: table +
5
+ * upsert the PR/MR comment · auto: detect the platform from the environment.
5
6
  */
6
- export declare function report(kind: 'none' | 'stdout' | 'github', result: CheckReport): Promise<void>;
7
+ export declare function report(kind: ReporterKind, result: CheckReport): Promise<void>;
@@ -69,6 +69,8 @@ function printStdout(report) {
69
69
  utils_1.logger.info(SUMMARY_RULE);
70
70
  utils_1.logger.info(` ✅ ${passed} passed ⚠️ ${warned} warnings ❌ ${failed} failed ⏭️ ${skipped} skipped`);
71
71
  utils_1.logger.info(SUMMARY_BORDER);
72
+ // Always shown under the report — the docs cover every check and config key.
73
+ utils_1.logger.info(' DOCUMENTATION: https://pr-checkmate-docs.pages.dev/');
72
74
  }
73
75
  async function upsertGithubComment(body) {
74
76
  const ctx = getPrContext();
@@ -110,15 +112,115 @@ async function upsertGithubComment(body) {
110
112
  utils_1.logger.warn(`[reporter]: ⚠️ Could not post GitHub comment: ${err}`);
111
113
  }
112
114
  }
115
+ /**
116
+ * Upsert a GitLab MR note (find our marker → update, else create). Uses the
117
+ * standard CI variables plus a `GITLAB_TOKEN` with `api` scope. Skips gracefully
118
+ * when not in an MR pipeline or the token is missing.
119
+ */
120
+ async function upsertGitlabNote(body) {
121
+ const base = process.env.CI_API_V4_URL;
122
+ const project = process.env.CI_PROJECT_ID;
123
+ const iid = process.env.CI_MERGE_REQUEST_IID;
124
+ const token = process.env.GITLAB_TOKEN;
125
+ if (!base || !project || !iid || !token) {
126
+ utils_1.logger.info('[reporter]: Skipping GitLab note (not an MR pipeline or GITLAB_TOKEN missing)');
127
+ return;
128
+ }
129
+ const notes = `${base}/projects/${project}/merge_requests/${iid}/notes`;
130
+ const headers = { 'PRIVATE-TOKEN': token, 'Content-Type': 'application/json' };
131
+ try {
132
+ const listRes = await fetch(notes, { headers });
133
+ if (listRes.ok) {
134
+ const existing = (await listRes.json()).find(n => typeof n.body === 'string' && n.body.includes(COMMENT_MARKER));
135
+ if (existing) {
136
+ await fetch(`${notes}/${existing.id}`, {
137
+ method: 'PUT',
138
+ headers,
139
+ body: JSON.stringify({ body }),
140
+ });
141
+ utils_1.logger.info('[reporter]: ✅ Updated PR CheckMate note on GitLab MR');
142
+ return;
143
+ }
144
+ }
145
+ await fetch(notes, { method: 'POST', headers, body: JSON.stringify({ body }) });
146
+ utils_1.logger.info('[reporter]: ✅ Posted PR CheckMate summary to GitLab MR');
147
+ }
148
+ catch (err) {
149
+ utils_1.logger.warn(`[reporter]: ⚠️ Could not post GitLab note: ${err}`);
150
+ }
151
+ }
152
+ /**
153
+ * Upsert a Bitbucket PR comment (find our marker → update, else create). Uses
154
+ * Bitbucket Pipelines variables plus a `BITBUCKET_TOKEN` (repo/PR write). Skips
155
+ * gracefully when not in a PR pipeline or the token is missing.
156
+ */
157
+ async function upsertBitbucketComment(body) {
158
+ const fullName = process.env.BITBUCKET_REPO_FULL_NAME;
159
+ const prId = process.env.BITBUCKET_PR_ID;
160
+ const token = process.env.BITBUCKET_TOKEN;
161
+ if (!fullName || !prId || !token) {
162
+ utils_1.logger.info('[reporter]: Skipping Bitbucket comment (not a PR pipeline or BITBUCKET_TOKEN missing)');
163
+ return;
164
+ }
165
+ const commentsUrl = `https://api.bitbucket.org/2.0/repositories/${fullName}/pullrequests/${prId}/comments`;
166
+ const headers = { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' };
167
+ try {
168
+ const listRes = await fetch(`${commentsUrl}?pagelen=100`, { headers });
169
+ if (listRes.ok) {
170
+ const values = (await listRes.json()).values;
171
+ const existing = values?.find(c => c.content?.raw?.includes(COMMENT_MARKER));
172
+ if (existing) {
173
+ await fetch(`${commentsUrl}/${existing.id}`, {
174
+ method: 'PUT',
175
+ headers,
176
+ body: JSON.stringify({ content: { raw: body } }),
177
+ });
178
+ utils_1.logger.info('[reporter]: ✅ Updated PR CheckMate comment on Bitbucket PR');
179
+ return;
180
+ }
181
+ }
182
+ await fetch(commentsUrl, {
183
+ method: 'POST',
184
+ headers,
185
+ body: JSON.stringify({ content: { raw: body } }),
186
+ });
187
+ utils_1.logger.info('[reporter]: ✅ Posted PR CheckMate summary to Bitbucket PR');
188
+ }
189
+ catch (err) {
190
+ utils_1.logger.warn(`[reporter]: ⚠️ Could not post Bitbucket comment: ${err}`);
191
+ }
192
+ }
193
+ /** Detect the CI platform from the environment, or null when unknown/local. */
194
+ function detectPlatform() {
195
+ if (process.env.GITHUB_ACTIONS || process.env.GITHUB_EVENT_NAME)
196
+ return 'github';
197
+ if (process.env.GITLAB_CI)
198
+ return 'gitlab';
199
+ if (process.env.BITBUCKET_PIPELINE_UUID || process.env.BITBUCKET_PR_ID)
200
+ return 'bitbucket';
201
+ return null;
202
+ }
203
+ /** Post the markdown report to a specific platform's PR/MR. */
204
+ async function postToPlatform(platform, result) {
205
+ const body = buildMarkdownReport(result);
206
+ if (platform === 'github')
207
+ return upsertGithubComment(body);
208
+ if (platform === 'gitlab')
209
+ return upsertGitlabNote(body);
210
+ return upsertBitbucketComment(body);
211
+ }
113
212
  /**
114
213
  * Emit the report through the selected channel.
115
- * none: nothing · stdout: console table · github: table + upsert PR comment.
214
+ * none: nothing · stdout: console table · github/gitlab/bitbucket: table +
215
+ * upsert the PR/MR comment · auto: detect the platform from the environment.
116
216
  */
117
217
  async function report(kind, result) {
118
218
  if (kind === 'none')
119
219
  return;
120
220
  printStdout(result);
121
- if (kind === 'github' && result.results.length > 0) {
122
- await upsertGithubComment(buildMarkdownReport(result));
123
- }
221
+ if (result.results.length === 0)
222
+ return;
223
+ const platform = kind === 'auto' ? detectPlatform() : kind === 'stdout' ? null : kind;
224
+ if (platform)
225
+ await postToPlatform(platform, result);
124
226
  }
@@ -50,6 +50,20 @@ function selectChecks(checks, opts) {
50
50
  function errMessage(err) {
51
51
  return err instanceof Error ? err.message.split('\n')[0].slice(0, 160) : String(err);
52
52
  }
53
+ /**
54
+ * Apply the universal `config.severity` override for a check. Only remaps
55
+ * between warn↔fail (pass/skip are never changed); `off` is handled earlier by
56
+ * excluding the check from the run entirely. This is what lets a repo turn any
57
+ * advisory check into a gate, or downgrade a blocking one, by name.
58
+ */
59
+ function applySeverity(name, status, ctx) {
60
+ const sev = ctx.config.severity?.[name];
61
+ if (sev === 'error' && status === 'warn')
62
+ return 'fail';
63
+ if (sev === 'warn' && status === 'fail')
64
+ return 'warn';
65
+ return status;
66
+ }
53
67
  /** Run one check: gate on `applies`, execute, translate throws into `fail`. */
54
68
  async function runOne(check, ctx, collector) {
55
69
  try {
@@ -63,12 +77,13 @@ async function runOne(check, ctx, collector) {
63
77
  return;
64
78
  }
65
79
  const outcome = await check.run(ctx);
66
- collector.register(check.name, outcome.status, outcome.details);
80
+ collector.register(check.name, applySeverity(check.name, outcome.status, ctx), outcome.details);
67
81
  }
68
82
  catch (err) {
69
83
  // An unexpected throw in a blocking check fails the run; in informational/format
70
84
  // phases it only warns — those phases never block, matching the legacy pipeline.
71
- const status = check.phase === 'blocking' ? 'fail' : 'warn';
85
+ const base = check.phase === 'blocking' ? 'fail' : 'warn';
86
+ const status = applySeverity(check.name, base, ctx);
72
87
  utils_1.logger.error(`[runChecks]: ${status === 'fail' ? '❌' : '⚠️'} ${check.name} threw: ${err}`);
73
88
  collector.register(check.name, status, errMessage(err));
74
89
  }
@@ -82,7 +97,10 @@ async function runOne(check, ctx, collector) {
82
97
  */
83
98
  async function runChecks(opts = {}) {
84
99
  const ctx = await (0, context_1.buildContext)(opts);
85
- const selected = selectChecks(resolveChecks(opts), opts);
100
+ // `severity: { "<name>": "off" }` disables any check by name, universally —
101
+ // a superset of the per-check `enabled: false` flags.
102
+ const off = ctx.config.severity ?? {};
103
+ const selected = selectChecks(resolveChecks(opts), opts).filter(c => off[c.name] !== 'off');
86
104
  const collector = new result_collector_1.ResultCollector();
87
105
  for (const phase of types_1.PHASES) {
88
106
  const inPhase = selected.filter(c => c.phase === phase);
@@ -6,7 +6,10 @@ import type { CheckContext } from './types';
6
6
  * `null` when git itself is unusable (caller should `skip`).
7
7
  *
8
8
  * Only ACMR files are returned (Added/Copied/Modified/Renamed) — deletions are
9
- * excluded, so every returned path exists on disk and is safe to read.
9
+ * excluded. Results are additionally filtered to paths that still exist on disk,
10
+ * so a rename/move within the diff range (where the old path shows up but no
11
+ * longer exists) can never hand a missing path to a downstream tool like ESLint
12
+ * or Prettier, which crash hard on a non-existent pattern.
10
13
  */
11
14
  export declare function resolveTargetFiles(ctx: CheckContext, globs: string[]): Promise<string[] | null>;
12
15
  /**
@@ -1,8 +1,13 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.resolveTargetFiles = resolveTargetFiles;
4
7
  exports.resolveScopedTargetFiles = resolveScopedTargetFiles;
5
8
  exports.listTrackedFiles = listTrackedFiles;
9
+ const node_fs_1 = __importDefault(require("node:fs"));
10
+ const node_path_1 = __importDefault(require("node:path"));
6
11
  const config_1 = require("../config");
7
12
  const utils_1 = require("../utils");
8
13
  const execa_1 = require("../utils/execa");
@@ -25,7 +30,10 @@ function excludeIgnoredDirs(files) {
25
30
  * `null` when git itself is unusable (caller should `skip`).
26
31
  *
27
32
  * Only ACMR files are returned (Added/Copied/Modified/Renamed) — deletions are
28
- * excluded, so every returned path exists on disk and is safe to read.
33
+ * excluded. Results are additionally filtered to paths that still exist on disk,
34
+ * so a rename/move within the diff range (where the old path shows up but no
35
+ * longer exists) can never hand a missing path to a downstream tool like ESLint
36
+ * or Prettier, which crash hard on a non-existent pattern.
29
37
  */
30
38
  async function resolveTargetFiles(ctx, globs) {
31
39
  try {
@@ -40,11 +48,16 @@ async function resolveTargetFiles(ctx, globs) {
40
48
  ...globs,
41
49
  ]
42
50
  : ['ls-files', ...globs];
43
- const { stdout } = await (0, execa_1.execa)('git', args, { cwd: ctx.cwd, reject: false });
51
+ const cacheKey = `t:${args.join('')}`;
52
+ let stdout = ctx.cache?.get(cacheKey);
53
+ if (stdout === undefined) {
54
+ stdout = (await (0, execa_1.execa)('git', args, { cwd: ctx.cwd, reject: false })).stdout;
55
+ ctx.cache?.set(cacheKey, stdout);
56
+ }
44
57
  const files = excludeIgnoredDirs(stdout
45
58
  .split('\n')
46
59
  .map(f => f.trim())
47
- .filter(Boolean));
60
+ .filter(Boolean)).filter(f => node_fs_1.default.existsSync(node_path_1.default.join(ctx.cwd, f)));
48
61
  utils_1.logger.info(`[targets]: ${ctx.baseSha ? '📂 Delta' : 'Full scan'}: ${files.length} file(s) for ${globs.join(', ')}`);
49
62
  return files;
50
63
  }
@@ -24,6 +24,12 @@ export interface CheckContext {
24
24
  /** Diff range for delta-aware checks; undefined → full-repo scan. */
25
25
  baseSha?: string;
26
26
  headSha?: string;
27
+ /**
28
+ * Per-run memoization cache for identical git invocations (many checks diff
29
+ * the same range). Created once per `runChecks`; safe because git state is
30
+ * stable within a run. Keyed by the git args.
31
+ */
32
+ cache?: Map<string, string>;
27
33
  }
28
34
  /** Outcome of a single check — the name is attached by the orchestrator. */
29
35
  export interface CheckOutcome {
@@ -42,7 +48,7 @@ export interface Check {
42
48
  applies(ctx: CheckContext): boolean | Promise<boolean>;
43
49
  run(ctx: CheckContext): Promise<CheckOutcome>;
44
50
  }
45
- export type ReporterKind = 'none' | 'stdout' | 'github';
51
+ export type ReporterKind = 'none' | 'stdout' | 'github' | 'gitlab' | 'bitbucket' | 'auto';
46
52
  /** Custom reporter: receives the final report to emit however the consumer wants. */
47
53
  export type ReporterFn = (report: CheckReport) => void | Promise<void>;
48
54
  export interface RunOptions {
@@ -78,6 +84,12 @@ export interface RunOptions {
78
84
  config?: PRCheckMateConfig;
79
85
  baseSha?: string;
80
86
  headSha?: string;
87
+ /**
88
+ * Pre-commit / local mode: check the staged changes (index vs HEAD) instead of
89
+ * a PR range. Implemented by diffing HEAD against the index tree
90
+ * (`git write-tree`), so every delta-aware check works unchanged.
91
+ */
92
+ staged?: boolean;
81
93
  }
82
94
  export interface CheckReport {
83
95
  results: CheckResult[];
package/dist/init.js CHANGED
@@ -28,6 +28,18 @@ function loadDefaultEslintRules() {
28
28
  * Default configuration structure with customization options
29
29
  */
30
30
  const DEFAULT_CONFIG = {
31
+ // Points editors (VS Code, etc.) at the bundled JSON schema for autocomplete
32
+ // and inline validation of this file.
33
+ $schema: './node_modules/pr-checkmate/pr-checkmate.schema.json',
34
+ // Human-readable hints. JSON has no comments, so these `//`-prefixed keys are
35
+ // just informational strings — PR CheckMate ignores them at runtime.
36
+ '// reporting': [
37
+ 'Results post to the PR/MR automatically.',
38
+ 'GitHub: no setup — the runner injects GITHUB_TOKEN; just keep `permissions: pull-requests: write` in the workflow.',
39
+ 'GitLab: create a token (Project → Settings → Access Tokens, scope `api`) and add it as CI/CD variable GITLAB_TOKEN.',
40
+ 'Bitbucket: create a token (Repo → Settings → Access tokens, scope pull-requests:write) and add it as repo variable BITBUCKET_TOKEN.',
41
+ 'The token lives in YOUR CI variables, never in this file. Without it, checks still run and print to the log — only the comment is skipped.',
42
+ ].join(' '),
31
43
  sourcePath: '.',
32
44
  // Directories excluded from every check (matched on any path segment). Edit
33
45
  // freely — add your own generated/vendored dirs or remove ones you want scanned.
@@ -94,6 +106,57 @@ const DEFAULT_CONFIG = {
94
106
  // test runner. Add barrels/entry points here to exempt intentional exports.
95
107
  ignoreFiles: config_1.DEAD_CODE_IGNORE_DEFAULTS,
96
108
  },
109
+ // User-defined rules: regex patterns run against added diff lines. The escape
110
+ // hatch to enforce any project rule without code. Example entry:
111
+ // { "name": "no-focused-tests", "pattern": "\\b(describe|it)\\.only\\(",
112
+ // "message": "focused test left in", "severity": "error",
113
+ // "include": ["src/**"], "exclude": ["**/*.test.*"] }
114
+ customRules: [],
115
+ // Universal per-check severity override, keyed by the check's display name (as
116
+ // shown in the report). "error" fails the run, "warn" is advisory, "off"
117
+ // disables the check. Empty by default — every check uses its built-in
118
+ // severity. Examples: { "Merge Conflict": "warn", "ESLint": "error" }.
119
+ severity: {},
120
+ // Git / PR-diff heuristics (delta-aware). Merge Conflict is blocking by
121
+ // default; the rest are advisory — promote any of them with `severity`.
122
+ mergeConflict: { enabled: true },
123
+ leftoverDebug: { enabled: true },
124
+ lockfileDrift: { enabled: true },
125
+ largeFiles: { enabled: true, maxSizeKb: 500, allow: [] },
126
+ missingTests: { enabled: true },
127
+ todoFixme: { enabled: true, requireTicket: false },
128
+ migrationSafety: { enabled: true },
129
+ // Security (regex diff scans + sensitive-file guard). Secret scan / npm audit
130
+ // live under `security`.
131
+ diffSecurity: { enabled: true },
132
+ sensitiveFileGuard: { enabled: true },
133
+ workflowSecurity: { enabled: true }, // GitHub Actions: injection, unpinned actions
134
+ dockerfileSecurity: { enabled: true }, // Dockerfile: :latest, USER root, curl|bash, …
135
+ caseCollision: { enabled: true }, // files differing only by case
136
+ vulnScan: { enabled: false }, // cross-language dep vulns (osv-scanner, runner-dependency)
137
+ // Test-coverage presence. Opt-in: enable once your CI produces a coverage
138
+ // report (lcov/cobertura/jacoco/…); warns if none is found.
139
+ coverage: { enabled: false },
140
+ // PR metadata / dependency hygiene.
141
+ prSize: { enabled: true, maxFiles: 50, maxLines: 1000 },
142
+ commitlint: { enabled: true },
143
+ // Dependencies you want to forbid. Example: { "name": "moment", "message": "use date-fns" }.
144
+ bannedImports: [],
145
+ // License/copyright header on new source files. Opt-in — set enabled + pattern.
146
+ licenseHeader: { enabled: false, pattern: '', lines: 10 },
147
+ outdatedDeps: { enabled: true, ignore: [] },
148
+ // Per-language checks. Bundled (no client install): TypeScript, Python (Ruff),
149
+ // C++ (clang-format). Runner-dependency (need the tool on the runner, skip
150
+ // gracefully if absent): Swift, Kotlin, Go, Rust, C#, Ruby, PHP.
151
+ python: { enabled: true, typecheck: true },
152
+ cpp: { enabled: true },
153
+ swift: { enabled: true },
154
+ kotlin: { enabled: true },
155
+ go: { enabled: true },
156
+ rust: { enabled: true },
157
+ csharp: { enabled: true },
158
+ ruby: { enabled: true },
159
+ php: { enabled: true },
97
160
  documentation: config_1.documentation,
98
161
  };
99
162
  const MANUAL_ENTRY = 'Enter manually';
@@ -169,6 +232,25 @@ async function createConfig(root) {
169
232
  sourcePath,
170
233
  languages,
171
234
  };
235
+ // Omit config blocks for languages not present in the repo — a cleaner,
236
+ // tailored file. This does NOT disable them: the key is simply absent, which
237
+ // still defaults to enabled, and runtime language detection gates whether each
238
+ // check runs. So a language added later auto-activates without editing config.
239
+ const LANGUAGE_TOGGLE_KEYS = [
240
+ 'python',
241
+ 'cpp',
242
+ 'swift',
243
+ 'kotlin',
244
+ 'go',
245
+ 'rust',
246
+ 'csharp',
247
+ 'ruby',
248
+ 'php',
249
+ ];
250
+ for (const key of LANGUAGE_TOGGLE_KEYS) {
251
+ if (!languages.includes(key))
252
+ delete config[key];
253
+ }
172
254
  fs_1.default.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
173
255
  utils_1.logger.info(`[init]: ✅ Created pr-checkmate.json with sourcePath: ${sourcePath}`);
174
256
  utils_1.logger.info(`[init]: 💡 You can now customize ESLint rules, Prettier settings,
@@ -24,6 +24,16 @@ export interface PRCheckMateConfig {
24
24
  sourcePath: string | string[];
25
25
  ignoreDirs?: string[];
26
26
  languages?: Language[];
27
+ customRules?: Array<{
28
+ name: string;
29
+ pattern: string;
30
+ message?: string;
31
+ severity?: 'error' | 'warn';
32
+ include?: string[];
33
+ exclude?: string[];
34
+ flags?: string;
35
+ }>;
36
+ severity?: Record<string, 'error' | 'warn' | 'off'>;
27
37
  lint?: {
28
38
  rules?: Record<string, any>;
29
39
  ignorePatterns?: string[];
@@ -39,6 +49,7 @@ export interface PRCheckMateConfig {
39
49
  security?: {
40
50
  gitleaks?: {
41
51
  enabled: boolean;
52
+ version?: string;
42
53
  };
43
54
  'npm-audit'?: {
44
55
  enabled: boolean;
@@ -60,10 +71,52 @@ export interface PRCheckMateConfig {
60
71
  lint?: boolean;
61
72
  format?: boolean;
62
73
  };
74
+ typecheck?: boolean;
75
+ typeChecker?: 'mypy' | 'pyright';
63
76
  };
64
77
  diffSmell?: {
65
78
  enabled?: boolean;
66
79
  };
80
+ prSize?: {
81
+ enabled?: boolean;
82
+ maxFiles?: number;
83
+ maxLines?: number;
84
+ };
85
+ leftoverDebug?: {
86
+ enabled?: boolean;
87
+ severity?: 'error' | 'warn';
88
+ };
89
+ todoFixme?: {
90
+ enabled?: boolean;
91
+ severity?: 'error' | 'warn';
92
+ requireTicket?: boolean;
93
+ keywords?: string[];
94
+ };
95
+ lockfileDrift?: {
96
+ enabled?: boolean;
97
+ severity?: 'error' | 'warn';
98
+ };
99
+ missingTests?: {
100
+ enabled?: boolean;
101
+ severity?: 'error' | 'warn';
102
+ ignore?: string[];
103
+ };
104
+ migrationSafety?: {
105
+ enabled?: boolean;
106
+ severity?: 'error' | 'warn';
107
+ paths?: string[];
108
+ };
109
+ coverage?: {
110
+ enabled?: boolean;
111
+ severity?: 'error' | 'warn';
112
+ reportPath?: string | string[];
113
+ };
114
+ largeFiles?: {
115
+ enabled?: boolean;
116
+ severity?: 'error' | 'warn';
117
+ maxSizeKb?: number;
118
+ allow?: string[];
119
+ };
67
120
  prBody?: {
68
121
  enabled?: boolean;
69
122
  minLength?: number;
@@ -101,6 +154,7 @@ export interface PRCheckMateConfig {
101
154
  };
102
155
  outdatedDeps?: {
103
156
  enabled?: boolean;
157
+ timeoutMs?: number;
104
158
  /**
105
159
  * Exact package names to exclude from the outdated-deps warning, e.g.
106
160
  * "commander". Use for dependencies deliberately pinned behind their latest
@@ -122,6 +176,38 @@ export interface PRCheckMateConfig {
122
176
  };
123
177
  diffSecurity?: {
124
178
  enabled?: boolean;
179
+ ignore?: string[];
180
+ };
181
+ mergeConflict?: {
182
+ enabled?: boolean;
183
+ severity?: 'error' | 'warn';
184
+ };
185
+ workflowSecurity?: {
186
+ enabled?: boolean;
187
+ ignore?: string[];
188
+ };
189
+ dockerfileSecurity?: {
190
+ enabled?: boolean;
191
+ ignore?: string[];
192
+ };
193
+ caseCollision?: {
194
+ enabled?: boolean;
195
+ };
196
+ configValidation?: {
197
+ enabled?: boolean;
198
+ };
199
+ vulnScan?: {
200
+ enabled?: boolean;
201
+ };
202
+ bannedImports?: Array<{
203
+ name: string;
204
+ message?: string;
205
+ severity?: 'error' | 'warn';
206
+ }>;
207
+ licenseHeader?: {
208
+ enabled?: boolean;
209
+ pattern?: string;
210
+ lines?: number;
125
211
  };
126
212
  sensitiveFileGuard?: {
127
213
  enabled?: boolean;
@@ -132,6 +218,21 @@ export interface PRCheckMateConfig {
132
218
  kotlin?: {
133
219
  enabled?: boolean;
134
220
  };
221
+ go?: {
222
+ enabled?: boolean;
223
+ };
224
+ rust?: {
225
+ enabled?: boolean;
226
+ };
227
+ csharp?: {
228
+ enabled?: boolean;
229
+ };
230
+ ruby?: {
231
+ enabled?: boolean;
232
+ };
233
+ php?: {
234
+ enabled?: boolean;
235
+ };
135
236
  deadCode?: {
136
237
  enabled?: boolean;
137
238
  /**