@kernel.chat/kbot 3.37.0 → 3.39.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.
- package/README.md +111 -7
- package/dist/agent-teams.d.ts +55 -0
- package/dist/agent-teams.d.ts.map +1 -0
- package/dist/agent-teams.js +135 -0
- package/dist/agent-teams.js.map +1 -0
- package/dist/autonomous-contributor.d.ts +88 -0
- package/dist/autonomous-contributor.d.ts.map +1 -0
- package/dist/autonomous-contributor.js +862 -0
- package/dist/autonomous-contributor.js.map +1 -0
- package/dist/collective-network.d.ts +102 -0
- package/dist/collective-network.d.ts.map +1 -0
- package/dist/collective-network.js +634 -0
- package/dist/collective-network.js.map +1 -0
- package/dist/community-autopilot.d.ts +98 -0
- package/dist/community-autopilot.d.ts.map +1 -0
- package/dist/community-autopilot.js +676 -0
- package/dist/community-autopilot.js.map +1 -0
- package/dist/cross-device-sync.d.ts +36 -0
- package/dist/cross-device-sync.d.ts.map +1 -0
- package/dist/cross-device-sync.js +532 -0
- package/dist/cross-device-sync.js.map +1 -0
- package/dist/forge-marketplace-server.d.ts +23 -0
- package/dist/forge-marketplace-server.d.ts.map +1 -0
- package/dist/forge-marketplace-server.js +457 -0
- package/dist/forge-marketplace-server.js.map +1 -0
- package/dist/hooks-integration.d.ts +89 -0
- package/dist/hooks-integration.d.ts.map +1 -0
- package/dist/hooks-integration.js +457 -0
- package/dist/hooks-integration.js.map +1 -0
- package/dist/plugin/index.d.ts +71 -0
- package/dist/plugin/index.d.ts.map +1 -0
- package/dist/plugin/index.js +133 -0
- package/dist/plugin/index.js.map +1 -0
- package/dist/voice-loop.d.ts +59 -0
- package/dist/voice-loop.d.ts.map +1 -0
- package/dist/voice-loop.js +525 -0
- package/dist/voice-loop.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,862 @@
|
|
|
1
|
+
// kbot Autonomous Contributor — Point at any repo, analyze, propose fixes
|
|
2
|
+
//
|
|
3
|
+
// Clones any public GitHub repo, runs bootstrap + guardian analysis,
|
|
4
|
+
// identifies actionable improvements, and generates a contribution report
|
|
5
|
+
// with proposed fixes. v1 is analysis-only — no automatic PR creation.
|
|
6
|
+
//
|
|
7
|
+
// Usage:
|
|
8
|
+
// import { runAutonomousContributor, listGoodFirstIssues } from './autonomous-contributor.js'
|
|
9
|
+
// const report = await runAutonomousContributor('https://github.com/owner/repo')
|
|
10
|
+
// const issues = await listGoodFirstIssues('https://github.com/owner/repo')
|
|
11
|
+
import { existsSync, readFileSync, mkdirSync, rmSync, readdirSync, statSync, writeFileSync } from 'node:fs';
|
|
12
|
+
import { join, relative, extname } from 'node:path';
|
|
13
|
+
import { tmpdir, homedir } from 'node:os';
|
|
14
|
+
import { execSync } from 'node:child_process';
|
|
15
|
+
// ── Constants ──
|
|
16
|
+
const IGNORED_DIRS = new Set([
|
|
17
|
+
'node_modules', '.git', 'dist', 'build', 'out', '.next', '.nuxt',
|
|
18
|
+
'coverage', '.cache', '.turbo', '.parcel-cache', '__pycache__',
|
|
19
|
+
'vendor', 'target', '.output', '.vercel', '.tox', '.mypy_cache',
|
|
20
|
+
'.pytest_cache', 'venv', '.venv', 'env',
|
|
21
|
+
]);
|
|
22
|
+
const CODE_EXTENSIONS = new Set([
|
|
23
|
+
'.ts', '.tsx', '.js', '.jsx', '.mts', '.mjs', '.py', '.rs',
|
|
24
|
+
'.go', '.java', '.rb', '.php', '.swift', '.kt', '.c', '.cpp', '.h',
|
|
25
|
+
'.cs', '.vue', '.svelte', '.astro',
|
|
26
|
+
]);
|
|
27
|
+
const SEVERITY_ORDER = {
|
|
28
|
+
'info': 0,
|
|
29
|
+
'warn': 1,
|
|
30
|
+
'critical': 2,
|
|
31
|
+
};
|
|
32
|
+
const REPORT_DIR = join(homedir(), '.kbot', 'contributions');
|
|
33
|
+
// ── Helpers ──
|
|
34
|
+
function ensureDir(dir) {
|
|
35
|
+
if (!existsSync(dir))
|
|
36
|
+
mkdirSync(dir, { recursive: true });
|
|
37
|
+
}
|
|
38
|
+
function execQuiet(cmd, cwd, timeoutMs = 30000) {
|
|
39
|
+
try {
|
|
40
|
+
return execSync(cmd, {
|
|
41
|
+
encoding: 'utf-8',
|
|
42
|
+
timeout: timeoutMs,
|
|
43
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
44
|
+
cwd,
|
|
45
|
+
}).trim();
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** Extract "owner/repo" from a GitHub URL or owner/repo string */
|
|
52
|
+
function parseRepoIdentifier(input) {
|
|
53
|
+
// Full URL: https://github.com/owner/repo or git@github.com:owner/repo.git
|
|
54
|
+
const httpsMatch = input.match(/github\.com\/([^/]+\/[^/.]+)/);
|
|
55
|
+
if (httpsMatch)
|
|
56
|
+
return httpsMatch[1].replace(/\.git$/, '');
|
|
57
|
+
const sshMatch = input.match(/github\.com:([^/]+\/[^/.]+)/);
|
|
58
|
+
if (sshMatch)
|
|
59
|
+
return sshMatch[1].replace(/\.git$/, '');
|
|
60
|
+
// Already in owner/repo format
|
|
61
|
+
if (/^[^/]+\/[^/]+$/.test(input))
|
|
62
|
+
return input;
|
|
63
|
+
throw new Error(`Cannot parse repo identifier from: ${input}`);
|
|
64
|
+
}
|
|
65
|
+
function cloneUrl(repoId) {
|
|
66
|
+
return `https://github.com/${repoId}.git`;
|
|
67
|
+
}
|
|
68
|
+
// ── File Walking ──
|
|
69
|
+
function walkCodeFiles(dir, maxFiles, files = []) {
|
|
70
|
+
if (files.length >= maxFiles)
|
|
71
|
+
return files;
|
|
72
|
+
let entries;
|
|
73
|
+
try {
|
|
74
|
+
entries = readdirSync(dir);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return files;
|
|
78
|
+
}
|
|
79
|
+
for (const entry of entries) {
|
|
80
|
+
if (files.length >= maxFiles)
|
|
81
|
+
break;
|
|
82
|
+
const fullPath = join(dir, entry);
|
|
83
|
+
let stats;
|
|
84
|
+
try {
|
|
85
|
+
stats = statSync(fullPath);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (stats.isDirectory()) {
|
|
91
|
+
if (!IGNORED_DIRS.has(entry) && !entry.startsWith('.')) {
|
|
92
|
+
walkCodeFiles(fullPath, maxFiles, files);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
else if (stats.isFile()) {
|
|
96
|
+
const ext = extname(entry).toLowerCase();
|
|
97
|
+
if (CODE_EXTENSIONS.has(ext) && stats.size < 500_000) {
|
|
98
|
+
files.push(fullPath);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return files;
|
|
103
|
+
}
|
|
104
|
+
function detectProjectProfile(rootDir) {
|
|
105
|
+
const profile = {
|
|
106
|
+
language: 'unknown',
|
|
107
|
+
framework: 'none',
|
|
108
|
+
hasTests: false,
|
|
109
|
+
hasCI: false,
|
|
110
|
+
hasReadme: false,
|
|
111
|
+
hasLicense: false,
|
|
112
|
+
hasContributing: false,
|
|
113
|
+
hasTypeConfig: false,
|
|
114
|
+
};
|
|
115
|
+
// Check for common files
|
|
116
|
+
profile.hasReadme = existsSync(join(rootDir, 'README.md')) || existsSync(join(rootDir, 'readme.md'));
|
|
117
|
+
profile.hasLicense = existsSync(join(rootDir, 'LICENSE')) || existsSync(join(rootDir, 'LICENSE.md'));
|
|
118
|
+
profile.hasContributing = existsSync(join(rootDir, 'CONTRIBUTING.md'));
|
|
119
|
+
profile.hasCI = existsSync(join(rootDir, '.github', 'workflows')) ||
|
|
120
|
+
existsSync(join(rootDir, '.circleci')) ||
|
|
121
|
+
existsSync(join(rootDir, '.travis.yml')) ||
|
|
122
|
+
existsSync(join(rootDir, 'Jenkinsfile'));
|
|
123
|
+
// Language detection
|
|
124
|
+
if (existsSync(join(rootDir, 'package.json'))) {
|
|
125
|
+
profile.language = 'javascript';
|
|
126
|
+
// Check for TypeScript
|
|
127
|
+
if (existsSync(join(rootDir, 'tsconfig.json'))) {
|
|
128
|
+
profile.language = 'typescript';
|
|
129
|
+
profile.hasTypeConfig = true;
|
|
130
|
+
}
|
|
131
|
+
// Framework detection from package.json
|
|
132
|
+
try {
|
|
133
|
+
const pkg = JSON.parse(readFileSync(join(rootDir, 'package.json'), 'utf-8'));
|
|
134
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
135
|
+
if (allDeps['next'])
|
|
136
|
+
profile.framework = 'next.js';
|
|
137
|
+
else if (allDeps['nuxt'] || allDeps['nuxt3'])
|
|
138
|
+
profile.framework = 'nuxt';
|
|
139
|
+
else if (allDeps['@angular/core'])
|
|
140
|
+
profile.framework = 'angular';
|
|
141
|
+
else if (allDeps['svelte'] || allDeps['@sveltejs/kit'])
|
|
142
|
+
profile.framework = 'svelte';
|
|
143
|
+
else if (allDeps['vue'])
|
|
144
|
+
profile.framework = 'vue';
|
|
145
|
+
else if (allDeps['react'])
|
|
146
|
+
profile.framework = 'react';
|
|
147
|
+
else if (allDeps['express'])
|
|
148
|
+
profile.framework = 'express';
|
|
149
|
+
else if (allDeps['fastify'])
|
|
150
|
+
profile.framework = 'fastify';
|
|
151
|
+
else if (allDeps['hono'])
|
|
152
|
+
profile.framework = 'hono';
|
|
153
|
+
// Test detection
|
|
154
|
+
if (allDeps['jest'] || allDeps['vitest'] || allDeps['mocha'] || allDeps['ava'] ||
|
|
155
|
+
allDeps['@testing-library/react'] || allDeps['playwright'] || allDeps['cypress']) {
|
|
156
|
+
profile.hasTests = true;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
catch { /* malformed package.json */ }
|
|
160
|
+
}
|
|
161
|
+
else if (existsSync(join(rootDir, 'Cargo.toml'))) {
|
|
162
|
+
profile.language = 'rust';
|
|
163
|
+
profile.hasTypeConfig = true; // Rust is always typed
|
|
164
|
+
profile.hasTests = true; // Rust has built-in tests
|
|
165
|
+
}
|
|
166
|
+
else if (existsSync(join(rootDir, 'go.mod'))) {
|
|
167
|
+
profile.language = 'go';
|
|
168
|
+
profile.hasTypeConfig = true; // Go is always typed
|
|
169
|
+
// Check for test files
|
|
170
|
+
const goTestOutput = execQuiet('find . -name "*_test.go" -maxdepth 3 | head -1', rootDir, 5000);
|
|
171
|
+
if (goTestOutput)
|
|
172
|
+
profile.hasTests = true;
|
|
173
|
+
}
|
|
174
|
+
else if (existsSync(join(rootDir, 'pyproject.toml')) || existsSync(join(rootDir, 'setup.py'))) {
|
|
175
|
+
profile.language = 'python';
|
|
176
|
+
if (existsSync(join(rootDir, 'mypy.ini')) || existsSync(join(rootDir, 'pyrightconfig.json'))) {
|
|
177
|
+
profile.hasTypeConfig = true;
|
|
178
|
+
}
|
|
179
|
+
// Check pyproject.toml for framework + test tools
|
|
180
|
+
try {
|
|
181
|
+
const pyproject = readFileSync(join(rootDir, 'pyproject.toml'), 'utf-8');
|
|
182
|
+
if (/django/i.test(pyproject))
|
|
183
|
+
profile.framework = 'django';
|
|
184
|
+
else if (/flask/i.test(pyproject))
|
|
185
|
+
profile.framework = 'flask';
|
|
186
|
+
else if (/fastapi/i.test(pyproject))
|
|
187
|
+
profile.framework = 'fastapi';
|
|
188
|
+
if (/pytest|unittest|nose/i.test(pyproject))
|
|
189
|
+
profile.hasTests = true;
|
|
190
|
+
}
|
|
191
|
+
catch { /* no pyproject */ }
|
|
192
|
+
}
|
|
193
|
+
else if (existsSync(join(rootDir, 'Gemfile'))) {
|
|
194
|
+
profile.language = 'ruby';
|
|
195
|
+
if (existsSync(join(rootDir, 'config', 'routes.rb')))
|
|
196
|
+
profile.framework = 'rails';
|
|
197
|
+
}
|
|
198
|
+
// Fallback test detection: check for test directories
|
|
199
|
+
if (!profile.hasTests) {
|
|
200
|
+
const testDirs = ['test', 'tests', '__tests__', 'spec', 'specs'];
|
|
201
|
+
for (const td of testDirs) {
|
|
202
|
+
if (existsSync(join(rootDir, td))) {
|
|
203
|
+
profile.hasTests = true;
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return profile;
|
|
209
|
+
}
|
|
210
|
+
function scanTodos(files, rootDir) {
|
|
211
|
+
const todos = [];
|
|
212
|
+
const pattern = /\b(TODO|FIXME|HACK|XXX)\b[:\s]*(.*)/i;
|
|
213
|
+
for (const file of files) {
|
|
214
|
+
let source;
|
|
215
|
+
try {
|
|
216
|
+
source = readFileSync(file, 'utf-8');
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
const relPath = relative(rootDir, file);
|
|
222
|
+
const lines = source.split('\n');
|
|
223
|
+
for (let i = 0; i < lines.length; i++) {
|
|
224
|
+
const match = pattern.exec(lines[i]);
|
|
225
|
+
if (match) {
|
|
226
|
+
todos.push({
|
|
227
|
+
file: relPath,
|
|
228
|
+
line: i + 1,
|
|
229
|
+
text: match[2]?.trim() || match[0],
|
|
230
|
+
type: match[1].toUpperCase(),
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return todos;
|
|
236
|
+
}
|
|
237
|
+
function scanComplexity(files, rootDir) {
|
|
238
|
+
const results = [];
|
|
239
|
+
const funcPattern = /(?:export\s+)?(?:async\s+)?(?:function\s+(\w+)|(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?(?:\([^)]*\)|[a-zA-Z_]\w*)\s*=>|(\w+)\s*\([^)]*\)\s*(?::\s*\w[^{]*)?\s*\{)/;
|
|
240
|
+
for (const file of files) {
|
|
241
|
+
let source;
|
|
242
|
+
try {
|
|
243
|
+
source = readFileSync(file, 'utf-8');
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
const relPath = relative(rootDir, file);
|
|
249
|
+
const lines = source.split('\n');
|
|
250
|
+
let i = 0;
|
|
251
|
+
while (i < lines.length) {
|
|
252
|
+
const match = funcPattern.exec(lines[i]);
|
|
253
|
+
if (match) {
|
|
254
|
+
const funcName = match[1] || match[2] || match[3] || 'anonymous';
|
|
255
|
+
const startLine = i + 1;
|
|
256
|
+
let braceDepth = 0;
|
|
257
|
+
let funcStarted = false;
|
|
258
|
+
let bodyLines = 0;
|
|
259
|
+
let j = i;
|
|
260
|
+
while (j < lines.length) {
|
|
261
|
+
for (const ch of lines[j]) {
|
|
262
|
+
if (ch === '{') {
|
|
263
|
+
braceDepth++;
|
|
264
|
+
if (!funcStarted)
|
|
265
|
+
funcStarted = true;
|
|
266
|
+
}
|
|
267
|
+
else if (ch === '}') {
|
|
268
|
+
braceDepth--;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (funcStarted)
|
|
272
|
+
bodyLines++;
|
|
273
|
+
if (funcStarted && braceDepth === 0)
|
|
274
|
+
break;
|
|
275
|
+
j++;
|
|
276
|
+
}
|
|
277
|
+
if (bodyLines > 60) {
|
|
278
|
+
results.push({ file: relPath, name: funcName, lineCount: bodyLines, startLine });
|
|
279
|
+
}
|
|
280
|
+
i = j + 1;
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
i++;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
results.sort((a, b) => b.lineCount - a.lineCount);
|
|
288
|
+
return results.slice(0, 20);
|
|
289
|
+
}
|
|
290
|
+
// ── Duplicate Pattern Scanner (simplified from codebase-guardian) ──
|
|
291
|
+
function normalizeLine(line) {
|
|
292
|
+
return line
|
|
293
|
+
.replace(/\/\/.*$/, '')
|
|
294
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
295
|
+
.replace(/#.*$/, '')
|
|
296
|
+
.replace(/\s+/g, ' ')
|
|
297
|
+
.trim();
|
|
298
|
+
}
|
|
299
|
+
function scanDuplicates(files, rootDir) {
|
|
300
|
+
const CHUNK_SIZE = 4;
|
|
301
|
+
const chunkMap = new Map();
|
|
302
|
+
const chunkOriginals = new Map();
|
|
303
|
+
for (const file of files) {
|
|
304
|
+
let source;
|
|
305
|
+
try {
|
|
306
|
+
source = readFileSync(file, 'utf-8');
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
const relPath = relative(rootDir, file);
|
|
312
|
+
const lines = source.split('\n').map(normalizeLine).filter(l => l.length > 10);
|
|
313
|
+
for (let i = 0; i <= lines.length - CHUNK_SIZE; i++) {
|
|
314
|
+
const chunk = lines.slice(i, i + CHUNK_SIZE).join('\n');
|
|
315
|
+
if (chunk.length > 30) {
|
|
316
|
+
if (!chunkMap.has(chunk)) {
|
|
317
|
+
chunkMap.set(chunk, new Set());
|
|
318
|
+
// Store original for reporting
|
|
319
|
+
const rawLines = source.split('\n');
|
|
320
|
+
for (let j = 0; j < rawLines.length - CHUNK_SIZE; j++) {
|
|
321
|
+
const normalized = rawLines.slice(j, j + CHUNK_SIZE)
|
|
322
|
+
.map(normalizeLine).filter(l => l.length > 10).join('\n');
|
|
323
|
+
if (normalized === chunk) {
|
|
324
|
+
chunkOriginals.set(chunk, rawLines.slice(j, j + CHUNK_SIZE).join('\n'));
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
chunkMap.get(chunk).add(relPath);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
const duplicates = [];
|
|
334
|
+
for (const [chunk, fileSet] of chunkMap) {
|
|
335
|
+
if (fileSet.size >= 3) {
|
|
336
|
+
duplicates.push({
|
|
337
|
+
pattern: (chunkOriginals.get(chunk) || chunk).slice(0, 200),
|
|
338
|
+
files: Array.from(fileSet),
|
|
339
|
+
occurrences: fileSet.size,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
duplicates.sort((a, b) => b.occurrences - a.occurrences);
|
|
344
|
+
return duplicates.slice(0, 10);
|
|
345
|
+
}
|
|
346
|
+
// ── Typo Scanner ──
|
|
347
|
+
// Common programming typos in identifiers and comments
|
|
348
|
+
const COMMON_TYPOS = [
|
|
349
|
+
[/\brecieve\b/gi, 'receive'],
|
|
350
|
+
[/\boccured\b/gi, 'occurred'],
|
|
351
|
+
[/\bseperatel?y?\b/gi, 'separately'],
|
|
352
|
+
[/\bdefau[lt]{2,}s?\b/gi, 'default(s)'],
|
|
353
|
+
[/\baccidentaly\b/gi, 'accidentally'],
|
|
354
|
+
[/\bneccessary\b/gi, 'necessary'],
|
|
355
|
+
[/\boccurr?ance\b/gi, 'occurrence'],
|
|
356
|
+
[/\bwidht\b/gi, 'width'],
|
|
357
|
+
[/\bheigth\b/gi, 'height'],
|
|
358
|
+
[/\blegnth\b/gi, 'length'],
|
|
359
|
+
[/\bfunciton\b/gi, 'function'],
|
|
360
|
+
[/\bretrun\b/gi, 'return'],
|
|
361
|
+
[/\bparmeter\b/gi, 'parameter'],
|
|
362
|
+
[/\bargumnet\b/gi, 'argument'],
|
|
363
|
+
[/\benviroment\b/gi, 'environment'],
|
|
364
|
+
[/\bresponc?se\b/gi, 'response'],
|
|
365
|
+
[/\binitilaize\b/gi, 'initialize'],
|
|
366
|
+
[/\bsucess\b/gi, 'success'],
|
|
367
|
+
[/\babstarct\b/gi, 'abstract'],
|
|
368
|
+
[/\boveride\b/gi, 'override'],
|
|
369
|
+
[/\binterupts?\b/gi, 'interrupt(s)'],
|
|
370
|
+
[/\bwether\b/gi, 'whether'],
|
|
371
|
+
[/\bteh\b/gi, 'the'],
|
|
372
|
+
[/\badn\b/gi, 'and'],
|
|
373
|
+
];
|
|
374
|
+
function scanTypos(files, rootDir) {
|
|
375
|
+
const typos = [];
|
|
376
|
+
for (const file of files) {
|
|
377
|
+
let source;
|
|
378
|
+
try {
|
|
379
|
+
source = readFileSync(file, 'utf-8');
|
|
380
|
+
}
|
|
381
|
+
catch {
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
const relPath = relative(rootDir, file);
|
|
385
|
+
const lines = source.split('\n');
|
|
386
|
+
for (let i = 0; i < lines.length; i++) {
|
|
387
|
+
const line = lines[i];
|
|
388
|
+
// Only check comments and string literals for typos
|
|
389
|
+
const isComment = /^\s*(\/\/|#|\*|\/\*)/.test(line);
|
|
390
|
+
const hasString = /['"`]/.test(line);
|
|
391
|
+
if (!isComment && !hasString)
|
|
392
|
+
continue;
|
|
393
|
+
for (const [pattern, fix] of COMMON_TYPOS) {
|
|
394
|
+
const match = pattern.exec(line);
|
|
395
|
+
if (match) {
|
|
396
|
+
typos.push({
|
|
397
|
+
file: relPath,
|
|
398
|
+
line: i + 1,
|
|
399
|
+
found: match[0],
|
|
400
|
+
suggestion: fix,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
// Reset regex lastIndex for global patterns
|
|
404
|
+
pattern.lastIndex = 0;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return typos.slice(0, 30);
|
|
409
|
+
}
|
|
410
|
+
function scanMissingTypes(files, rootDir) {
|
|
411
|
+
const results = [];
|
|
412
|
+
// Only check .ts/.tsx files
|
|
413
|
+
const tsFiles = files.filter(f => f.endsWith('.ts') || f.endsWith('.tsx'));
|
|
414
|
+
for (const file of tsFiles) {
|
|
415
|
+
let source;
|
|
416
|
+
try {
|
|
417
|
+
source = readFileSync(file, 'utf-8');
|
|
418
|
+
}
|
|
419
|
+
catch {
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
const relPath = relative(rootDir, file);
|
|
423
|
+
const lines = source.split('\n');
|
|
424
|
+
for (let i = 0; i < lines.length; i++) {
|
|
425
|
+
const line = lines[i];
|
|
426
|
+
// Exported function without return type
|
|
427
|
+
if (/^export\s+(async\s+)?function\s+\w+\s*\([^)]*\)\s*\{/.test(line) &&
|
|
428
|
+
!/\)\s*:\s*\S/.test(line)) {
|
|
429
|
+
results.push({ file: relPath, line: i + 1, code: line.trim().slice(0, 80) });
|
|
430
|
+
}
|
|
431
|
+
// Exported const arrow function without return type (only top-level)
|
|
432
|
+
if (/^export\s+const\s+\w+\s*=\s*(async\s+)?\(/.test(line) &&
|
|
433
|
+
!/\)\s*:\s*\S/.test(line) &&
|
|
434
|
+
/=>\s*\{?\s*$/.test(line)) {
|
|
435
|
+
results.push({ file: relPath, line: i + 1, code: line.trim().slice(0, 80) });
|
|
436
|
+
}
|
|
437
|
+
// `any` type usage
|
|
438
|
+
if (/:\s*any\b/.test(line) && !/\/\/.*any/.test(line) && !/eslint-disable/.test(line)) {
|
|
439
|
+
results.push({ file: relPath, line: i + 1, code: line.trim().slice(0, 80) });
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
return results.slice(0, 30);
|
|
444
|
+
}
|
|
445
|
+
// ── Build Findings ──
|
|
446
|
+
function buildFindings(rootDir, files, profile) {
|
|
447
|
+
const findings = [];
|
|
448
|
+
// TODOs
|
|
449
|
+
const todos = scanTodos(files, rootDir);
|
|
450
|
+
for (const todo of todos) {
|
|
451
|
+
findings.push({
|
|
452
|
+
category: 'todo-removal',
|
|
453
|
+
severity: todo.type === 'FIXME' || todo.type === 'HACK' ? 'warn' : 'info',
|
|
454
|
+
title: `${todo.type}: ${todo.text.slice(0, 60)}`,
|
|
455
|
+
description: `${todo.type} comment at ${todo.file}:${todo.line} — "${todo.text}"`,
|
|
456
|
+
file: todo.file,
|
|
457
|
+
line: todo.line,
|
|
458
|
+
original: todo.text,
|
|
459
|
+
isSimpleFix: todo.type === 'TODO' && todo.text.length < 80,
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
// Typos
|
|
463
|
+
const typos = scanTypos(files, rootDir);
|
|
464
|
+
for (const typo of typos) {
|
|
465
|
+
findings.push({
|
|
466
|
+
category: 'typo',
|
|
467
|
+
severity: 'info',
|
|
468
|
+
title: `Typo: "${typo.found}" should be "${typo.suggestion}"`,
|
|
469
|
+
description: `Found "${typo.found}" at ${typo.file}:${typo.line}. Suggested correction: "${typo.suggestion}"`,
|
|
470
|
+
file: typo.file,
|
|
471
|
+
line: typo.line,
|
|
472
|
+
original: typo.found,
|
|
473
|
+
isSimpleFix: true,
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
// Missing types (TypeScript only)
|
|
477
|
+
if (profile.language === 'typescript') {
|
|
478
|
+
const missingTypes = scanMissingTypes(files, rootDir);
|
|
479
|
+
for (const mt of missingTypes) {
|
|
480
|
+
const isAny = mt.code.includes(': any');
|
|
481
|
+
findings.push({
|
|
482
|
+
category: 'missing-type',
|
|
483
|
+
severity: isAny ? 'warn' : 'info',
|
|
484
|
+
title: isAny
|
|
485
|
+
? `Explicit \`any\` type at ${mt.file}:${mt.line}`
|
|
486
|
+
: `Missing return type at ${mt.file}:${mt.line}`,
|
|
487
|
+
description: isAny
|
|
488
|
+
? `Replace explicit \`any\` with a proper type: ${mt.code}`
|
|
489
|
+
: `Exported function missing return type annotation: ${mt.code}`,
|
|
490
|
+
file: mt.file,
|
|
491
|
+
line: mt.line,
|
|
492
|
+
original: mt.code,
|
|
493
|
+
isSimpleFix: isAny,
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
// Complexity
|
|
498
|
+
const complex = scanComplexity(files, rootDir);
|
|
499
|
+
for (const fn of complex) {
|
|
500
|
+
findings.push({
|
|
501
|
+
category: 'complexity',
|
|
502
|
+
severity: fn.lineCount > 100 ? 'critical' : 'warn',
|
|
503
|
+
title: `Long function: ${fn.name} (${fn.lineCount} lines)`,
|
|
504
|
+
description: `${fn.file}:${fn.startLine} — function \`${fn.name}\` is ${fn.lineCount} lines long. Consider breaking into smaller functions.`,
|
|
505
|
+
file: fn.file,
|
|
506
|
+
line: fn.startLine,
|
|
507
|
+
isSimpleFix: false,
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
// Duplicates
|
|
511
|
+
const duplicates = scanDuplicates(files, rootDir);
|
|
512
|
+
for (const dup of duplicates) {
|
|
513
|
+
findings.push({
|
|
514
|
+
category: 'duplicate-pattern',
|
|
515
|
+
severity: dup.occurrences >= 5 ? 'critical' : 'warn',
|
|
516
|
+
title: `Repeated code block in ${dup.occurrences} files`,
|
|
517
|
+
description: `A 4-line code block appears in ${dup.occurrences} files: ${dup.files.slice(0, 3).join(', ')}${dup.files.length > 3 ? ` (+${dup.files.length - 3} more)` : ''}`,
|
|
518
|
+
file: dup.files[0],
|
|
519
|
+
original: dup.pattern,
|
|
520
|
+
isSimpleFix: false,
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
// Missing docs: exported functions without JSDoc
|
|
524
|
+
if (profile.language === 'typescript' || profile.language === 'javascript') {
|
|
525
|
+
let undocumented = 0;
|
|
526
|
+
for (const file of files.slice(0, 100)) {
|
|
527
|
+
let source;
|
|
528
|
+
try {
|
|
529
|
+
source = readFileSync(file, 'utf-8');
|
|
530
|
+
}
|
|
531
|
+
catch {
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
const relPath = relative(rootDir, file);
|
|
535
|
+
const lines = source.split('\n');
|
|
536
|
+
for (let i = 0; i < lines.length; i++) {
|
|
537
|
+
if (/^export\s+(async\s+)?function\s+\w+/.test(lines[i])) {
|
|
538
|
+
// Check if preceded by JSDoc
|
|
539
|
+
const prevLine = i > 0 ? lines[i - 1].trim() : '';
|
|
540
|
+
const prevPrevLine = i > 1 ? lines[i - 2].trim() : '';
|
|
541
|
+
if (!prevLine.endsWith('*/') && !prevPrevLine.endsWith('*/')) {
|
|
542
|
+
undocumented++;
|
|
543
|
+
if (undocumented <= 10) {
|
|
544
|
+
findings.push({
|
|
545
|
+
category: 'missing-docs',
|
|
546
|
+
severity: 'info',
|
|
547
|
+
title: `Exported function without JSDoc: ${relPath}:${i + 1}`,
|
|
548
|
+
description: `The exported function at ${relPath}:${i + 1} has no JSDoc comment. Adding documentation improves maintainability.`,
|
|
549
|
+
file: relPath,
|
|
550
|
+
line: i + 1,
|
|
551
|
+
original: lines[i].trim().slice(0, 80),
|
|
552
|
+
isSimpleFix: true,
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
// Project-level findings
|
|
561
|
+
if (!profile.hasReadme) {
|
|
562
|
+
findings.push({
|
|
563
|
+
category: 'missing-docs',
|
|
564
|
+
severity: 'critical',
|
|
565
|
+
title: 'No README.md',
|
|
566
|
+
description: 'This project has no README. Every project needs a README explaining what it does and how to use it.',
|
|
567
|
+
file: 'README.md',
|
|
568
|
+
isSimpleFix: false,
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
if (!profile.hasLicense) {
|
|
572
|
+
findings.push({
|
|
573
|
+
category: 'other',
|
|
574
|
+
severity: 'warn',
|
|
575
|
+
title: 'No LICENSE file',
|
|
576
|
+
description: 'Without a license, others cannot legally use this code. Consider adding MIT, Apache 2.0, or another open-source license.',
|
|
577
|
+
file: 'LICENSE',
|
|
578
|
+
isSimpleFix: true,
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
if (!profile.hasContributing) {
|
|
582
|
+
findings.push({
|
|
583
|
+
category: 'missing-docs',
|
|
584
|
+
severity: 'info',
|
|
585
|
+
title: 'No CONTRIBUTING.md',
|
|
586
|
+
description: 'Adding a CONTRIBUTING.md helps new contributors understand how to get started.',
|
|
587
|
+
file: 'CONTRIBUTING.md',
|
|
588
|
+
isSimpleFix: true,
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
if (!profile.hasCI) {
|
|
592
|
+
findings.push({
|
|
593
|
+
category: 'other',
|
|
594
|
+
severity: 'warn',
|
|
595
|
+
title: 'No CI configuration detected',
|
|
596
|
+
description: 'No GitHub Actions, CircleCI, Travis, or Jenkins configuration found. CI helps catch regressions automatically.',
|
|
597
|
+
file: '.github/workflows',
|
|
598
|
+
isSimpleFix: false,
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
return findings;
|
|
602
|
+
}
|
|
603
|
+
// ── Build Proposed Fixes ──
|
|
604
|
+
function buildProposedFixes(findings) {
|
|
605
|
+
const fixes = [];
|
|
606
|
+
for (const finding of findings) {
|
|
607
|
+
if (!finding.isSimpleFix)
|
|
608
|
+
continue;
|
|
609
|
+
let changeSummary;
|
|
610
|
+
let estimatedReviewMinutes;
|
|
611
|
+
switch (finding.category) {
|
|
612
|
+
case 'todo-removal':
|
|
613
|
+
changeSummary = `Address the ${finding.original?.split(':')[0] || 'TODO'} at ${finding.file}:${finding.line ?? '?'}. Either implement the requested change or remove the stale comment.`;
|
|
614
|
+
estimatedReviewMinutes = 5;
|
|
615
|
+
break;
|
|
616
|
+
case 'typo':
|
|
617
|
+
changeSummary = `Fix typo: change "${finding.original}" to the correct spelling in ${finding.file}:${finding.line ?? '?'}.`;
|
|
618
|
+
estimatedReviewMinutes = 1;
|
|
619
|
+
break;
|
|
620
|
+
case 'missing-type':
|
|
621
|
+
changeSummary = `Add proper type annotation to replace \`any\` or add return type at ${finding.file}:${finding.line ?? '?'}.`;
|
|
622
|
+
estimatedReviewMinutes = 3;
|
|
623
|
+
break;
|
|
624
|
+
case 'missing-docs':
|
|
625
|
+
changeSummary = `Add JSDoc documentation for the exported function at ${finding.file}:${finding.line ?? '?'}.`;
|
|
626
|
+
estimatedReviewMinutes = 3;
|
|
627
|
+
break;
|
|
628
|
+
default:
|
|
629
|
+
changeSummary = `Address: ${finding.title}`;
|
|
630
|
+
estimatedReviewMinutes = 5;
|
|
631
|
+
break;
|
|
632
|
+
}
|
|
633
|
+
fixes.push({
|
|
634
|
+
finding,
|
|
635
|
+
description: finding.description,
|
|
636
|
+
changeSummary,
|
|
637
|
+
estimatedReviewMinutes,
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
// Sort: typos first (easiest), then missing-type, then todos, then docs
|
|
641
|
+
const categoryOrder = {
|
|
642
|
+
'typo': 0,
|
|
643
|
+
'missing-type': 1,
|
|
644
|
+
'todo-removal': 2,
|
|
645
|
+
'missing-docs': 3,
|
|
646
|
+
};
|
|
647
|
+
fixes.sort((a, b) => {
|
|
648
|
+
const aOrder = categoryOrder[a.finding.category] ?? 10;
|
|
649
|
+
const bOrder = categoryOrder[b.finding.category] ?? 10;
|
|
650
|
+
return aOrder - bOrder;
|
|
651
|
+
});
|
|
652
|
+
return fixes;
|
|
653
|
+
}
|
|
654
|
+
// ── Main: Autonomous Contributor ──
|
|
655
|
+
/**
|
|
656
|
+
* Point at any GitHub repo, clone it, analyze it, and generate a contribution
|
|
657
|
+
* report with findings and proposed fixes.
|
|
658
|
+
*
|
|
659
|
+
* v1 is analysis-only — no automatic PR creation. The report gives you
|
|
660
|
+
* everything needed to make targeted contributions.
|
|
661
|
+
*/
|
|
662
|
+
export async function runAutonomousContributor(repoUrl, options = {}) {
|
|
663
|
+
const { maxFiles = 500, minSeverity = 'info', localPath, keepClone = false, } = options;
|
|
664
|
+
const repoId = parseRepoIdentifier(repoUrl);
|
|
665
|
+
const clonedAt = new Date().toISOString();
|
|
666
|
+
// ── Clone or use local path ──
|
|
667
|
+
let rootDir;
|
|
668
|
+
if (localPath) {
|
|
669
|
+
rootDir = localPath;
|
|
670
|
+
if (!existsSync(rootDir)) {
|
|
671
|
+
throw new Error(`Local path does not exist: ${rootDir}`);
|
|
672
|
+
}
|
|
673
|
+
console.log(`Analyzing local path: ${rootDir}`);
|
|
674
|
+
}
|
|
675
|
+
else {
|
|
676
|
+
rootDir = join(tmpdir(), `kbot-contributor-${repoId.replace('/', '-')}-${Date.now()}`);
|
|
677
|
+
console.log(`Cloning ${repoId} to ${rootDir}...`);
|
|
678
|
+
const cloneResult = execQuiet(`git clone --depth 1 --single-branch ${cloneUrl(repoId)} "${rootDir}"`, tmpdir(), 60000);
|
|
679
|
+
if (cloneResult === null && !existsSync(join(rootDir, '.git'))) {
|
|
680
|
+
throw new Error(`Failed to clone ${repoId}. Is the repository public and accessible?`);
|
|
681
|
+
}
|
|
682
|
+
console.log('Clone complete.');
|
|
683
|
+
}
|
|
684
|
+
try {
|
|
685
|
+
// ── Detect project profile ──
|
|
686
|
+
console.log('Detecting project profile...');
|
|
687
|
+
const profile = detectProjectProfile(rootDir);
|
|
688
|
+
console.log(` Language: ${profile.language}, Framework: ${profile.framework}`);
|
|
689
|
+
// ── Walk code files ──
|
|
690
|
+
console.log('Scanning files...');
|
|
691
|
+
const files = walkCodeFiles(rootDir, maxFiles);
|
|
692
|
+
console.log(` ${files.length} code files found`);
|
|
693
|
+
// ── Run analysis ──
|
|
694
|
+
console.log('Running analysis...');
|
|
695
|
+
const findings = buildFindings(rootDir, files, profile);
|
|
696
|
+
// Filter by minimum severity
|
|
697
|
+
const minSevOrder = SEVERITY_ORDER[minSeverity];
|
|
698
|
+
const filteredFindings = findings.filter(f => SEVERITY_ORDER[f.severity] >= minSevOrder);
|
|
699
|
+
// ── Build proposed fixes ──
|
|
700
|
+
const proposed_fixes = buildProposedFixes(filteredFindings);
|
|
701
|
+
// ── Build impact summary ──
|
|
702
|
+
const categories = {};
|
|
703
|
+
for (const f of filteredFindings) {
|
|
704
|
+
categories[f.category] = (categories[f.category] || 0) + 1;
|
|
705
|
+
}
|
|
706
|
+
const analyzedAt = new Date().toISOString();
|
|
707
|
+
const report = {
|
|
708
|
+
repo: repoId,
|
|
709
|
+
clonedAt,
|
|
710
|
+
analyzedAt,
|
|
711
|
+
language: profile.language,
|
|
712
|
+
framework: profile.framework,
|
|
713
|
+
filesScanned: files.length,
|
|
714
|
+
findings: filteredFindings,
|
|
715
|
+
proposed_fixes,
|
|
716
|
+
estimated_impact: {
|
|
717
|
+
totalFindings: filteredFindings.length,
|
|
718
|
+
simpleFixes: proposed_fixes.length,
|
|
719
|
+
complexFindings: filteredFindings.length - proposed_fixes.length,
|
|
720
|
+
categories,
|
|
721
|
+
},
|
|
722
|
+
projectHealth: {
|
|
723
|
+
hasReadme: profile.hasReadme,
|
|
724
|
+
hasLicense: profile.hasLicense,
|
|
725
|
+
hasTests: profile.hasTests,
|
|
726
|
+
hasCI: profile.hasCI,
|
|
727
|
+
hasContributing: profile.hasContributing,
|
|
728
|
+
hasTypeConfig: profile.hasTypeConfig,
|
|
729
|
+
},
|
|
730
|
+
};
|
|
731
|
+
// Save report
|
|
732
|
+
ensureDir(REPORT_DIR);
|
|
733
|
+
const reportFile = join(REPORT_DIR, `${repoId.replace('/', '-')}-${Date.now()}.json`);
|
|
734
|
+
writeFileSync(reportFile, JSON.stringify(report, null, 2), 'utf-8');
|
|
735
|
+
console.log(`Analysis complete: ${filteredFindings.length} findings, ` +
|
|
736
|
+
`${proposed_fixes.length} proposed fixes. Report saved to ${reportFile}`);
|
|
737
|
+
return report;
|
|
738
|
+
}
|
|
739
|
+
finally {
|
|
740
|
+
// Clean up clone unless keepClone is set or using localPath
|
|
741
|
+
if (!localPath && !keepClone && existsSync(rootDir)) {
|
|
742
|
+
try {
|
|
743
|
+
rmSync(rootDir, { recursive: true, force: true });
|
|
744
|
+
console.log('Cleaned up temporary clone.');
|
|
745
|
+
}
|
|
746
|
+
catch {
|
|
747
|
+
console.log(`Warning: could not clean up ${rootDir}`);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
// ── Good First Issues ──
|
|
753
|
+
/**
|
|
754
|
+
* Fetch open issues labeled "good first issue" or "help wanted" from a GitHub repo.
|
|
755
|
+
* Uses the public GitHub API (no auth required for public repos).
|
|
756
|
+
*/
|
|
757
|
+
export async function listGoodFirstIssues(repoUrl) {
|
|
758
|
+
const repoId = parseRepoIdentifier(repoUrl);
|
|
759
|
+
const labels = ['good first issue', 'help wanted', 'beginner-friendly', 'easy'];
|
|
760
|
+
const allIssues = [];
|
|
761
|
+
const seenNumbers = new Set();
|
|
762
|
+
for (const label of labels) {
|
|
763
|
+
const encoded = encodeURIComponent(label);
|
|
764
|
+
const url = `https://api.github.com/repos/${repoId}/issues?labels=${encoded}&state=open&per_page=20&sort=created&direction=desc`;
|
|
765
|
+
try {
|
|
766
|
+
const res = await fetch(url, {
|
|
767
|
+
headers: {
|
|
768
|
+
'User-Agent': 'kbot-contributor/1.0',
|
|
769
|
+
'Accept': 'application/vnd.github.v3+json',
|
|
770
|
+
},
|
|
771
|
+
signal: AbortSignal.timeout(10000),
|
|
772
|
+
});
|
|
773
|
+
if (!res.ok)
|
|
774
|
+
continue;
|
|
775
|
+
const items = await res.json();
|
|
776
|
+
for (const item of items) {
|
|
777
|
+
// Skip PRs (GitHub returns them in the issues endpoint)
|
|
778
|
+
if (item.pull_request)
|
|
779
|
+
continue;
|
|
780
|
+
if (seenNumbers.has(item.number))
|
|
781
|
+
continue;
|
|
782
|
+
seenNumbers.add(item.number);
|
|
783
|
+
allIssues.push({
|
|
784
|
+
number: item.number,
|
|
785
|
+
title: item.title,
|
|
786
|
+
url: item.html_url,
|
|
787
|
+
labels: item.labels.map(l => l.name),
|
|
788
|
+
created_at: item.created_at,
|
|
789
|
+
author: item.user.login,
|
|
790
|
+
comments: item.comments,
|
|
791
|
+
body_preview: (item.body || '').slice(0, 200),
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
catch {
|
|
796
|
+
// API error for this label, continue with others
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
// Sort by newest first
|
|
800
|
+
allIssues.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
|
|
801
|
+
return allIssues;
|
|
802
|
+
}
|
|
803
|
+
// ── Report Formatting ──
|
|
804
|
+
/**
|
|
805
|
+
* Format a ContributionReport as a readable markdown string.
|
|
806
|
+
*/
|
|
807
|
+
export function formatContributionReport(report) {
|
|
808
|
+
const lines = [
|
|
809
|
+
`# Contribution Report: ${report.repo}`,
|
|
810
|
+
'',
|
|
811
|
+
`> Analyzed ${report.filesScanned} files | Language: ${report.language} | Framework: ${report.framework}`,
|
|
812
|
+
`> ${report.analyzedAt}`,
|
|
813
|
+
'',
|
|
814
|
+
'## Project Health',
|
|
815
|
+
'',
|
|
816
|
+
`| Check | Status |`,
|
|
817
|
+
`|-------|--------|`,
|
|
818
|
+
`| README | ${report.projectHealth.hasReadme ? 'Yes' : 'Missing'} |`,
|
|
819
|
+
`| LICENSE | ${report.projectHealth.hasLicense ? 'Yes' : 'Missing'} |`,
|
|
820
|
+
`| Tests | ${report.projectHealth.hasTests ? 'Yes' : 'None detected'} |`,
|
|
821
|
+
`| CI | ${report.projectHealth.hasCI ? 'Yes' : 'None detected'} |`,
|
|
822
|
+
`| CONTRIBUTING | ${report.projectHealth.hasContributing ? 'Yes' : 'Missing'} |`,
|
|
823
|
+
`| Type Config | ${report.projectHealth.hasTypeConfig ? 'Yes' : 'None'} |`,
|
|
824
|
+
'',
|
|
825
|
+
'## Impact Summary',
|
|
826
|
+
'',
|
|
827
|
+
`- **Total findings:** ${report.estimated_impact.totalFindings}`,
|
|
828
|
+
`- **Simple fixes (PR-ready):** ${report.estimated_impact.simpleFixes}`,
|
|
829
|
+
`- **Complex findings:** ${report.estimated_impact.complexFindings}`,
|
|
830
|
+
'',
|
|
831
|
+
'### By Category',
|
|
832
|
+
'',
|
|
833
|
+
];
|
|
834
|
+
for (const [cat, count] of Object.entries(report.estimated_impact.categories).sort((a, b) => b[1] - a[1])) {
|
|
835
|
+
lines.push(`- ${cat}: ${count}`);
|
|
836
|
+
}
|
|
837
|
+
if (report.proposed_fixes.length > 0) {
|
|
838
|
+
lines.push('');
|
|
839
|
+
lines.push('## Proposed Fixes');
|
|
840
|
+
lines.push('');
|
|
841
|
+
for (const fix of report.proposed_fixes.slice(0, 20)) {
|
|
842
|
+
lines.push(`### ${fix.finding.title}`);
|
|
843
|
+
lines.push(`- **File:** ${fix.finding.file}${fix.finding.line ? `:${fix.finding.line}` : ''}`);
|
|
844
|
+
lines.push(`- **Change:** ${fix.changeSummary}`);
|
|
845
|
+
lines.push(`- **Review time:** ~${fix.estimatedReviewMinutes} min`);
|
|
846
|
+
lines.push('');
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
const criticalFindings = report.findings.filter(f => f.severity === 'critical');
|
|
850
|
+
if (criticalFindings.length > 0) {
|
|
851
|
+
lines.push('## Critical Findings');
|
|
852
|
+
lines.push('');
|
|
853
|
+
for (const f of criticalFindings) {
|
|
854
|
+
lines.push(`- **${f.title}** (${f.file}) — ${f.description}`);
|
|
855
|
+
}
|
|
856
|
+
lines.push('');
|
|
857
|
+
}
|
|
858
|
+
lines.push('---');
|
|
859
|
+
lines.push('*Generated by kbot Autonomous Contributor*');
|
|
860
|
+
return lines.join('\n');
|
|
861
|
+
}
|
|
862
|
+
//# sourceMappingURL=autonomous-contributor.js.map
|