pr-checkmate 3.3.0 → 4.0.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 (127) hide show
  1. package/LICENSE +8 -3
  2. package/README.md +59 -4
  3. package/cspell.json +1 -0
  4. package/dist/cjs/agent/hook.d.ts +38 -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 +21 -0
  9. package/dist/cjs/agent/setup.js +229 -0
  10. package/dist/cjs/cli.js +88 -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 +10 -0
  14. package/dist/cjs/core/checks/findings.js +33 -7
  15. package/dist/cjs/core/checks/git/diff-smell.js +3 -1
  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 +27 -0
  19. package/dist/cjs/core/checks/languages/locate.d.ts +7 -0
  20. package/dist/cjs/core/checks/languages/locate.js +68 -0
  21. package/dist/cjs/core/checks/languages/prettier.js +15 -2
  22. package/dist/cjs/core/checks/languages/typecheck.js +4 -1
  23. package/dist/cjs/core/checks/pr/pr-size-check.d.ts +2 -0
  24. package/dist/cjs/core/checks/pr/pr-size-check.js +5 -5
  25. package/dist/cjs/core/checks/security/agent-instructions.d.ts +4 -0
  26. package/dist/cjs/core/checks/security/agent-instructions.js +149 -0
  27. package/dist/cjs/core/checks/security/diff-security.d.ts +7 -0
  28. package/dist/cjs/core/checks/security/diff-security.js +21 -1
  29. package/dist/cjs/core/checks/security/security.d.ts +0 -13
  30. package/dist/cjs/core/checks/security/security.js +27 -0
  31. package/dist/cjs/core/checks/security/workflow-security.js +33 -0
  32. package/dist/cjs/core/commands.d.ts +6 -0
  33. package/dist/cjs/core/commands.js +11 -1
  34. package/dist/cjs/core/finding-store.d.ts +16 -0
  35. package/dist/cjs/core/finding-store.js +73 -0
  36. package/dist/cjs/core/registry.js +2 -0
  37. package/dist/cjs/core/reporters.js +9 -0
  38. package/dist/cjs/core/run-checks.js +15 -5
  39. package/dist/cjs/core/sarif.d.ts +15 -0
  40. package/dist/cjs/core/sarif.js +60 -0
  41. package/dist/cjs/core/types.d.ts +7 -2
  42. package/dist/cjs/index.d.ts +2 -1
  43. package/dist/cjs/index.js +4 -1
  44. package/dist/cjs/init.js +11 -0
  45. package/dist/cjs/interfaces/index.d.ts +12 -0
  46. package/dist/cjs/mcp/config-tools.d.ts +30 -0
  47. package/dist/cjs/mcp/config-tools.js +320 -0
  48. package/dist/cjs/mcp/dependency.d.ts +34 -0
  49. package/dist/cjs/mcp/dependency.js +413 -0
  50. package/dist/cjs/mcp/protocol.d.ts +91 -0
  51. package/dist/cjs/mcp/protocol.js +181 -0
  52. package/dist/cjs/mcp/review.d.ts +64 -0
  53. package/dist/cjs/mcp/review.js +150 -0
  54. package/dist/cjs/mcp/serve.d.ts +8 -0
  55. package/dist/cjs/mcp/serve.js +49 -0
  56. package/dist/cjs/mcp/settings.d.ts +13 -0
  57. package/dist/cjs/mcp/settings.js +50 -0
  58. package/dist/cjs/mcp/tools.d.ts +23 -0
  59. package/dist/cjs/mcp/tools.js +618 -0
  60. package/dist/cjs/utils/execa.d.ts +7 -0
  61. package/dist/cjs/utils/execa.js +41 -1
  62. package/dist/cjs/utils/result-collector.d.ts +17 -1
  63. package/dist/cjs/utils/result-collector.js +7 -2
  64. package/dist/esm/agent/hook.d.ts +38 -0
  65. package/dist/esm/agent/hook.js +167 -0
  66. package/dist/esm/agent/registration.d.ts +10 -0
  67. package/dist/esm/agent/registration.js +21 -0
  68. package/dist/esm/agent/setup.d.ts +21 -0
  69. package/dist/esm/agent/setup.js +220 -0
  70. package/dist/esm/cli.js +86 -1
  71. package/dist/esm/config/constants.d.ts +2 -0
  72. package/dist/esm/config/constants.js +4 -0
  73. package/dist/esm/config/pr-checkmate.template.json +10 -0
  74. package/dist/esm/core/checks/findings.js +33 -7
  75. package/dist/esm/core/checks/git/diff-smell.js +3 -1
  76. package/dist/esm/core/checks/git/large-files.d.ts +1 -0
  77. package/dist/esm/core/checks/git/large-files.js +1 -1
  78. package/dist/esm/core/checks/languages/lint.js +28 -1
  79. package/dist/esm/core/checks/languages/locate.d.ts +7 -0
  80. package/dist/esm/core/checks/languages/locate.js +60 -0
  81. package/dist/esm/core/checks/languages/prettier.js +15 -2
  82. package/dist/esm/core/checks/languages/typecheck.js +4 -1
  83. package/dist/esm/core/checks/pr/pr-size-check.d.ts +2 -0
  84. package/dist/esm/core/checks/pr/pr-size-check.js +2 -2
  85. package/dist/esm/core/checks/security/agent-instructions.d.ts +4 -0
  86. package/dist/esm/core/checks/security/agent-instructions.js +146 -0
  87. package/dist/esm/core/checks/security/diff-security.d.ts +7 -0
  88. package/dist/esm/core/checks/security/diff-security.js +20 -0
  89. package/dist/esm/core/checks/security/security.d.ts +0 -13
  90. package/dist/esm/core/checks/security/security.js +27 -0
  91. package/dist/esm/core/checks/security/workflow-security.js +33 -0
  92. package/dist/esm/core/commands.d.ts +6 -0
  93. package/dist/esm/core/commands.js +10 -0
  94. package/dist/esm/core/finding-store.d.ts +16 -0
  95. package/dist/esm/core/finding-store.js +67 -0
  96. package/dist/esm/core/registry.js +2 -0
  97. package/dist/esm/core/reporters.js +9 -0
  98. package/dist/esm/core/run-checks.js +15 -5
  99. package/dist/esm/core/sarif.d.ts +15 -0
  100. package/dist/esm/core/sarif.js +57 -0
  101. package/dist/esm/core/types.d.ts +7 -2
  102. package/dist/esm/index.d.ts +2 -1
  103. package/dist/esm/index.js +2 -0
  104. package/dist/esm/init.js +12 -1
  105. package/dist/esm/interfaces/index.d.ts +12 -0
  106. package/dist/esm/mcp/config-tools.d.ts +30 -0
  107. package/dist/esm/mcp/config-tools.js +310 -0
  108. package/dist/esm/mcp/dependency.d.ts +34 -0
  109. package/dist/esm/mcp/dependency.js +408 -0
  110. package/dist/esm/mcp/protocol.d.ts +91 -0
  111. package/dist/esm/mcp/protocol.js +175 -0
  112. package/dist/esm/mcp/review.d.ts +64 -0
  113. package/dist/esm/mcp/review.js +139 -0
  114. package/dist/esm/mcp/serve.d.ts +8 -0
  115. package/dist/esm/mcp/serve.js +43 -0
  116. package/dist/esm/mcp/settings.d.ts +13 -0
  117. package/dist/esm/mcp/settings.js +43 -0
  118. package/dist/esm/mcp/tools.d.ts +23 -0
  119. package/dist/esm/mcp/tools.js +611 -0
  120. package/dist/esm/utils/execa.d.ts +7 -0
  121. package/dist/esm/utils/execa.js +40 -1
  122. package/dist/esm/utils/result-collector.d.ts +17 -1
  123. package/dist/esm/utils/result-collector.js +7 -2
  124. package/package.json +7 -3
  125. package/pr-checkmate.schema.json +35 -0
  126. package/src/config/cspell.json +1 -0
  127. package/src/config/pr-checkmate.schema.json +35 -0
package/dist/cjs/index.js CHANGED
@@ -5,13 +5,16 @@
5
5
  * @module pr-checkmate
6
6
  */
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
- exports.Logger = exports.hasLocalConfig = exports.getPackageConfigPath = exports.getIgnoredDirs = exports.getSourcePaths = exports.getSourcePath = exports.defineCheck = exports.skip = exports.fail = exports.warn = exports.pass = exports.REGISTRY = exports.buildContext = exports.runChecks = void 0;
8
+ exports.Logger = exports.hasLocalConfig = exports.getPackageConfigPath = exports.getIgnoredDirs = exports.getSourcePaths = exports.getSourcePath = exports.defineCheck = exports.skip = exports.fail = exports.warn = exports.pass = exports.toSarif = exports.REGISTRY = exports.buildContext = exports.runChecks = void 0;
9
9
  // Primary programmatic API: run checks and receive a structured report.
10
10
  // Never throws for violations, never calls process.exit — the caller decides.
11
11
  var index_js_1 = require("./core/index.js");
12
12
  Object.defineProperty(exports, "runChecks", { enumerable: true, get: function () { return index_js_1.runChecks; } });
13
13
  Object.defineProperty(exports, "buildContext", { enumerable: true, get: function () { return index_js_1.buildContext; } });
14
14
  Object.defineProperty(exports, "REGISTRY", { enumerable: true, get: function () { return index_js_1.REGISTRY; } });
15
+ // SARIF 2.1.0 export of a report, for code-scanning dashboards and editors.
16
+ var sarif_js_1 = require("./core/sarif.js");
17
+ Object.defineProperty(exports, "toSarif", { enumerable: true, get: function () { return sarif_js_1.toSarif; } });
15
18
  // Authoring helpers for consumers writing their own checks (plugins).
16
19
  var index_js_2 = require("./core/index.js");
17
20
  Object.defineProperty(exports, "pass", { enumerable: true, get: function () { return index_js_2.pass; } });
package/dist/cjs/init.js CHANGED
@@ -419,6 +419,16 @@ async function createConfig(root) {
419
419
  * update notice insisted `init` must not be skipped after an upgrade. Defaults
420
420
  * now apply when the config is loaded, so there is nothing to top up.
421
421
  */
422
+ /** Adds the agent-setup command to a client's `commands` block when it is missing; nothing else in it is touched. */
423
+ function withAgentCommand(commands) {
424
+ if (commands === undefined)
425
+ return undefined;
426
+ if (typeof commands !== 'object' || commands === null || Array.isArray(commands))
427
+ return commands;
428
+ if (index_js_1.AGENT_SETUP_COMMAND in commands)
429
+ return commands;
430
+ return { ...commands, [index_js_1.AGENT_SETUP_COMMAND]: index_js_1.DEFAULT_COMMANDS[index_js_1.AGENT_SETUP_COMMAND] };
431
+ }
422
432
  async function updateConfig(root) {
423
433
  index_js_2.logger.info('[init]: pr-checkmate.json already exists — updating it.');
424
434
  try {
@@ -449,6 +459,7 @@ async function updateConfig(root) {
449
459
  // when missing; a client's own block is never touched.
450
460
  grypeScan: json.grypeScan ?? structuredClone(GRYPE_SCAN_SEED),
451
461
  ...templateExtras({ ...json, grypeScan: true }),
462
+ commands: withAgentCommand(json.commands),
452
463
  languages,
453
464
  };
454
465
  fs_1.default.writeFileSync(CONFIG_FILE, `${JSON.stringify(updated, null, 2)}\n`);
@@ -317,6 +317,18 @@ export interface PRCheckMateConfig {
317
317
  severityOverrides?: Record<string, 'error' | 'warn' | 'off'>;
318
318
  ignorePaths?: string[];
319
319
  };
320
+ mcp?: {
321
+ enabled?: boolean;
322
+ disabledTools?: string[];
323
+ readOnly?: boolean;
324
+ };
325
+ agentInstructions?: {
326
+ enabled?: boolean;
327
+ ignore?: string[];
328
+ failOn?: 'error' | 'warn' | 'never';
329
+ severityOverrides?: Record<string, 'error' | 'warn' | 'off'>;
330
+ ignorePaths?: string[];
331
+ };
320
332
  dockerfileSecurity?: {
321
333
  enabled?: boolean;
322
334
  ignore?: string[];
@@ -0,0 +1,30 @@
1
+ import type { ToolResult } from './protocol.js';
2
+ type Json = Record<string, unknown>;
3
+ /** What a key means, from the schema that ships with the package. */
4
+ export declare function explainConfig(key?: string): ToolResult;
5
+ /** Problems in a proposed config, phrased so the agent can correct them. Empty means it is acceptable. */
6
+ export declare function validateConfig(config: Json, cwd: string): string[];
7
+ /** Validate and write `config` into the repository's `pr-checkmate.json`. */
8
+ export declare function writeConfig(cwd: string, config: unknown, overwrite: boolean): ToolResult;
9
+ interface Suggestion {
10
+ proposed: Json;
11
+ notes: string[];
12
+ }
13
+ /**
14
+ * Look at the repository and propose a config.
15
+ *
16
+ * The baseline is a whole-repository run of the fast checks. A check that already
17
+ * fails is proposed as `warn`: the gate is on from the first commit, the existing
18
+ * debt is visible, and nothing new can slip in unnoticed — then each check is
19
+ * promoted back to blocking as it reaches zero. Proposing `error` everywhere would
20
+ * make the first CI run red for reasons nobody on the pull request caused, and the
21
+ * usual response to that is to switch the whole tool off.
22
+ */
23
+ export declare function suggestConfig(cwd: string): Promise<Suggestion & {
24
+ languages: string[];
25
+ }>;
26
+ /** `suggest_config` as the text an agent reads. */
27
+ export declare function formatSuggestion(s: Suggestion & {
28
+ languages: string[];
29
+ }): ToolResult;
30
+ export {};
@@ -0,0 +1,320 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.explainConfig = explainConfig;
7
+ exports.validateConfig = validateConfig;
8
+ exports.writeConfig = writeConfig;
9
+ exports.suggestConfig = suggestConfig;
10
+ exports.formatSuggestion = formatSuggestion;
11
+ /**
12
+ * Setting pr-checkmate up for one specific repository, by the agent that is
13
+ * already sitting in it.
14
+ *
15
+ * The slow part of adopting a gate is not turning it on; it is deciding what to do
16
+ * about the findings an existing codebase already has. The documented answer is a
17
+ * strict workflow with a permissive config, then promoting checks one at a time as
18
+ * they go clean. That is a procedure an agent can carry out — if it can see the
19
+ * baseline — so these tools hand it exactly that: what the repository looks like,
20
+ * what it fails today, and a config that makes the gate real from day one without
21
+ * turning the first run into three thousand red lines.
22
+ *
23
+ * Nothing here writes except `write_config`, and that one validates first, merges
24
+ * over what is already there, and never replaces a value the user chose.
25
+ */
26
+ const node_fs_1 = __importDefault(require("node:fs"));
27
+ const node_path_1 = __importDefault(require("node:path"));
28
+ const constants_js_1 = require("../config/constants.js");
29
+ const loader_js_1 = require("../config/loader.js");
30
+ const config_validation_js_1 = require("../core/checks/quality/config-validation.js");
31
+ const registry_js_1 = require("../core/registry.js");
32
+ const detect_language_js_1 = require("../utils/detect-language.js");
33
+ const own_deps_js_1 = require("../utils/own-deps.js");
34
+ const review_js_1 = require("./review.js");
35
+ const CONFIG_FILE = 'pr-checkmate.json';
36
+ const LEVELS = new Set(['error', 'warn', 'off']);
37
+ const FAIL_ON = new Set(['error', 'warn', 'never']);
38
+ /** Directories that hold code nobody on the team wrote by hand. Not in the default ignore list. */
39
+ const GENERATED_DIRS = ['generated', '__generated__', 'vendor', 'third_party', 'third-party'];
40
+ /**
41
+ * Checks whose failure is never "existing debt". A conflict marker is a file that
42
+ * does not run; a leaked secret is already exposed. Proposing `warn` for these
43
+ * would make the gate quietly agree to ship them, so they are reported as things
44
+ * to fix, and left blocking.
45
+ */
46
+ const NEVER_DOWNGRADE = new Set(['Merge Conflict', 'Security Scan']);
47
+ const isRecord = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
48
+ /** The schema shipped with the package, or `undefined` when this install does not carry it. */
49
+ function loadSchema() {
50
+ const root = (0, own_deps_js_1.findOwnPackageRoot)();
51
+ if (!root)
52
+ return undefined;
53
+ try {
54
+ return JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(root, 'pr-checkmate.schema.json'), 'utf-8'));
55
+ }
56
+ catch {
57
+ return undefined;
58
+ }
59
+ }
60
+ function resolveRef(node, schema) {
61
+ const ref = node.$ref?.replace('#/$defs/', '');
62
+ return ref && schema.$defs?.[ref] ? { ...schema.$defs[ref], ...node, $ref: undefined } : node;
63
+ }
64
+ /** First sentence, so a listing of sixty keys stays a listing. */
65
+ const firstSentence = (text = '') => {
66
+ const cut = text.search(/(?<=[.!?])\s/);
67
+ return (cut === -1 ? text : text.slice(0, cut)).slice(0, 200);
68
+ };
69
+ /** What a key means, from the schema that ships with the package. */
70
+ function explainConfig(key) {
71
+ const schema = loadSchema();
72
+ if (!schema?.properties) {
73
+ return { text: 'The configuration schema is not available in this install.', isError: true };
74
+ }
75
+ if (!key) {
76
+ const rows = Object.entries(schema.properties)
77
+ .filter(([name]) => name !== '$schema')
78
+ .map(([name, node]) => {
79
+ const resolved = resolveRef(node, schema);
80
+ return `- ${name}${resolved.description ? ` — ${firstSentence(resolved.description)}` : ''}`;
81
+ });
82
+ return {
83
+ text: `Keys of ${CONFIG_FILE} (pass one as "key" for details; nested keys use dots, e.g. "prSize.maxLines"):\n${rows.join('\n')}`,
84
+ };
85
+ }
86
+ let node = schema;
87
+ for (const part of key.split('.')) {
88
+ node = node?.properties?.[part] ? resolveRef(node.properties[part], schema) : undefined;
89
+ if (!node)
90
+ break;
91
+ }
92
+ if (!node) {
93
+ const near = (0, config_validation_js_1.suggestName)(key.split('.')[0], Object.keys(schema.properties));
94
+ return {
95
+ text: `No such key "${key}".${near ? ` Did you mean "${near}"?` : ''}`,
96
+ isError: true,
97
+ };
98
+ }
99
+ const lines = [`${key}${node.type ? ` (${node.type})` : ''}`];
100
+ if (node.description)
101
+ lines.push(node.description);
102
+ if (node.default !== undefined)
103
+ lines.push(`default: ${JSON.stringify(node.default)}`);
104
+ if (node.enum)
105
+ lines.push(`one of: ${node.enum.map(v => JSON.stringify(v)).join(', ')}`);
106
+ for (const [name, child] of Object.entries(node.properties ?? {})) {
107
+ const resolved = resolveRef(child, schema);
108
+ lines.push(`- ${key}.${name}${resolved.description ? ` — ${firstSentence(resolved.description)}` : ''}`);
109
+ }
110
+ return { text: lines.join('\n') };
111
+ }
112
+ /** Problems in a proposed `severity` map: names that are not checks, levels that are not levels. */
113
+ function validateSeverity(severity) {
114
+ if (severity === undefined)
115
+ return [];
116
+ if (!isRecord(severity)) {
117
+ return ['"severity" must be an object of check name → "error" | "warn" | "off"'];
118
+ }
119
+ const problems = [];
120
+ const names = registry_js_1.REGISTRY.map(c => c.name);
121
+ for (const [check, level] of Object.entries(severity)) {
122
+ if (!names.includes(check)) {
123
+ const near = (0, config_validation_js_1.suggestName)(check, names);
124
+ problems.push(`severity: no check named "${check}"${near ? ` — did you mean "${near}"?` : ''}`);
125
+ }
126
+ if (typeof level !== 'string' || !LEVELS.has(level)) {
127
+ problems.push(`severity.${check}: "${String(level)}" is not one of error, warn, off`);
128
+ }
129
+ }
130
+ return problems;
131
+ }
132
+ /** Problems in a proposed config, phrased so the agent can correct them. Empty means it is acceptable. */
133
+ function validateConfig(config, cwd) {
134
+ const problems = [];
135
+ const schema = loadSchema();
136
+ const known = Object.keys(schema?.properties ?? {});
137
+ if (known.length > 0) {
138
+ for (const key of Object.keys(config)) {
139
+ if (known.includes(key) || key.startsWith('//'))
140
+ continue;
141
+ const near = (0, config_validation_js_1.suggestName)(key, known);
142
+ problems.push(`unknown key "${key}"${near ? ` — did you mean "${near}"?` : ''}`);
143
+ }
144
+ }
145
+ problems.push(...validateSeverity(config.severity));
146
+ for (const [key, block] of Object.entries(config)) {
147
+ if (isRecord(block) && typeof block.failOn === 'string' && !FAIL_ON.has(block.failOn)) {
148
+ problems.push(`${key}.failOn: "${block.failOn}" is not one of error, warn, never`);
149
+ }
150
+ }
151
+ const sources = config.sourcePath === undefined ? [] : [config.sourcePath].flat();
152
+ for (const source of sources) {
153
+ if (typeof source !== 'string' || !node_fs_1.default.existsSync(node_path_1.default.resolve(cwd, source))) {
154
+ problems.push(`sourcePath: "${String(source)}" does not exist in this repository`);
155
+ }
156
+ }
157
+ return problems;
158
+ }
159
+ /** `{a:{b:1,c:2}}` under `a` → `a.b`, `a.c`: what was added, not just where it hangs. */
160
+ function leafPaths(value, here) {
161
+ return isRecord(value) && Object.keys(value).length > 0
162
+ ? Object.entries(value).flatMap(([k, v]) => leafPaths(v, `${here}.${k}`))
163
+ : [here];
164
+ }
165
+ /**
166
+ * Merge `incoming` into `existing`. A value that is already set and differs is
167
+ * kept — the file is the user's — and listed, so the agent can ask rather than
168
+ * overwrite. Objects merge key by key; arrays and scalars are leaves.
169
+ */
170
+ function mergeKeepingExisting(existing, incoming, trail, overwrite, report) {
171
+ const out = { ...existing };
172
+ for (const [key, value] of Object.entries(incoming)) {
173
+ const here = [...trail, key].join('.');
174
+ if (!(key in existing)) {
175
+ out[key] = value;
176
+ report.added.push(...leafPaths(value, here));
177
+ }
178
+ else if (isRecord(existing[key]) && isRecord(value)) {
179
+ out[key] = mergeKeepingExisting(existing[key], value, [...trail, key], overwrite, report);
180
+ }
181
+ else if (JSON.stringify(existing[key]) === JSON.stringify(value)) {
182
+ continue;
183
+ }
184
+ else if (overwrite) {
185
+ out[key] = value;
186
+ report.added.push(`${here} (replaced)`);
187
+ }
188
+ else {
189
+ report.kept.push(here);
190
+ }
191
+ }
192
+ return out;
193
+ }
194
+ /** Validate and write `config` into the repository's `pr-checkmate.json`. */
195
+ function writeConfig(cwd, config, overwrite) {
196
+ if (!isRecord(config))
197
+ return { text: '"config" must be a JSON object.', isError: true };
198
+ const problems = validateConfig(config, cwd);
199
+ if (problems.length > 0) {
200
+ return {
201
+ text: `Not written — fix these first:\n${problems.map(p => `- ${p}`).join('\n')}`,
202
+ isError: true,
203
+ };
204
+ }
205
+ const file = node_path_1.default.join(cwd, CONFIG_FILE);
206
+ let existing = {};
207
+ if (node_fs_1.default.existsSync(file)) {
208
+ try {
209
+ const parsed = JSON.parse(node_fs_1.default.readFileSync(file, 'utf-8'));
210
+ if (isRecord(parsed))
211
+ existing = parsed;
212
+ else
213
+ throw new Error('not an object');
214
+ }
215
+ catch {
216
+ // A file we cannot read is a file we must not replace: the user may be mid-edit.
217
+ return {
218
+ text: `${CONFIG_FILE} exists but is not valid JSON; fix it by hand first.`,
219
+ isError: true,
220
+ };
221
+ }
222
+ }
223
+ const report = { added: [], kept: [] };
224
+ const merged = mergeKeepingExisting(existing, config, [], overwrite, report);
225
+ if (report.added.length === 0) {
226
+ return {
227
+ 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).` : ''}`,
228
+ structured: { written: false, added: [], kept: report.kept },
229
+ };
230
+ }
231
+ // Point editors at the schema when this repository has the package installed:
232
+ // that line is what makes every other key discoverable by autocomplete.
233
+ const schemaPath = './node_modules/pr-checkmate/pr-checkmate.schema.json';
234
+ const withSchema = !('$schema' in merged) && node_fs_1.default.existsSync(node_path_1.default.join(cwd, schemaPath))
235
+ ? { $schema: schemaPath, ...merged }
236
+ : merged;
237
+ node_fs_1.default.writeFileSync(file, `${JSON.stringify(withSchema, null, 2)}\n`);
238
+ return {
239
+ text: [
240
+ `${existing && Object.keys(existing).length > 0 ? 'Updated' : 'Created'} ${CONFIG_FILE}:`,
241
+ ...report.added.map(k => ` + ${k}`),
242
+ ...(report.kept.length > 0
243
+ ? [
244
+ `Left as the user set them: ${report.kept.join(', ')} (overwrite: true replaces them — only if asked).`,
245
+ ]
246
+ : []),
247
+ ].join('\n'),
248
+ structured: { written: true, added: report.added, kept: report.kept },
249
+ };
250
+ }
251
+ /**
252
+ * Look at the repository and propose a config.
253
+ *
254
+ * The baseline is a whole-repository run of the fast checks. A check that already
255
+ * fails is proposed as `warn`: the gate is on from the first commit, the existing
256
+ * debt is visible, and nothing new can slip in unnoticed — then each check is
257
+ * promoted back to blocking as it reaches zero. Proposing `error` everywhere would
258
+ * make the first CI run red for reasons nobody on the pull request caused, and the
259
+ * usual response to that is to switch the whole tool off.
260
+ */
261
+ async function suggestConfig(cwd) {
262
+ const current = (0, loader_js_1.loadProjectConfig)(cwd);
263
+ const usage = await (0, detect_language_js_1.detectLanguageUsage)(cwd, current.ignoreDirs ?? constants_js_1.IGNORED_DIRS, current.detect);
264
+ const languages = usage.map(u => `${u.lang} (${u.files} file${u.files === 1 ? '' : 's'})`);
265
+ const hasConfig = node_fs_1.default.existsSync(node_path_1.default.join(cwd, CONFIG_FILE));
266
+ const proposed = {};
267
+ const notes = [];
268
+ const generated = GENERATED_DIRS.filter(d => node_fs_1.default.existsSync(node_path_1.default.join(cwd, d)) && !(current.ignoreDirs ?? constants_js_1.IGNORED_DIRS).includes(d));
269
+ if (generated.length > 0) {
270
+ proposed.excludePaths = generated.map(d => `${d}/**`);
271
+ notes.push(`Excluded ${generated.join(', ')}: code nobody maintains by hand only produces findings no one can act on.`);
272
+ }
273
+ const baseline = await (0, review_js_1.review)({ cwd, scope: 'full' });
274
+ const severity = {};
275
+ const skipped = [];
276
+ for (const r of baseline.report.results) {
277
+ if (r.status === 'fail' && NEVER_DOWNGRADE.has(r.name)) {
278
+ 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.`);
279
+ }
280
+ else if (r.status === 'fail') {
281
+ severity[r.name] = 'warn';
282
+ 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.`);
283
+ }
284
+ else if (r.status === 'skip' && /tooling unavailable/.test(r.details ?? '')) {
285
+ skipped.push(r.name);
286
+ }
287
+ }
288
+ if (Object.keys(severity).length > 0)
289
+ proposed.severity = severity;
290
+ if (skipped.length > 0) {
291
+ notes.push(`Could not run ${skipped.join(', ')}: tooling not available here (run "pr-checkmate install" and ask again for a complete baseline).`);
292
+ }
293
+ const clean = baseline.report.results.filter(r => r.status === 'pass').length;
294
+ 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).`);
295
+ if (hasConfig)
296
+ notes.push(`${CONFIG_FILE} already exists: write_config keeps every value already set and only adds the rest.`);
297
+ if (Object.keys(proposed).length === 0) {
298
+ notes.push('The defaults already fit this repository: no config file is needed. Everything is on and blocking where it should be.');
299
+ }
300
+ return { proposed, notes, languages };
301
+ }
302
+ /** `suggest_config` as the text an agent reads. */
303
+ function formatSuggestion(s) {
304
+ const body = Object.keys(s.proposed).length > 0
305
+ ? `\nProposed ${CONFIG_FILE}:\n${JSON.stringify(s.proposed, null, 2)}\n`
306
+ : '\n';
307
+ return {
308
+ text: [
309
+ `Languages: ${s.languages.join(', ') || 'none detected'}`,
310
+ ...s.notes.map(n => `- ${n}`),
311
+ body,
312
+ Object.keys(s.proposed).length > 0
313
+ ? 'Show this to the user and, if they agree, call write_config with it. Do not write it unasked.'
314
+ : '',
315
+ ]
316
+ .filter(Boolean)
317
+ .join('\n'),
318
+ structured: { languages: s.languages, proposed: s.proposed, notes: s.notes },
319
+ };
320
+ }
@@ -0,0 +1,34 @@
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
+ 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;
34
+ export {};