pr-checkmate 3.2.3 → 4.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 (174) hide show
  1. package/.eslintrc.js +3 -9
  2. package/LICENSE +8 -3
  3. package/README.md +54 -5
  4. package/dist/cjs/agent/hook.d.ts +37 -0
  5. package/dist/cjs/agent/hook.js +175 -0
  6. package/dist/cjs/agent/registration.d.ts +10 -0
  7. package/dist/cjs/agent/registration.js +28 -0
  8. package/dist/cjs/agent/setup.d.ts +20 -0
  9. package/dist/cjs/agent/setup.js +229 -0
  10. package/dist/cjs/cli.js +101 -0
  11. package/dist/cjs/config/constants.d.ts +2 -0
  12. package/dist/cjs/config/constants.js +5 -1
  13. package/dist/cjs/config/pr-checkmate.template.json +11 -1
  14. package/dist/cjs/core/checks/dependencies/grype-scan.js +18 -0
  15. package/dist/cjs/core/checks/findings.js +33 -7
  16. package/dist/cjs/core/checks/git/large-files.d.ts +1 -0
  17. package/dist/cjs/core/checks/git/large-files.js +3 -3
  18. package/dist/cjs/core/checks/languages/lint.js +12 -0
  19. package/dist/cjs/core/checks/languages/ruff-shared.js +6 -1
  20. package/dist/cjs/core/checks/pr/commitlint.js +4 -2
  21. package/dist/cjs/core/checks/pr/pr-size-check.d.ts +2 -0
  22. package/dist/cjs/core/checks/pr/pr-size-check.js +5 -5
  23. package/dist/cjs/core/checks/quality/package-size.d.ts +23 -1
  24. package/dist/cjs/core/checks/quality/package-size.js +20 -4
  25. package/dist/cjs/core/checks/quality/spellcheck.js +8 -5
  26. package/dist/cjs/core/checks/security/agent-instructions.d.ts +4 -0
  27. package/dist/cjs/core/checks/security/agent-instructions.js +149 -0
  28. package/dist/cjs/core/checks/security/diff-security.d.ts +7 -0
  29. package/dist/cjs/core/checks/security/diff-security.js +21 -1
  30. package/dist/cjs/core/checks/security/security.d.ts +0 -13
  31. package/dist/cjs/core/checks/security/security.js +27 -0
  32. package/dist/cjs/core/checks/security/workflow-security.js +33 -0
  33. package/dist/cjs/core/commands.d.ts +6 -0
  34. package/dist/cjs/core/commands.js +11 -1
  35. package/dist/cjs/core/finding-store.d.ts +16 -0
  36. package/dist/cjs/core/finding-store.js +73 -0
  37. package/dist/cjs/core/install/doctor.js +12 -0
  38. package/dist/cjs/core/install/grype-db.d.ts +25 -0
  39. package/dist/cjs/core/install/grype-db.js +45 -0
  40. package/dist/cjs/core/install/profiles.d.ts +1 -1
  41. package/dist/cjs/core/install/profiles.js +1 -1
  42. package/dist/cjs/core/install/reuse.js +6 -3
  43. package/dist/cjs/core/registry.js +2 -0
  44. package/dist/cjs/core/reporters.js +9 -0
  45. package/dist/cjs/core/run-checks.js +15 -5
  46. package/dist/cjs/core/sarif.d.ts +13 -0
  47. package/dist/cjs/core/sarif.js +60 -0
  48. package/dist/cjs/core/types.d.ts +7 -2
  49. package/dist/cjs/index.d.ts +2 -1
  50. package/dist/cjs/index.js +4 -1
  51. package/dist/cjs/init.d.ts +0 -8
  52. package/dist/cjs/init.js +12 -2
  53. package/dist/cjs/interfaces/index.d.ts +12 -0
  54. package/dist/cjs/mcp/config-tools.d.ts +30 -0
  55. package/dist/cjs/mcp/config-tools.js +320 -0
  56. package/dist/cjs/mcp/dependency.d.ts +33 -0
  57. package/dist/cjs/mcp/dependency.js +407 -0
  58. package/dist/cjs/mcp/protocol.d.ts +97 -0
  59. package/dist/cjs/mcp/protocol.js +181 -0
  60. package/dist/cjs/mcp/review.d.ts +63 -0
  61. package/dist/cjs/mcp/review.js +150 -0
  62. package/dist/cjs/mcp/serve.d.ts +8 -0
  63. package/dist/cjs/mcp/serve.js +49 -0
  64. package/dist/cjs/mcp/settings.d.ts +13 -0
  65. package/dist/cjs/mcp/settings.js +50 -0
  66. package/dist/cjs/mcp/tools.d.ts +23 -0
  67. package/dist/cjs/mcp/tools.js +633 -0
  68. package/dist/cjs/utils/execa.d.ts +7 -0
  69. package/dist/cjs/utils/execa.js +41 -1
  70. package/dist/cjs/utils/own-deps.js +8 -1
  71. package/dist/cjs/utils/resolve-bin.d.ts +2 -0
  72. package/dist/cjs/utils/resolve-bin.js +5 -0
  73. package/dist/cjs/utils/result-collector.d.ts +17 -1
  74. package/dist/cjs/utils/result-collector.js +7 -2
  75. package/dist/esm/agent/hook.d.ts +37 -0
  76. package/dist/esm/agent/hook.js +167 -0
  77. package/dist/esm/agent/registration.d.ts +10 -0
  78. package/dist/esm/agent/registration.js +21 -0
  79. package/dist/esm/agent/setup.d.ts +20 -0
  80. package/dist/esm/agent/setup.js +220 -0
  81. package/dist/esm/cli.js +100 -2
  82. package/dist/esm/config/constants.d.ts +2 -0
  83. package/dist/esm/config/constants.js +4 -0
  84. package/dist/esm/config/pr-checkmate.template.json +11 -1
  85. package/dist/esm/core/checks/dependencies/dependency-check.js +2 -2
  86. package/dist/esm/core/checks/dependencies/grype-scan.js +18 -0
  87. package/dist/esm/core/checks/dependencies/license-check.js +2 -2
  88. package/dist/esm/core/checks/dependencies/npm-audit.js +2 -2
  89. package/dist/esm/core/checks/findings.js +33 -7
  90. package/dist/esm/core/checks/git/diff-smell.js +1 -1
  91. package/dist/esm/core/checks/git/large-files.d.ts +1 -0
  92. package/dist/esm/core/checks/git/large-files.js +1 -1
  93. package/dist/esm/core/checks/languages/cpp-format.js +1 -1
  94. package/dist/esm/core/checks/languages/lint.js +12 -3
  95. package/dist/esm/core/checks/languages/prettier.js +3 -3
  96. package/dist/esm/core/checks/languages/python-format.js +2 -2
  97. package/dist/esm/core/checks/languages/python-lint.js +2 -2
  98. package/dist/esm/core/checks/languages/ruff-shared.js +6 -1
  99. package/dist/esm/core/checks/languages/typecheck.js +1 -1
  100. package/dist/esm/core/checks/pr/commitlint.js +4 -2
  101. package/dist/esm/core/checks/pr/pr-body.js +1 -1
  102. package/dist/esm/core/checks/pr/pr-size-check.d.ts +2 -0
  103. package/dist/esm/core/checks/pr/pr-size-check.js +3 -3
  104. package/dist/esm/core/checks/pr/pr-title-lint.js +1 -1
  105. package/dist/esm/core/checks/quality/dead-code.js +1 -1
  106. package/dist/esm/core/checks/quality/duplicate-check.js +1 -1
  107. package/dist/esm/core/checks/quality/markdown-lint.js +1 -1
  108. package/dist/esm/core/checks/quality/package-size.d.ts +23 -1
  109. package/dist/esm/core/checks/quality/package-size.js +19 -4
  110. package/dist/esm/core/checks/quality/spellcheck.js +11 -8
  111. package/dist/esm/core/checks/security/agent-instructions.d.ts +4 -0
  112. package/dist/esm/core/checks/security/agent-instructions.js +146 -0
  113. package/dist/esm/core/checks/security/diff-security.d.ts +7 -0
  114. package/dist/esm/core/checks/security/diff-security.js +21 -1
  115. package/dist/esm/core/checks/security/security.d.ts +0 -13
  116. package/dist/esm/core/checks/security/security.js +28 -1
  117. package/dist/esm/core/checks/security/workflow-security.js +33 -0
  118. package/dist/esm/core/commands.d.ts +6 -0
  119. package/dist/esm/core/commands.js +10 -0
  120. package/dist/esm/core/finding-store.d.ts +16 -0
  121. package/dist/esm/core/finding-store.js +67 -0
  122. package/dist/esm/core/install/doctor.js +12 -0
  123. package/dist/esm/core/install/grype-db.d.ts +25 -0
  124. package/dist/esm/core/install/grype-db.js +41 -0
  125. package/dist/esm/core/install/prepare.js +1 -1
  126. package/dist/esm/core/install/profiles.d.ts +1 -1
  127. package/dist/esm/core/install/profiles.js +1 -1
  128. package/dist/esm/core/install/reuse.js +6 -3
  129. package/dist/esm/core/registry.js +2 -0
  130. package/dist/esm/core/reporters.js +9 -0
  131. package/dist/esm/core/run-checks.js +17 -7
  132. package/dist/esm/core/sarif.d.ts +13 -0
  133. package/dist/esm/core/sarif.js +57 -0
  134. package/dist/esm/core/types.d.ts +7 -2
  135. package/dist/esm/index.d.ts +2 -1
  136. package/dist/esm/index.js +2 -0
  137. package/dist/esm/init.d.ts +0 -8
  138. package/dist/esm/init.js +15 -4
  139. package/dist/esm/interfaces/index.d.ts +12 -0
  140. package/dist/esm/mcp/config-tools.d.ts +30 -0
  141. package/dist/esm/mcp/config-tools.js +310 -0
  142. package/dist/esm/mcp/dependency.d.ts +33 -0
  143. package/dist/esm/mcp/dependency.js +402 -0
  144. package/dist/esm/mcp/protocol.d.ts +97 -0
  145. package/dist/esm/mcp/protocol.js +175 -0
  146. package/dist/esm/mcp/review.d.ts +63 -0
  147. package/dist/esm/mcp/review.js +139 -0
  148. package/dist/esm/mcp/serve.d.ts +8 -0
  149. package/dist/esm/mcp/serve.js +43 -0
  150. package/dist/esm/mcp/settings.d.ts +13 -0
  151. package/dist/esm/mcp/settings.js +43 -0
  152. package/dist/esm/mcp/tools.d.ts +23 -0
  153. package/dist/esm/mcp/tools.js +626 -0
  154. package/dist/esm/utils/execa.d.ts +7 -0
  155. package/dist/esm/utils/execa.js +40 -1
  156. package/dist/esm/utils/own-deps.js +8 -1
  157. package/dist/esm/utils/resolve-bin.d.ts +2 -0
  158. package/dist/esm/utils/resolve-bin.js +4 -0
  159. package/dist/esm/utils/result-collector.d.ts +17 -1
  160. package/dist/esm/utils/result-collector.js +7 -2
  161. package/eslint-default-rules.json +2 -8
  162. package/eslint.config.mjs +37 -8
  163. package/install-manifests/commitlint/package-lock.json +5 -4
  164. package/install-manifests/core/package-lock.json +5 -3
  165. package/install-manifests/js/package-lock.json +250 -571
  166. package/install-manifests/js/package.json +10 -8
  167. package/install-manifests/markdown/package-lock.json +16 -13
  168. package/install-manifests/markdown/package.json +3 -0
  169. package/install-manifests/spellcheck/package-lock.json +3 -3
  170. package/package.json +24 -16
  171. package/pr-checkmate.schema.json +35 -0
  172. package/src/config/.eslintrc.js +3 -9
  173. package/src/config/eslint-default-rules.json +2 -8
  174. package/src/config/pr-checkmate.schema.json +35 -0
@@ -0,0 +1,310 @@
1
+ /**
2
+ * Setting pr-checkmate up for one specific repository, by the agent that is
3
+ * already sitting in it.
4
+ *
5
+ * The slow part of adopting a gate is not turning it on; it is deciding what to do
6
+ * about the findings an existing codebase already has. The documented answer is a
7
+ * strict workflow with a permissive config, then promoting checks one at a time as
8
+ * they go clean. That is a procedure an agent can carry out — if it can see the
9
+ * baseline — so these tools hand it exactly that: what the repository looks like,
10
+ * what it fails today, and a config that makes the gate real from day one without
11
+ * turning the first run into three thousand red lines.
12
+ *
13
+ * Nothing here writes except `write_config`, and that one validates first, merges
14
+ * over what is already there, and never replaces a value the user chose.
15
+ */
16
+ import fs from 'node:fs';
17
+ import path from 'node:path';
18
+ import { IGNORED_DIRS } from '../config/constants.js';
19
+ import { loadProjectConfig } from '../config/loader.js';
20
+ import { suggestName } from '../core/checks/quality/config-validation.js';
21
+ import { REGISTRY } from '../core/registry.js';
22
+ import { detectLanguageUsage } from '../utils/detect-language.js';
23
+ import { findOwnPackageRoot } from '../utils/own-deps.js';
24
+ import { review } from './review.js';
25
+ const CONFIG_FILE = 'pr-checkmate.json';
26
+ const LEVELS = new Set(['error', 'warn', 'off']);
27
+ const FAIL_ON = new Set(['error', 'warn', 'never']);
28
+ /** Directories that hold code nobody on the team wrote by hand. Not in the default ignore list. */
29
+ const GENERATED_DIRS = ['generated', '__generated__', 'vendor', 'third_party', 'third-party'];
30
+ /**
31
+ * Checks whose failure is never "existing debt". A conflict marker is a file that
32
+ * does not run; a leaked secret is already exposed. Proposing `warn` for these
33
+ * would make the gate quietly agree to ship them, so they are reported as things
34
+ * to fix, and left blocking.
35
+ */
36
+ const NEVER_DOWNGRADE = new Set(['Merge Conflict', 'Security Scan']);
37
+ const isRecord = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
38
+ /** The schema shipped with the package, or `undefined` when this install does not carry it. */
39
+ function loadSchema() {
40
+ const root = findOwnPackageRoot();
41
+ if (!root)
42
+ return undefined;
43
+ try {
44
+ return JSON.parse(fs.readFileSync(path.join(root, 'pr-checkmate.schema.json'), 'utf-8'));
45
+ }
46
+ catch {
47
+ return undefined;
48
+ }
49
+ }
50
+ function resolveRef(node, schema) {
51
+ const ref = node.$ref?.replace('#/$defs/', '');
52
+ return ref && schema.$defs?.[ref] ? { ...schema.$defs[ref], ...node, $ref: undefined } : node;
53
+ }
54
+ /** First sentence, so a listing of sixty keys stays a listing. */
55
+ const firstSentence = (text = '') => {
56
+ const cut = text.search(/(?<=[.!?])\s/);
57
+ return (cut === -1 ? text : text.slice(0, cut)).slice(0, 200);
58
+ };
59
+ /** What a key means, from the schema that ships with the package. */
60
+ export function explainConfig(key) {
61
+ const schema = loadSchema();
62
+ if (!schema?.properties) {
63
+ return { text: 'The configuration schema is not available in this install.', isError: true };
64
+ }
65
+ if (!key) {
66
+ const rows = Object.entries(schema.properties)
67
+ .filter(([name]) => name !== '$schema')
68
+ .map(([name, node]) => {
69
+ const resolved = resolveRef(node, schema);
70
+ return `- ${name}${resolved.description ? ` — ${firstSentence(resolved.description)}` : ''}`;
71
+ });
72
+ return {
73
+ text: `Keys of ${CONFIG_FILE} (pass one as "key" for details; nested keys use dots, e.g. "prSize.maxLines"):\n${rows.join('\n')}`,
74
+ };
75
+ }
76
+ let node = schema;
77
+ for (const part of key.split('.')) {
78
+ node = node?.properties?.[part] ? resolveRef(node.properties[part], schema) : undefined;
79
+ if (!node)
80
+ break;
81
+ }
82
+ if (!node) {
83
+ const near = suggestName(key.split('.')[0], Object.keys(schema.properties));
84
+ return {
85
+ text: `No such key "${key}".${near ? ` Did you mean "${near}"?` : ''}`,
86
+ isError: true,
87
+ };
88
+ }
89
+ const lines = [`${key}${node.type ? ` (${node.type})` : ''}`];
90
+ if (node.description)
91
+ lines.push(node.description);
92
+ if (node.default !== undefined)
93
+ lines.push(`default: ${JSON.stringify(node.default)}`);
94
+ if (node.enum)
95
+ lines.push(`one of: ${node.enum.map(v => JSON.stringify(v)).join(', ')}`);
96
+ for (const [name, child] of Object.entries(node.properties ?? {})) {
97
+ const resolved = resolveRef(child, schema);
98
+ lines.push(`- ${key}.${name}${resolved.description ? ` — ${firstSentence(resolved.description)}` : ''}`);
99
+ }
100
+ return { text: lines.join('\n') };
101
+ }
102
+ /** Problems in a proposed `severity` map: names that are not checks, levels that are not levels. */
103
+ function validateSeverity(severity) {
104
+ if (severity === undefined)
105
+ return [];
106
+ if (!isRecord(severity)) {
107
+ return ['"severity" must be an object of check name → "error" | "warn" | "off"'];
108
+ }
109
+ const problems = [];
110
+ const names = REGISTRY.map(c => c.name);
111
+ for (const [check, level] of Object.entries(severity)) {
112
+ if (!names.includes(check)) {
113
+ const near = suggestName(check, names);
114
+ problems.push(`severity: no check named "${check}"${near ? ` — did you mean "${near}"?` : ''}`);
115
+ }
116
+ if (typeof level !== 'string' || !LEVELS.has(level)) {
117
+ problems.push(`severity.${check}: "${String(level)}" is not one of error, warn, off`);
118
+ }
119
+ }
120
+ return problems;
121
+ }
122
+ /** Problems in a proposed config, phrased so the agent can correct them. Empty means it is acceptable. */
123
+ export function validateConfig(config, cwd) {
124
+ const problems = [];
125
+ const schema = loadSchema();
126
+ const known = Object.keys(schema?.properties ?? {});
127
+ if (known.length > 0) {
128
+ for (const key of Object.keys(config)) {
129
+ if (known.includes(key) || key.startsWith('//'))
130
+ continue;
131
+ const near = suggestName(key, known);
132
+ problems.push(`unknown key "${key}"${near ? ` — did you mean "${near}"?` : ''}`);
133
+ }
134
+ }
135
+ problems.push(...validateSeverity(config.severity));
136
+ for (const [key, block] of Object.entries(config)) {
137
+ if (isRecord(block) && typeof block.failOn === 'string' && !FAIL_ON.has(block.failOn)) {
138
+ problems.push(`${key}.failOn: "${block.failOn}" is not one of error, warn, never`);
139
+ }
140
+ }
141
+ const sources = config.sourcePath === undefined ? [] : [config.sourcePath].flat();
142
+ for (const source of sources) {
143
+ if (typeof source !== 'string' || !fs.existsSync(path.resolve(cwd, source))) {
144
+ problems.push(`sourcePath: "${String(source)}" does not exist in this repository`);
145
+ }
146
+ }
147
+ return problems;
148
+ }
149
+ /** `{a:{b:1,c:2}}` under `a` → `a.b`, `a.c`: what was added, not just where it hangs. */
150
+ function leafPaths(value, here) {
151
+ return isRecord(value) && Object.keys(value).length > 0
152
+ ? Object.entries(value).flatMap(([k, v]) => leafPaths(v, `${here}.${k}`))
153
+ : [here];
154
+ }
155
+ /**
156
+ * Merge `incoming` into `existing`. A value that is already set and differs is
157
+ * kept — the file is the user's — and listed, so the agent can ask rather than
158
+ * overwrite. Objects merge key by key; arrays and scalars are leaves.
159
+ */
160
+ function mergeKeepingExisting(existing, incoming, trail, overwrite, report) {
161
+ const out = { ...existing };
162
+ for (const [key, value] of Object.entries(incoming)) {
163
+ const here = [...trail, key].join('.');
164
+ if (!(key in existing)) {
165
+ out[key] = value;
166
+ report.added.push(...leafPaths(value, here));
167
+ }
168
+ else if (isRecord(existing[key]) && isRecord(value)) {
169
+ out[key] = mergeKeepingExisting(existing[key], value, [...trail, key], overwrite, report);
170
+ }
171
+ else if (JSON.stringify(existing[key]) === JSON.stringify(value)) {
172
+ continue;
173
+ }
174
+ else if (overwrite) {
175
+ out[key] = value;
176
+ report.added.push(`${here} (replaced)`);
177
+ }
178
+ else {
179
+ report.kept.push(here);
180
+ }
181
+ }
182
+ return out;
183
+ }
184
+ /** Validate and write `config` into the repository's `pr-checkmate.json`. */
185
+ export function writeConfig(cwd, config, overwrite) {
186
+ if (!isRecord(config))
187
+ return { text: '"config" must be a JSON object.', isError: true };
188
+ const problems = validateConfig(config, cwd);
189
+ if (problems.length > 0) {
190
+ return {
191
+ text: `Not written — fix these first:\n${problems.map(p => `- ${p}`).join('\n')}`,
192
+ isError: true,
193
+ };
194
+ }
195
+ const file = path.join(cwd, CONFIG_FILE);
196
+ let existing = {};
197
+ if (fs.existsSync(file)) {
198
+ try {
199
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf-8'));
200
+ if (isRecord(parsed))
201
+ existing = parsed;
202
+ else
203
+ throw new Error('not an object');
204
+ }
205
+ catch {
206
+ // A file we cannot read is a file we must not replace: the user may be mid-edit.
207
+ return {
208
+ text: `${CONFIG_FILE} exists but is not valid JSON; fix it by hand first.`,
209
+ isError: true,
210
+ };
211
+ }
212
+ }
213
+ const report = { added: [], kept: [] };
214
+ const merged = mergeKeepingExisting(existing, config, [], overwrite, report);
215
+ if (report.added.length === 0) {
216
+ return {
217
+ text: `Nothing to write: ${CONFIG_FILE} already holds these values.${report.kept.length > 0 ? ` Left as the user set them: ${report.kept.join(', ')} (pass overwrite: true only if they asked for the change).` : ''}`,
218
+ structured: { written: false, added: [], kept: report.kept },
219
+ };
220
+ }
221
+ // Point editors at the schema when this repository has the package installed:
222
+ // that line is what makes every other key discoverable by autocomplete.
223
+ const schemaPath = './node_modules/pr-checkmate/pr-checkmate.schema.json';
224
+ const withSchema = !('$schema' in merged) && fs.existsSync(path.join(cwd, schemaPath))
225
+ ? { $schema: schemaPath, ...merged }
226
+ : merged;
227
+ fs.writeFileSync(file, `${JSON.stringify(withSchema, null, 2)}\n`);
228
+ return {
229
+ text: [
230
+ `${existing && Object.keys(existing).length > 0 ? 'Updated' : 'Created'} ${CONFIG_FILE}:`,
231
+ ...report.added.map(k => ` + ${k}`),
232
+ ...(report.kept.length > 0
233
+ ? [
234
+ `Left as the user set them: ${report.kept.join(', ')} (overwrite: true replaces them — only if asked).`,
235
+ ]
236
+ : []),
237
+ ].join('\n'),
238
+ structured: { written: true, added: report.added, kept: report.kept },
239
+ };
240
+ }
241
+ /**
242
+ * Look at the repository and propose a config.
243
+ *
244
+ * The baseline is a whole-repository run of the fast checks. A check that already
245
+ * fails is proposed as `warn`: the gate is on from the first commit, the existing
246
+ * debt is visible, and nothing new can slip in unnoticed — then each check is
247
+ * promoted back to blocking as it reaches zero. Proposing `error` everywhere would
248
+ * make the first CI run red for reasons nobody on the pull request caused, and the
249
+ * usual response to that is to switch the whole tool off.
250
+ */
251
+ export async function suggestConfig(cwd) {
252
+ const current = loadProjectConfig(cwd);
253
+ const usage = await detectLanguageUsage(cwd, current.ignoreDirs ?? IGNORED_DIRS, current.detect);
254
+ const languages = usage.map(u => `${u.lang} (${u.files} file${u.files === 1 ? '' : 's'})`);
255
+ const hasConfig = fs.existsSync(path.join(cwd, CONFIG_FILE));
256
+ const proposed = {};
257
+ const notes = [];
258
+ const generated = GENERATED_DIRS.filter(d => fs.existsSync(path.join(cwd, d)) && !(current.ignoreDirs ?? IGNORED_DIRS).includes(d));
259
+ if (generated.length > 0) {
260
+ proposed.excludePaths = generated.map(d => `${d}/**`);
261
+ notes.push(`Excluded ${generated.join(', ')}: code nobody maintains by hand only produces findings no one can act on.`);
262
+ }
263
+ const baseline = await review({ cwd, scope: 'full' });
264
+ const severity = {};
265
+ const skipped = [];
266
+ for (const r of baseline.report.results) {
267
+ if (r.status === 'fail' && NEVER_DOWNGRADE.has(r.name)) {
268
+ notes.push(`${r.name} fails today (${r.details ?? 'findings'}) and is NOT proposed as a warning: fix it${r.name === 'Security Scan' ? ' and rotate whatever was exposed' : ''} instead of silencing it.`);
269
+ }
270
+ else if (r.status === 'fail') {
271
+ severity[r.name] = 'warn';
272
+ notes.push(`${r.name} fails today (${r.details ?? 'findings'}) → warn, so existing debt does not block every PR. Promote it back to "error" once it is clean.`);
273
+ }
274
+ else if (r.status === 'skip' && /tooling unavailable/.test(r.details ?? '')) {
275
+ skipped.push(r.name);
276
+ }
277
+ }
278
+ if (Object.keys(severity).length > 0)
279
+ proposed.severity = severity;
280
+ if (skipped.length > 0) {
281
+ notes.push(`Could not run ${skipped.join(', ')}: tooling not available here (run "pr-checkmate install" and ask again for a complete baseline).`);
282
+ }
283
+ const clean = baseline.report.results.filter(r => r.status === 'pass').length;
284
+ notes.push(`Baseline: ${clean} check(s) pass, ${baseline.report.summary.failed} fail, ${baseline.report.summary.warned} warn (fast checks only; "verify" adds the slow ones).`);
285
+ if (hasConfig)
286
+ notes.push(`${CONFIG_FILE} already exists: write_config keeps every value already set and only adds the rest.`);
287
+ if (Object.keys(proposed).length === 0) {
288
+ notes.push('The defaults already fit this repository: no config file is needed. Everything is on and blocking where it should be.');
289
+ }
290
+ return { proposed, notes, languages };
291
+ }
292
+ /** `suggest_config` as the text an agent reads. */
293
+ export function formatSuggestion(s) {
294
+ const body = Object.keys(s.proposed).length > 0
295
+ ? `\nProposed ${CONFIG_FILE}:\n${JSON.stringify(s.proposed, null, 2)}\n`
296
+ : '\n';
297
+ return {
298
+ text: [
299
+ `Languages: ${s.languages.join(', ') || 'none detected'}`,
300
+ ...s.notes.map(n => `- ${n}`),
301
+ body,
302
+ Object.keys(s.proposed).length > 0
303
+ ? 'Show this to the user and, if they agree, call write_config with it. Do not write it unasked.'
304
+ : '',
305
+ ]
306
+ .filter(Boolean)
307
+ .join('\n'),
308
+ structured: { languages: s.languages, proposed: s.proposed, notes: s.notes },
309
+ };
310
+ }
@@ -0,0 +1,33 @@
1
+ export type Ecosystem = 'npm' | 'pypi';
2
+ /**
3
+ * `avoid` — do not add it (does not exist, no such version, known serious advisory)
4
+ * `review` — a person should look first (new, rarely used, deprecated, install script…)
5
+ * `ok` — nothing found
6
+ * `unknown` — a lookup failed, so nothing can be said
7
+ */
8
+ export type Verdict = 'avoid' | 'review' | 'ok' | 'unknown';
9
+ export interface DependencyReport {
10
+ name: string;
11
+ ecosystem: Ecosystem;
12
+ version?: string;
13
+ verdict: Verdict;
14
+ reasons: string[];
15
+ facts: Record<string, string | number | boolean>;
16
+ }
17
+ /** The slice of a fetch the checks need; injectable so tests never touch the network. */
18
+ export type FetchJson = (url: string, init?: RequestInit) => Promise<{
19
+ ok: boolean;
20
+ status: number;
21
+ body?: unknown;
22
+ error?: string;
23
+ }>;
24
+ /** The famous package `name` is suspiciously close to, if any. A name that *is* the famous one is not a match. */
25
+ export declare function lookalikeOf(name: string, ecosystem: Ecosystem): string | undefined;
26
+ /** Is `name` safe to add? Never throws: every failure becomes part of the verdict. */
27
+ export declare function checkDependency(name: string, opts?: {
28
+ version?: string;
29
+ ecosystem?: Ecosystem;
30
+ fetchJson?: FetchJson;
31
+ }): Promise<DependencyReport>;
32
+ /** The report as the text an agent reads. */
33
+ export declare function formatDependency(r: DependencyReport): string;