@kernlang/cli 3.1.6 → 3.1.7
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/dist/cli.js +32 -2440
- package/dist/cli.js.map +1 -1
- package/dist/commands/compile.d.ts +1 -0
- package/dist/commands/compile.js +193 -0
- package/dist/commands/compile.js.map +1 -0
- package/dist/commands/confidence.d.ts +1 -0
- package/dist/commands/confidence.js +105 -0
- package/dist/commands/confidence.js.map +1 -0
- package/dist/commands/dev.d.ts +1 -0
- package/dist/commands/dev.js +123 -0
- package/dist/commands/dev.js.map +1 -0
- package/dist/commands/evolve/backfill.d.ts +1 -0
- package/dist/commands/evolve/backfill.js +88 -0
- package/dist/commands/evolve/backfill.js.map +1 -0
- package/dist/commands/evolve/discover.d.ts +1 -0
- package/dist/commands/evolve/discover.js +158 -0
- package/dist/commands/evolve/discover.js.map +1 -0
- package/dist/commands/evolve/index.d.ts +1 -0
- package/dist/commands/evolve/index.js +40 -0
- package/dist/commands/evolve/index.js.map +1 -0
- package/dist/commands/evolve/lifecycle.d.ts +8 -0
- package/dist/commands/evolve/lifecycle.js +190 -0
- package/dist/commands/evolve/lifecycle.js.map +1 -0
- package/dist/commands/evolve/main.d.ts +1 -0
- package/dist/commands/evolve/main.js +76 -0
- package/dist/commands/evolve/main.js.map +1 -0
- package/dist/commands/evolve/review-v4.d.ts +1 -0
- package/dist/commands/evolve/review-v4.js +181 -0
- package/dist/commands/evolve/review-v4.js.map +1 -0
- package/dist/commands/evolve/review.d.ts +1 -0
- package/dist/commands/evolve/review.js +63 -0
- package/dist/commands/evolve/review.js.map +1 -0
- package/dist/commands/review.d.ts +1 -0
- package/dist/commands/review.js +881 -0
- package/dist/commands/review.js.map +1 -0
- package/dist/commands/scan.d.ts +2 -0
- package/dist/commands/scan.js +89 -0
- package/dist/commands/scan.js.map +1 -0
- package/dist/commands/transpile.d.ts +2 -0
- package/dist/commands/transpile.js +286 -0
- package/dist/commands/transpile.js.map +1 -0
- package/dist/shared.d.ts +20 -0
- package/dist/shared.js +270 -0
- package/dist/shared.js.map +1 -0
- package/dist/transpiler-cli.d.ts +1 -1
- package/dist/transpiler-cli.js +10 -9
- package/dist/transpiler-cli.js.map +1 -1
- package/package.json +13 -13
|
@@ -0,0 +1,881 @@
|
|
|
1
|
+
import { clearTemplates, registerTemplate, VALID_TARGETS } from '@kernlang/core';
|
|
2
|
+
import { analyzeTaint, buildLLMPrompt, buildReviewInstructions, checkEnforcement, checkSpecFiles, clearReviewCache, dedup, exportKernIR, formatEnforcement, formatReport, formatSARIF, formatSummary, getRuleRegistry, isLLMAvailable, linkToNodes, resolveImportGraph, reviewFile, reviewGraph, runESLint, runLLMReview, runTSCDiagnosticsFromPaths, specViolationsToFindings, } from '@kernlang/review';
|
|
3
|
+
import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from 'fs';
|
|
4
|
+
import { basename, relative, resolve } from 'path';
|
|
5
|
+
import { collectTsFilesFlat, hasFlag, loadConfig, parseAndSurface, parseFlag } from '../shared.js';
|
|
6
|
+
// ── Review pipeline ──────────────────────────────────────────────────────
|
|
7
|
+
async function runReviewPipeline(reviewConfig, entryFilePaths, modes) {
|
|
8
|
+
const { graphMode, batchMode, llmMode, cloudMode, securityMode, mcpMode, specMode, fixMode, autofixMode, lintMode, exportKern, enforce, jsonOutput, sarifOutput, maxDepth, batchSize, tsconfigPath, specFile, minCoverageArg, maxComplexityArg, maxErrorsArg, maxWarningsArg, } = modes;
|
|
9
|
+
let reports = [];
|
|
10
|
+
if (graphMode && entryFilePaths.length > 0) {
|
|
11
|
+
const graphOpts = { maxDepth, tsConfigFilePath: tsconfigPath ? resolve(tsconfigPath) : undefined };
|
|
12
|
+
const graph = resolveImportGraph(entryFilePaths, graphOpts);
|
|
13
|
+
console.log(` Graph: ${graph.totalFiles} files resolved (${graph.skipped} skipped, depth ${maxDepth})`);
|
|
14
|
+
reports = reviewGraph(entryFilePaths, reviewConfig, graphOpts);
|
|
15
|
+
}
|
|
16
|
+
else if (batchMode && entryFilePaths.length > batchSize) {
|
|
17
|
+
const totalBatches = Math.ceil(entryFilePaths.length / batchSize);
|
|
18
|
+
for (let i = 0; i < entryFilePaths.length; i += batchSize) {
|
|
19
|
+
const batch = entryFilePaths.slice(i, i + batchSize);
|
|
20
|
+
const batchNum = Math.floor(i / batchSize) + 1;
|
|
21
|
+
for (const f of batch) {
|
|
22
|
+
try {
|
|
23
|
+
reports.push(reviewFile(f, reviewConfig));
|
|
24
|
+
}
|
|
25
|
+
catch (e) {
|
|
26
|
+
console.error(` Review error in ${f}: ${e.message}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const batchFindings = reports.slice(-batch.length).reduce((sum, r) => sum + r.findings.length, 0);
|
|
30
|
+
console.log(` Batch ${batchNum}/${totalBatches}: ${batch.length} files reviewed (${batchFindings} findings)`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
for (const f of entryFilePaths) {
|
|
35
|
+
try {
|
|
36
|
+
reports.push(reviewFile(f, reviewConfig));
|
|
37
|
+
}
|
|
38
|
+
catch (e) {
|
|
39
|
+
console.error(` Review error in ${f}: ${e.message}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (reports.length === 0) {
|
|
44
|
+
console.log(' No reviewable files found (.ts/.tsx/.py/.kern).');
|
|
45
|
+
return { reports, exitCode: 0 };
|
|
46
|
+
}
|
|
47
|
+
// MCP security review
|
|
48
|
+
try {
|
|
49
|
+
const { reviewIfMCP, reviewMCPSource } = await import('@kernlang/review-mcp');
|
|
50
|
+
let mcpFileCount = 0;
|
|
51
|
+
for (const report of reports) {
|
|
52
|
+
const source = readFileSync(report.filePath, 'utf-8');
|
|
53
|
+
const mcpFindings = mcpMode ? reviewMCPSource(source, report.filePath) : reviewIfMCP(source, report.filePath);
|
|
54
|
+
if (mcpFindings && mcpFindings.length > 0) {
|
|
55
|
+
report.findings.push(...mcpFindings);
|
|
56
|
+
mcpFileCount++;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (mcpFileCount > 0 && !jsonOutput && !sarifOutput) {
|
|
60
|
+
console.log(` MCP security: ${mcpFileCount} server file(s) scanned`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
if (mcpMode) {
|
|
65
|
+
console.error(' @kernlang/review-mcp not installed. Run: pnpm add @kernlang/review-mcp');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Auto LLM review when API key is set
|
|
69
|
+
if (!llmMode && isLLMAvailable()) {
|
|
70
|
+
const llmInputs = reports.map((report) => {
|
|
71
|
+
let source;
|
|
72
|
+
try {
|
|
73
|
+
source = readFileSync(report.filePath, 'utf-8');
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
/* fallback */
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
filePath: report.filePath,
|
|
80
|
+
inferred: report.inferred,
|
|
81
|
+
templateMatches: report.templateMatches,
|
|
82
|
+
taintResults: analyzeTaint(report.inferred, report.filePath),
|
|
83
|
+
source,
|
|
84
|
+
staticFindings: report.findings,
|
|
85
|
+
target: reviewConfig.target,
|
|
86
|
+
};
|
|
87
|
+
});
|
|
88
|
+
try {
|
|
89
|
+
const llmFindings = await runLLMReview(llmInputs);
|
|
90
|
+
if (llmFindings.length > 0) {
|
|
91
|
+
if (!jsonOutput && !sarifOutput) {
|
|
92
|
+
console.log(` LLM review (auto): ${llmFindings.length} finding(s) from AI`);
|
|
93
|
+
}
|
|
94
|
+
for (const f of llmFindings) {
|
|
95
|
+
const report = reports.find((r) => r.filePath === f.primarySpan.file);
|
|
96
|
+
if (report)
|
|
97
|
+
report.findings.push(f);
|
|
98
|
+
else if (reports.length > 0)
|
|
99
|
+
reports[0].findings.push(f);
|
|
100
|
+
}
|
|
101
|
+
for (const report of reports) {
|
|
102
|
+
report.findings = dedup(report.findings);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// Auto LLM review failed silently
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (exportKern) {
|
|
111
|
+
for (const report of reports) {
|
|
112
|
+
console.log(`\n// ── ${report.filePath} ──`);
|
|
113
|
+
console.log(exportKernIR(report.inferred, report.templateMatches));
|
|
114
|
+
}
|
|
115
|
+
return { reports, exitCode: 0 };
|
|
116
|
+
}
|
|
117
|
+
// Spec mode
|
|
118
|
+
if (specMode && specFile) {
|
|
119
|
+
const kernFilePath = resolve(specFile);
|
|
120
|
+
if (!existsSync(kernFilePath)) {
|
|
121
|
+
console.error(` .kern spec file not found: ${specFile}`);
|
|
122
|
+
return { reports, exitCode: 1 };
|
|
123
|
+
}
|
|
124
|
+
console.log(`\n KERN spec check: ${specFile} → ${reports.length} implementation files\n`);
|
|
125
|
+
let totalViolations = 0;
|
|
126
|
+
for (const report of reports) {
|
|
127
|
+
const result = checkSpecFiles(kernFilePath, report.filePath);
|
|
128
|
+
if (result.violations.length > 0) {
|
|
129
|
+
const findings = specViolationsToFindings(result);
|
|
130
|
+
totalViolations += findings.length;
|
|
131
|
+
report.findings.push(...findings);
|
|
132
|
+
report.findings = dedup(report.findings);
|
|
133
|
+
for (const v of result.violations) {
|
|
134
|
+
const icon = v.kind.includes('missing') || v.kind === 'spec-unimplemented' ? '✗' : '~';
|
|
135
|
+
const sev = v.kind === 'spec-auth-missing' || v.kind === 'spec-unimplemented'
|
|
136
|
+
? 'ERROR'
|
|
137
|
+
: v.kind === 'spec-undeclared'
|
|
138
|
+
? 'INFO'
|
|
139
|
+
: 'WARN';
|
|
140
|
+
console.log(` ${icon} [${sev}] ${v.kind}: ${v.detail}`);
|
|
141
|
+
if (v.suggestion)
|
|
142
|
+
console.log(` → ${v.suggestion}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (result.matched.length > 0) {
|
|
146
|
+
const satisfied = result.matched.length -
|
|
147
|
+
result.violations.filter((v) => v.kind !== 'spec-undeclared' && v.kind !== 'spec-unimplemented').length;
|
|
148
|
+
console.log(`\n Matched: ${result.matched.length} routes | Satisfied: ${satisfied} | Violations: ${totalViolations}`);
|
|
149
|
+
if (result.unmatchedSpecs.length > 0)
|
|
150
|
+
console.log(` Unimplemented: ${result.unmatchedSpecs.map((s) => s.routeKey).join(', ')}`);
|
|
151
|
+
if (result.unmatchedImpls.length > 0)
|
|
152
|
+
console.log(` Undeclared: ${result.unmatchedImpls.map((i) => i.routeKey).join(', ')}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (totalViolations === 0) {
|
|
156
|
+
console.log(' All spec contracts satisfied.');
|
|
157
|
+
}
|
|
158
|
+
console.log('');
|
|
159
|
+
}
|
|
160
|
+
// Security mode
|
|
161
|
+
if (securityMode) {
|
|
162
|
+
const SECURITY_RULES = new Set([
|
|
163
|
+
'xss-unsafe-html',
|
|
164
|
+
'hardcoded-secret',
|
|
165
|
+
'command-injection',
|
|
166
|
+
'no-eval',
|
|
167
|
+
'insecure-random',
|
|
168
|
+
'cors-wildcard',
|
|
169
|
+
'helmet-missing',
|
|
170
|
+
'open-redirect',
|
|
171
|
+
'jwt-weak-verification',
|
|
172
|
+
'cookie-hardening',
|
|
173
|
+
'csrf-detection',
|
|
174
|
+
'csp-strength',
|
|
175
|
+
'path-traversal',
|
|
176
|
+
'weak-password-hashing',
|
|
177
|
+
'regex-dos',
|
|
178
|
+
'missing-input-validation',
|
|
179
|
+
'prototype-pollution',
|
|
180
|
+
'information-exposure',
|
|
181
|
+
'prompt-injection',
|
|
182
|
+
'taint-command',
|
|
183
|
+
'taint-fs',
|
|
184
|
+
'taint-sql',
|
|
185
|
+
'taint-redirect',
|
|
186
|
+
'taint-eval',
|
|
187
|
+
'taint-insufficient-sanitizer',
|
|
188
|
+
'taint-crossfile-command',
|
|
189
|
+
'taint-crossfile-fs',
|
|
190
|
+
'taint-crossfile-sql',
|
|
191
|
+
'taint-crossfile-redirect',
|
|
192
|
+
'taint-crossfile-eval',
|
|
193
|
+
'spec-auth-missing',
|
|
194
|
+
'spec-validate-missing',
|
|
195
|
+
'spec-guard-missing',
|
|
196
|
+
'spec-middleware-missing',
|
|
197
|
+
'spec-unimplemented',
|
|
198
|
+
]);
|
|
199
|
+
console.log('\n KERN Security Report\n');
|
|
200
|
+
let totalSec = 0;
|
|
201
|
+
for (const report of reports) {
|
|
202
|
+
const secFindings = report.findings.filter((f) => SECURITY_RULES.has(f.ruleId));
|
|
203
|
+
if (secFindings.length === 0)
|
|
204
|
+
continue;
|
|
205
|
+
totalSec += secFindings.length;
|
|
206
|
+
const rel = relative(process.cwd(), report.filePath);
|
|
207
|
+
console.log(` ${rel}:`);
|
|
208
|
+
for (const f of secFindings) {
|
|
209
|
+
const icon = f.severity === 'error' ? '✗' : f.severity === 'warning' ? '~' : '-';
|
|
210
|
+
console.log(` ${icon} L${f.primarySpan.startLine}: [${f.ruleId}] ${f.message}`);
|
|
211
|
+
if (f.suggestion)
|
|
212
|
+
console.log(` → ${f.suggestion}`);
|
|
213
|
+
}
|
|
214
|
+
console.log('');
|
|
215
|
+
}
|
|
216
|
+
if (totalSec === 0) {
|
|
217
|
+
console.log(' No security issues found.');
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
const errors = reports
|
|
221
|
+
.flatMap((r) => r.findings)
|
|
222
|
+
.filter((f) => SECURITY_RULES.has(f.ruleId) && f.severity === 'error').length;
|
|
223
|
+
const warnings = reports
|
|
224
|
+
.flatMap((r) => r.findings)
|
|
225
|
+
.filter((f) => SECURITY_RULES.has(f.ruleId) && f.severity === 'warning').length;
|
|
226
|
+
console.log(` Total: ${totalSec} security findings (${errors} errors, ${warnings} warnings)`);
|
|
227
|
+
}
|
|
228
|
+
console.log(' Rules: OWASP Top 10, OWASP LLM Top 10, Taint Tracking, Spec Contracts');
|
|
229
|
+
console.log('');
|
|
230
|
+
return { reports, exitCode: 0 };
|
|
231
|
+
}
|
|
232
|
+
// Cloud mode
|
|
233
|
+
if (cloudMode) {
|
|
234
|
+
console.log('');
|
|
235
|
+
console.log(' KERN Pro — Cloud-powered AI review');
|
|
236
|
+
console.log('');
|
|
237
|
+
console.log(' Coming soon. Cloud review will provide:');
|
|
238
|
+
console.log(' • LLM-powered security analysis without an AI IDE');
|
|
239
|
+
console.log(' • Team dashboard with trend tracking');
|
|
240
|
+
console.log(' • Custom rule engine for enterprise');
|
|
241
|
+
console.log(' • CI/CD integration with quality gates');
|
|
242
|
+
console.log('');
|
|
243
|
+
console.log(' For now, use --llm with your AI assistant (Claude Code, Cursor, etc.)');
|
|
244
|
+
console.log(' The assistant reads the KERN IR output and performs the AI review.');
|
|
245
|
+
console.log('');
|
|
246
|
+
console.log(' → kern review src/ --llm');
|
|
247
|
+
console.log('');
|
|
248
|
+
console.log(' Join the waitlist: https://kernlang.dev/pro');
|
|
249
|
+
console.log('');
|
|
250
|
+
return { reports, exitCode: 0 };
|
|
251
|
+
}
|
|
252
|
+
// LLM mode
|
|
253
|
+
if (llmMode) {
|
|
254
|
+
const llmGraphContext = graphMode
|
|
255
|
+
? (() => {
|
|
256
|
+
const fileDistances = new Map();
|
|
257
|
+
for (const report of reports) {
|
|
258
|
+
const finding = report.findings[0];
|
|
259
|
+
const distance = finding?.distance ?? 0;
|
|
260
|
+
fileDistances.set(report.filePath, distance);
|
|
261
|
+
}
|
|
262
|
+
for (const ep of entryFilePaths) {
|
|
263
|
+
fileDistances.set(ep, 0);
|
|
264
|
+
}
|
|
265
|
+
return { fileDistances };
|
|
266
|
+
})()
|
|
267
|
+
: undefined;
|
|
268
|
+
if (isLLMAvailable()) {
|
|
269
|
+
console.log(' LLM review: calling API (deep mode — source + static findings)...');
|
|
270
|
+
const llmInputs = reports.map((report) => {
|
|
271
|
+
let source;
|
|
272
|
+
try {
|
|
273
|
+
source = readFileSync(report.filePath, 'utf-8');
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
/* fallback */
|
|
277
|
+
}
|
|
278
|
+
return {
|
|
279
|
+
filePath: report.filePath,
|
|
280
|
+
inferred: report.inferred,
|
|
281
|
+
templateMatches: report.templateMatches,
|
|
282
|
+
taintResults: analyzeTaint(report.inferred, report.filePath),
|
|
283
|
+
graphContext: llmGraphContext,
|
|
284
|
+
source,
|
|
285
|
+
staticFindings: report.findings,
|
|
286
|
+
target: reviewConfig.target,
|
|
287
|
+
};
|
|
288
|
+
});
|
|
289
|
+
try {
|
|
290
|
+
const llmFindings = await runLLMReview(llmInputs);
|
|
291
|
+
console.log(` LLM review: ${llmFindings.length} findings from AI`);
|
|
292
|
+
for (const f of llmFindings) {
|
|
293
|
+
const report = reports.find((r) => r.filePath === f.primarySpan.file);
|
|
294
|
+
if (report)
|
|
295
|
+
report.findings.push(f);
|
|
296
|
+
else if (reports.length > 0)
|
|
297
|
+
reports[0].findings.push(f);
|
|
298
|
+
}
|
|
299
|
+
for (const report of reports) {
|
|
300
|
+
report.findings = dedup(report.findings);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
catch (err) {
|
|
304
|
+
console.error(` LLM review failed: ${err.message}`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
// No API key — AI CLI tool IS the reviewer
|
|
309
|
+
for (const report of reports) {
|
|
310
|
+
const rel = relative(process.cwd(), report.filePath);
|
|
311
|
+
if (report.findings.length > 0) {
|
|
312
|
+
const errors = report.findings.filter((f) => f.severity === 'error');
|
|
313
|
+
const warnings = report.findings.filter((f) => f.severity === 'warning');
|
|
314
|
+
console.log(`<kern-findings path="${rel}">`);
|
|
315
|
+
for (const f of [...errors, ...warnings]) {
|
|
316
|
+
const conf = f.confidence !== undefined ? ` (${(f.confidence * 100).toFixed(0)}%)` : '';
|
|
317
|
+
console.log(` L${f.primarySpan.startLine} [${f.severity}] ${f.ruleId}: ${f.message}${conf}`);
|
|
318
|
+
if (f.suggestion)
|
|
319
|
+
console.log(` → ${f.suggestion}`);
|
|
320
|
+
}
|
|
321
|
+
console.log('</kern-findings>\n');
|
|
322
|
+
}
|
|
323
|
+
const taintResults = analyzeTaint(report.inferred, report.filePath);
|
|
324
|
+
if (taintResults.length > 0) {
|
|
325
|
+
console.log(`<kern-taint path="${rel}">`);
|
|
326
|
+
for (const t of taintResults) {
|
|
327
|
+
console.log(` fn ${t.fnName} (L${t.startLine}):`);
|
|
328
|
+
for (const p of t.paths) {
|
|
329
|
+
const status = p.sanitized ? `SANITIZED by ${p.sanitizer}` : 'UNSANITIZED';
|
|
330
|
+
const insufficient = p.insufficientSanitizer
|
|
331
|
+
? ` (${p.insufficientSanitizer} is NOT sufficient for ${p.sink.category})`
|
|
332
|
+
: '';
|
|
333
|
+
console.log(` ${p.source.origin} → ${p.sink.name}() [${p.sink.category}] ${status}${insufficient}`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
console.log('</kern-taint>\n');
|
|
337
|
+
}
|
|
338
|
+
if (report.crossFileTaint && report.crossFileTaint.length > 0) {
|
|
339
|
+
console.log(`<kern-taint-cross-file path="${rel}">`);
|
|
340
|
+
for (const t of report.crossFileTaint) {
|
|
341
|
+
const calleeRel = relative(process.cwd(), t.calleeFile);
|
|
342
|
+
console.log(` ${t.source.origin} in ${t.callerFn}() L${t.callerLine} → ${t.calleeFn}() in ${calleeRel} → ${t.sinkInCallee.name}() [${t.sinkInCallee.category}] UNSANITIZED`);
|
|
343
|
+
console.log(` Tainted args: ${t.taintedArgs.join(', ')}`);
|
|
344
|
+
}
|
|
345
|
+
console.log('</kern-taint-cross-file>\n');
|
|
346
|
+
}
|
|
347
|
+
if (report.obligations && report.obligations.length > 0) {
|
|
348
|
+
console.log(`<kern-obligations path="${rel}">`);
|
|
349
|
+
for (const o of report.obligations) {
|
|
350
|
+
console.log(` (${o.type}) ${o.functionName} L${o.line}: ${o.claim}`);
|
|
351
|
+
}
|
|
352
|
+
console.log('</kern-obligations>\n');
|
|
353
|
+
}
|
|
354
|
+
if (report.semanticChanges && report.semanticChanges.length > 0) {
|
|
355
|
+
console.log(`<kern-diff path="${rel}">`);
|
|
356
|
+
for (const c of report.semanticChanges) {
|
|
357
|
+
console.log(` [${c.severity}] ${c.type}: ${c.functionName} — ${c.description}`);
|
|
358
|
+
}
|
|
359
|
+
console.log('</kern-diff>\n');
|
|
360
|
+
}
|
|
361
|
+
console.log(`<kern-ir path="${rel}">`);
|
|
362
|
+
console.log(buildLLMPrompt(report.inferred, report.templateMatches, llmGraphContext));
|
|
363
|
+
console.log('</kern-ir>\n');
|
|
364
|
+
}
|
|
365
|
+
console.log(`── KERN Review Instructions ──\n${buildReviewInstructions({ target: 'assistant', hasInlineSource: false })}\n`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
// Diff-aware precision
|
|
369
|
+
const diffRanges = globalThis.__diffRanges;
|
|
370
|
+
if (diffRanges && diffRanges.size > 0) {
|
|
371
|
+
const DIFF_CONTEXT = 3;
|
|
372
|
+
let filtered = 0;
|
|
373
|
+
for (const report of reports) {
|
|
374
|
+
const relPath = relative(process.cwd(), report.filePath);
|
|
375
|
+
const ranges = diffRanges.get(relPath);
|
|
376
|
+
if (!ranges || ranges.length === 0)
|
|
377
|
+
continue;
|
|
378
|
+
const sorted = [...ranges].sort((a, b) => a[0] - b[0]);
|
|
379
|
+
const before = report.findings.length;
|
|
380
|
+
report.findings = report.findings.filter((f) => {
|
|
381
|
+
const line = f.primarySpan.startLine;
|
|
382
|
+
let lo = 0, hi = sorted.length - 1;
|
|
383
|
+
while (lo <= hi) {
|
|
384
|
+
const mid = (lo + hi) >> 1;
|
|
385
|
+
if (line < sorted[mid][0] - DIFF_CONTEXT)
|
|
386
|
+
hi = mid - 1;
|
|
387
|
+
else if (line > sorted[mid][1] + DIFF_CONTEXT)
|
|
388
|
+
lo = mid + 1;
|
|
389
|
+
else
|
|
390
|
+
return true;
|
|
391
|
+
}
|
|
392
|
+
return false;
|
|
393
|
+
});
|
|
394
|
+
filtered += before - report.findings.length;
|
|
395
|
+
}
|
|
396
|
+
if (filtered > 0 && !jsonOutput && !sarifOutput) {
|
|
397
|
+
console.log(` Diff filter: ${filtered} finding(s) outside changed lines suppressed`);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
// Fix mode
|
|
401
|
+
if (fixMode) {
|
|
402
|
+
let fixed = 0;
|
|
403
|
+
let verified = 0;
|
|
404
|
+
for (const report of reports) {
|
|
405
|
+
for (const t of report.templateMatches) {
|
|
406
|
+
if (!t.suggestedKern)
|
|
407
|
+
continue;
|
|
408
|
+
const kernFileName = report.filePath.replace(/\.tsx?$/, '.kern');
|
|
409
|
+
try {
|
|
410
|
+
writeFileSync(kernFileName, `${t.suggestedKern}\n`);
|
|
411
|
+
try {
|
|
412
|
+
parseAndSurface(readFileSync(kernFileName, 'utf-8'), kernFileName);
|
|
413
|
+
console.log(` ${report.filePath} → ${kernFileName} (verified)`);
|
|
414
|
+
verified++;
|
|
415
|
+
}
|
|
416
|
+
catch (parseErr) {
|
|
417
|
+
console.error(` ${kernFileName} written but parse failed: ${parseErr.message}`);
|
|
418
|
+
}
|
|
419
|
+
fixed++;
|
|
420
|
+
}
|
|
421
|
+
catch (err) {
|
|
422
|
+
console.error(` Failed to write ${kernFileName}: ${err.message}`);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
if (fixed === 0) {
|
|
427
|
+
console.log(' No template suggestions to fix — nothing to migrate.');
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
console.log(`\n ${fixed} .kern file(s) written, ${verified} verified.`);
|
|
431
|
+
}
|
|
432
|
+
return { reports, exitCode: 0 };
|
|
433
|
+
}
|
|
434
|
+
// Autofix mode
|
|
435
|
+
if (autofixMode) {
|
|
436
|
+
const fixesByFile = new Map();
|
|
437
|
+
for (const report of reports) {
|
|
438
|
+
for (const f of report.findings) {
|
|
439
|
+
if (!f.autofix)
|
|
440
|
+
continue;
|
|
441
|
+
const file = f.autofix.span.file || report.filePath;
|
|
442
|
+
if (!fixesByFile.has(file))
|
|
443
|
+
fixesByFile.set(file, []);
|
|
444
|
+
fixesByFile.get(file).push({ finding: f, fix: f.autofix });
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
if (fixesByFile.size === 0) {
|
|
448
|
+
console.log(' No autofixes available in findings.');
|
|
449
|
+
return { reports, exitCode: 0 };
|
|
450
|
+
}
|
|
451
|
+
let totalApplied = 0;
|
|
452
|
+
let totalSkipped = 0;
|
|
453
|
+
for (const [file, fixes] of fixesByFile) {
|
|
454
|
+
if (!existsSync(file)) {
|
|
455
|
+
console.error(` Skipping ${file} — file not found`);
|
|
456
|
+
totalSkipped += fixes.length;
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
fixes.sort((a, b) => {
|
|
460
|
+
const lineDiff = b.fix.span.startLine - a.fix.span.startLine;
|
|
461
|
+
if (lineDiff !== 0)
|
|
462
|
+
return lineDiff;
|
|
463
|
+
return b.fix.span.startCol - a.fix.span.startCol;
|
|
464
|
+
});
|
|
465
|
+
const appliedSpans = [];
|
|
466
|
+
function overlaps(sl, el) {
|
|
467
|
+
return appliedSpans.some((s) => sl <= s.el && el >= s.sl);
|
|
468
|
+
}
|
|
469
|
+
const lines = readFileSync(file, 'utf-8').split('\n');
|
|
470
|
+
let applied = 0;
|
|
471
|
+
for (const { finding, fix } of fixes) {
|
|
472
|
+
const { startLine, startCol, endLine, endCol } = fix.span;
|
|
473
|
+
const sl = startLine - 1;
|
|
474
|
+
const el = endLine - 1;
|
|
475
|
+
if (sl < 0 || el >= lines.length) {
|
|
476
|
+
console.error(` Skipping ${finding.ruleId}@${startLine}:${startCol} — span out of range`);
|
|
477
|
+
totalSkipped++;
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
if (overlaps(sl, el)) {
|
|
481
|
+
console.error(` Skipping ${finding.ruleId}@${startLine}:${startCol} — overlaps with a previously applied fix`);
|
|
482
|
+
totalSkipped++;
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
if (fix.type === 'replace') {
|
|
486
|
+
const before = lines[sl].slice(0, startCol - 1);
|
|
487
|
+
const after = lines[el].slice(endCol - 1);
|
|
488
|
+
const replacementLines = fix.replacement.split('\n');
|
|
489
|
+
replacementLines[0] = before + replacementLines[0];
|
|
490
|
+
replacementLines[replacementLines.length - 1] += after;
|
|
491
|
+
lines.splice(sl, el - sl + 1, ...replacementLines);
|
|
492
|
+
}
|
|
493
|
+
else if (fix.type === 'insert-before') {
|
|
494
|
+
lines.splice(sl, 0, fix.replacement);
|
|
495
|
+
}
|
|
496
|
+
else if (fix.type === 'insert-after') {
|
|
497
|
+
lines.splice(el + 1, 0, fix.replacement);
|
|
498
|
+
}
|
|
499
|
+
else if (fix.type === 'remove') {
|
|
500
|
+
lines.splice(sl, el - sl + 1);
|
|
501
|
+
}
|
|
502
|
+
else if (fix.type === 'wrap') {
|
|
503
|
+
const original = lines.slice(sl, el + 1).join('\n');
|
|
504
|
+
const wrapped = fix.replacement.replace('$0', original);
|
|
505
|
+
lines.splice(sl, el - sl + 1, ...wrapped.split('\n'));
|
|
506
|
+
}
|
|
507
|
+
appliedSpans.push({ sl, el });
|
|
508
|
+
applied++;
|
|
509
|
+
}
|
|
510
|
+
writeFileSync(file, lines.join('\n'));
|
|
511
|
+
console.log(` ${file}: ${applied} fix${applied === 1 ? '' : 'es'} applied`);
|
|
512
|
+
totalApplied += applied;
|
|
513
|
+
}
|
|
514
|
+
console.log(`\n ${totalApplied} autofix${totalApplied === 1 ? '' : 'es'} applied, ${totalSkipped} skipped.`);
|
|
515
|
+
return { reports, exitCode: 0 };
|
|
516
|
+
}
|
|
517
|
+
// Lint mode
|
|
518
|
+
if (lintMode) {
|
|
519
|
+
const filePaths = reports.map((r) => r.filePath).filter((f) => existsSync(f));
|
|
520
|
+
const eslintFindings = await runESLint(filePaths, process.cwd());
|
|
521
|
+
if (eslintFindings.length > 0) {
|
|
522
|
+
console.log(` ESLint: ${eslintFindings.length} findings`);
|
|
523
|
+
for (const report of reports) {
|
|
524
|
+
const fileFindings = eslintFindings.filter((f) => f.primarySpan.file === report.filePath);
|
|
525
|
+
const linked = linkToNodes(fileFindings, report.inferred);
|
|
526
|
+
report.findings = dedup([...report.findings, ...linked]);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
else {
|
|
530
|
+
console.log(' ESLint: no findings (or not installed)');
|
|
531
|
+
}
|
|
532
|
+
const tscFindings = runTSCDiagnosticsFromPaths(filePaths);
|
|
533
|
+
if (tscFindings.length > 0) {
|
|
534
|
+
console.log(` tsc: ${tscFindings.length} findings`);
|
|
535
|
+
for (const report of reports) {
|
|
536
|
+
const fileFindings = tscFindings.filter((f) => f.primarySpan.file === report.filePath);
|
|
537
|
+
const linked = linkToNodes(fileFindings, report.inferred);
|
|
538
|
+
report.findings = dedup([...report.findings, ...linked]);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
else {
|
|
542
|
+
console.log(' tsc: no findings');
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
// Output
|
|
546
|
+
if (jsonOutput) {
|
|
547
|
+
const enriched = reports.map((report) => {
|
|
548
|
+
const llmPrompt = buildLLMPrompt(report.inferred, report.templateMatches);
|
|
549
|
+
const kernIR = exportKernIR(report.inferred, report.templateMatches);
|
|
550
|
+
return { ...report, kernIR, llmPrompt };
|
|
551
|
+
});
|
|
552
|
+
console.log(JSON.stringify(enriched.length === 1 ? enriched[0] : enriched, null, 2));
|
|
553
|
+
}
|
|
554
|
+
else if (sarifOutput) {
|
|
555
|
+
console.log(formatSARIF(reports));
|
|
556
|
+
}
|
|
557
|
+
else {
|
|
558
|
+
for (const report of reports) {
|
|
559
|
+
console.log('');
|
|
560
|
+
console.log(formatReport(report, reviewConfig));
|
|
561
|
+
}
|
|
562
|
+
if (reports.length > 1) {
|
|
563
|
+
console.log('');
|
|
564
|
+
console.log(formatSummary(reports));
|
|
565
|
+
}
|
|
566
|
+
const hasThresholds = minCoverageArg !== undefined ||
|
|
567
|
+
maxComplexityArg !== undefined ||
|
|
568
|
+
maxErrorsArg !== undefined ||
|
|
569
|
+
maxWarningsArg !== undefined;
|
|
570
|
+
if (enforce || hasThresholds) {
|
|
571
|
+
console.log('');
|
|
572
|
+
let allPassed = true;
|
|
573
|
+
for (const report of reports) {
|
|
574
|
+
const result = checkEnforcement(report, reviewConfig);
|
|
575
|
+
if (!result.passed) {
|
|
576
|
+
allPassed = false;
|
|
577
|
+
console.log(` File: ${report.filePath}`);
|
|
578
|
+
console.log(formatEnforcement(result));
|
|
579
|
+
console.log('');
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
if (allPassed) {
|
|
583
|
+
console.log(` Enforcement: PASS (all files checked against thresholds)`);
|
|
584
|
+
}
|
|
585
|
+
else {
|
|
586
|
+
return { reports, exitCode: 1 };
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
return { reports, exitCode: 0 };
|
|
591
|
+
}
|
|
592
|
+
// ── Review command entry point ───────────────────────────────────────────
|
|
593
|
+
export async function runReview(args) {
|
|
594
|
+
const jsonOutput = hasFlag(args, '--json');
|
|
595
|
+
const sarifOutput = hasFlag(args, '--sarif', '--format=sarif');
|
|
596
|
+
const recursive = hasFlag(args, '--recursive', '-r');
|
|
597
|
+
const enforce = hasFlag(args, '--enforce');
|
|
598
|
+
const exportKern = hasFlag(args, '--export-kern');
|
|
599
|
+
const llmMode = hasFlag(args, '--llm');
|
|
600
|
+
const cloudMode = hasFlag(args, '--cloud');
|
|
601
|
+
const securityMode = hasFlag(args, '--security');
|
|
602
|
+
const mcpMode = hasFlag(args, '--mcp');
|
|
603
|
+
const specMode = hasFlag(args, '--spec');
|
|
604
|
+
const specFile = args.find((a) => a.endsWith('.kern') && a !== 'review');
|
|
605
|
+
const fixMode = hasFlag(args, '--fix');
|
|
606
|
+
const autofixMode = hasFlag(args, '--autofix');
|
|
607
|
+
const lintMode = hasFlag(args, '--lint');
|
|
608
|
+
const graphMode = hasFlag(args, '--graph') || recursive;
|
|
609
|
+
const batchMode = hasFlag(args, '--batch');
|
|
610
|
+
const maxDepth = Number(parseFlag(args, '--max-depth') ?? 3);
|
|
611
|
+
const batchSize = Number(parseFlag(args, '--batch-size') ?? 20);
|
|
612
|
+
const tsconfigPath = parseFlag(args, '--tsconfig');
|
|
613
|
+
const minCoverageArg = parseFlag(args, '--min-coverage');
|
|
614
|
+
const minCoverage = minCoverageArg ? Number(minCoverageArg) : undefined;
|
|
615
|
+
const maxComplexityArg = parseFlag(args, '--max-complexity');
|
|
616
|
+
const maxComplexity = maxComplexityArg ? Number(maxComplexityArg) : 15;
|
|
617
|
+
const maxErrorsArg = parseFlag(args, '--max-errors');
|
|
618
|
+
const maxErrors = maxErrorsArg ? Number(maxErrorsArg) : 0;
|
|
619
|
+
const maxWarningsArg = parseFlag(args, '--max-warnings');
|
|
620
|
+
const maxWarnings = maxWarningsArg ? Number(maxWarningsArg) : undefined;
|
|
621
|
+
const showConfidence = hasFlag(args, '--confidence');
|
|
622
|
+
const minConfidenceArg = parseFlag(args, '--min-confidence');
|
|
623
|
+
const minConfidence = minConfidenceArg ? Number(minConfidenceArg) : undefined;
|
|
624
|
+
const disableRuleArgs = args.filter((a) => a.startsWith('--disable-rule=')).map((a) => a.split('=')[1]);
|
|
625
|
+
const rulesDirs = [];
|
|
626
|
+
for (let i = 0; i < args.length; i++) {
|
|
627
|
+
if (args[i] === '--rules-dir' && args[i + 1] && !args[i + 1].startsWith('--')) {
|
|
628
|
+
rulesDirs.push(resolve(args[i + 1]));
|
|
629
|
+
i++;
|
|
630
|
+
}
|
|
631
|
+
else if (args[i].startsWith('--rules-dir=')) {
|
|
632
|
+
rulesDirs.push(resolve(args[i].split('=')[1]));
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
const strictArg = args.find((a) => a === '--strict' || a.startsWith('--strict='));
|
|
636
|
+
const strict = strictArg === '--strict' ? 'inline' : strictArg === '--strict=all' ? 'all' : false;
|
|
637
|
+
const strictParse = hasFlag(args, '--strict-parse');
|
|
638
|
+
const listRules = hasFlag(args, '--list-rules');
|
|
639
|
+
const diffBase = args.find((a) => a.startsWith('--diff'))
|
|
640
|
+
? parseFlag(args, '--diff') || args[args.indexOf('--diff') + 1] || 'origin/main'
|
|
641
|
+
: undefined;
|
|
642
|
+
// --list-rules
|
|
643
|
+
if (listRules) {
|
|
644
|
+
const reviewCfg = loadConfig();
|
|
645
|
+
const targetArg = parseFlag(args, '--target');
|
|
646
|
+
const target = targetArg || reviewCfg.target;
|
|
647
|
+
const rules = getRuleRegistry(target);
|
|
648
|
+
const layers = new Map();
|
|
649
|
+
for (const r of rules) {
|
|
650
|
+
if (!layers.has(r.layer))
|
|
651
|
+
layers.set(r.layer, []);
|
|
652
|
+
layers.get(r.layer).push(r);
|
|
653
|
+
}
|
|
654
|
+
console.log(`\n KERN Review Rules (target: ${target}) — ${rules.length} rules active\n`);
|
|
655
|
+
for (const [layer, layerRules] of layers) {
|
|
656
|
+
console.log(` [${layer}] (${layerRules.length} rules)`);
|
|
657
|
+
for (const r of layerRules) {
|
|
658
|
+
const sev = r.severity === 'error' ? 'ERR' : r.severity === 'warning' ? 'WRN' : 'INF';
|
|
659
|
+
console.log(` ${sev} ${r.id.padEnd(30)} ${r.description}`);
|
|
660
|
+
}
|
|
661
|
+
console.log();
|
|
662
|
+
}
|
|
663
|
+
process.exit(0);
|
|
664
|
+
}
|
|
665
|
+
// Diff mode
|
|
666
|
+
const reviewInputs = args.filter((a) => !a.startsWith('--') && a !== 'review');
|
|
667
|
+
let reviewInput = reviewInputs[0];
|
|
668
|
+
if (diffBase && !reviewInput) {
|
|
669
|
+
try {
|
|
670
|
+
const { execFileSync } = await import('child_process');
|
|
671
|
+
const sanitizedBase = diffBase.replace(/[^a-zA-Z0-9_./\-~]/g, '');
|
|
672
|
+
const diffFiles = execFileSync('git', ['diff', '--name-only', '--diff-filter=ACMR', sanitizedBase], {
|
|
673
|
+
encoding: 'utf-8',
|
|
674
|
+
})
|
|
675
|
+
.trim()
|
|
676
|
+
.split('\n')
|
|
677
|
+
.filter((f) => f.endsWith('.ts') || f.endsWith('.tsx'))
|
|
678
|
+
.filter((f) => !f.endsWith('.d.ts') && !f.endsWith('.test.ts'));
|
|
679
|
+
if (diffFiles.length === 0) {
|
|
680
|
+
console.log(` No changed .ts/.tsx files since ${diffBase}`);
|
|
681
|
+
process.exit(0);
|
|
682
|
+
}
|
|
683
|
+
console.log(` Reviewing ${diffFiles.length} changed files (diff from ${diffBase})\n`);
|
|
684
|
+
const diffRanges = new Map();
|
|
685
|
+
try {
|
|
686
|
+
const unifiedDiff = execFileSync('git', ['diff', '--unified=0', '--diff-filter=ACMR', sanitizedBase], {
|
|
687
|
+
encoding: 'utf-8',
|
|
688
|
+
env: { ...process.env, LC_ALL: 'C' },
|
|
689
|
+
});
|
|
690
|
+
let currentFile = '';
|
|
691
|
+
for (const line of unifiedDiff.split('\n')) {
|
|
692
|
+
if (line.startsWith('+++ b/')) {
|
|
693
|
+
currentFile = line.slice(6);
|
|
694
|
+
if (!diffRanges.has(currentFile))
|
|
695
|
+
diffRanges.set(currentFile, []);
|
|
696
|
+
}
|
|
697
|
+
if (line.startsWith('@@') && currentFile) {
|
|
698
|
+
const match = line.match(/\+(\d+)(?:,(\d+))?/);
|
|
699
|
+
if (match) {
|
|
700
|
+
const start = parseInt(match[1], 10);
|
|
701
|
+
const count = match[2] ? parseInt(match[2], 10) : 1;
|
|
702
|
+
if (count > 0) {
|
|
703
|
+
diffRanges.get(currentFile).push([start, start + count - 1]);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
catch {
|
|
710
|
+
// If unified diff parsing fails, proceed without line filtering
|
|
711
|
+
}
|
|
712
|
+
reviewInput = '__diff__';
|
|
713
|
+
globalThis.__diffFiles = diffFiles;
|
|
714
|
+
globalThis.__diffRanges = diffRanges;
|
|
715
|
+
}
|
|
716
|
+
catch (err) {
|
|
717
|
+
console.error(` git diff failed: ${err.message}`);
|
|
718
|
+
process.exit(1);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
if (!reviewInput) {
|
|
722
|
+
console.error('Usage: kern review <file.ts|dir> [--security] [--mcp] [--llm] [--spec file.kern] [--cloud]');
|
|
723
|
+
console.error(' [--diff base] [--json] [--sarif] [--recursive] [--enforce] [--strict-parse] [--fix] [--autofix] [--rules-dir <dir>]');
|
|
724
|
+
process.exit(1);
|
|
725
|
+
}
|
|
726
|
+
if (reviewInput !== '__diff__') {
|
|
727
|
+
const reviewPath = resolve(reviewInput);
|
|
728
|
+
const stat = existsSync(reviewPath) ? statSync(reviewPath) : null;
|
|
729
|
+
if (!stat) {
|
|
730
|
+
console.error(`Not found: ${reviewInput}`);
|
|
731
|
+
process.exit(1);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
const reviewCfg = loadConfig();
|
|
735
|
+
if (!VALID_TARGETS.includes(reviewCfg.target)) {
|
|
736
|
+
console.error(`Invalid target '${reviewCfg.target}' in config. Valid: ${VALID_TARGETS.join(', ')}`);
|
|
737
|
+
process.exit(1);
|
|
738
|
+
}
|
|
739
|
+
if (!jsonOutput && !sarifOutput) {
|
|
740
|
+
const configExists = existsSync(resolve(process.cwd(), 'kern.config.ts'));
|
|
741
|
+
if (!configExists) {
|
|
742
|
+
console.log(` Target: ${reviewCfg.target} (auto-detected from package.json)`);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
const cfgDisabledRules = reviewCfg.review.disabledRules ?? [];
|
|
746
|
+
const mergedDisabledRules = [...new Set([...cfgDisabledRules, ...disableRuleArgs])];
|
|
747
|
+
const reviewConfig = {
|
|
748
|
+
registeredTemplates: [],
|
|
749
|
+
minCoverage: minCoverage ?? 0,
|
|
750
|
+
enforceTemplates: enforce,
|
|
751
|
+
maxComplexity: maxComplexity ?? reviewCfg.review.maxComplexity,
|
|
752
|
+
maxErrors,
|
|
753
|
+
maxWarnings,
|
|
754
|
+
target: reviewCfg.target,
|
|
755
|
+
showConfidence: showConfidence || reviewCfg.review.showConfidence,
|
|
756
|
+
minConfidence: minConfidence ?? reviewCfg.review.minConfidence,
|
|
757
|
+
disabledRules: mergedDisabledRules.length > 0 ? mergedDisabledRules : undefined,
|
|
758
|
+
rulesDirs: rulesDirs.length > 0 ? rulesDirs : undefined,
|
|
759
|
+
strict,
|
|
760
|
+
strictParse,
|
|
761
|
+
};
|
|
762
|
+
// Load templates for review
|
|
763
|
+
if (reviewCfg.templates && reviewCfg.templates.length > 0) {
|
|
764
|
+
clearTemplates();
|
|
765
|
+
for (const templatePath of reviewCfg.templates) {
|
|
766
|
+
const resolvedTpl = resolve(process.cwd(), templatePath);
|
|
767
|
+
if (!existsSync(resolvedTpl))
|
|
768
|
+
continue;
|
|
769
|
+
const tplStat = statSync(resolvedTpl);
|
|
770
|
+
const tplFiles = [];
|
|
771
|
+
if (tplStat.isDirectory()) {
|
|
772
|
+
for (const entry of readdirSync(resolvedTpl)) {
|
|
773
|
+
if (entry.endsWith('.kern'))
|
|
774
|
+
tplFiles.push(resolve(resolvedTpl, entry));
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
else if (resolvedTpl.endsWith('.kern')) {
|
|
778
|
+
tplFiles.push(resolvedTpl);
|
|
779
|
+
}
|
|
780
|
+
for (const file of tplFiles) {
|
|
781
|
+
try {
|
|
782
|
+
const source = readFileSync(file, 'utf-8');
|
|
783
|
+
const ast = parseAndSurface(source, file);
|
|
784
|
+
const nodes = ast.type === 'template' ? [ast] : (ast.children || []).filter((n) => n.type === 'template');
|
|
785
|
+
for (const node of nodes) {
|
|
786
|
+
const tplName = node.props?.name;
|
|
787
|
+
if (tplName)
|
|
788
|
+
reviewConfig.registeredTemplates.push(tplName);
|
|
789
|
+
registerTemplate(node, file);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
catch (e) {
|
|
793
|
+
console.error(` Warning: Failed to parse template ${basename(file)}: ${e.message}`);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
if (reviewConfig.registeredTemplates.length > 0) {
|
|
798
|
+
console.log(` Templates loaded: ${reviewConfig.registeredTemplates.join(', ')}`);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
// Collect entry file paths
|
|
802
|
+
let entryFilePaths = [];
|
|
803
|
+
if (reviewInput === '__diff__') {
|
|
804
|
+
const diffFiles = globalThis.__diffFiles;
|
|
805
|
+
entryFilePaths = diffFiles.map((f) => resolve(f)).filter((f) => existsSync(f));
|
|
806
|
+
}
|
|
807
|
+
else {
|
|
808
|
+
const paths = reviewInputs.length > 0 ? reviewInputs : [reviewInput];
|
|
809
|
+
for (const p of paths) {
|
|
810
|
+
const rPath = resolve(p);
|
|
811
|
+
if (!existsSync(rPath))
|
|
812
|
+
continue;
|
|
813
|
+
const rStat = statSync(rPath);
|
|
814
|
+
if (rStat.isDirectory()) {
|
|
815
|
+
entryFilePaths.push(...collectTsFilesFlat(rPath, recursive));
|
|
816
|
+
}
|
|
817
|
+
else {
|
|
818
|
+
entryFilePaths.push(rPath);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
const modes = {
|
|
823
|
+
graphMode,
|
|
824
|
+
batchMode,
|
|
825
|
+
llmMode,
|
|
826
|
+
cloudMode,
|
|
827
|
+
securityMode,
|
|
828
|
+
mcpMode,
|
|
829
|
+
specMode,
|
|
830
|
+
fixMode,
|
|
831
|
+
autofixMode,
|
|
832
|
+
lintMode,
|
|
833
|
+
exportKern,
|
|
834
|
+
enforce,
|
|
835
|
+
jsonOutput,
|
|
836
|
+
sarifOutput,
|
|
837
|
+
strictParse,
|
|
838
|
+
maxDepth,
|
|
839
|
+
batchSize,
|
|
840
|
+
tsconfigPath,
|
|
841
|
+
specFile,
|
|
842
|
+
minCoverageArg,
|
|
843
|
+
maxComplexityArg,
|
|
844
|
+
maxErrorsArg,
|
|
845
|
+
maxWarningsArg,
|
|
846
|
+
showConfidence,
|
|
847
|
+
};
|
|
848
|
+
const noCache = hasFlag(args, '--no-cache');
|
|
849
|
+
if (noCache) {
|
|
850
|
+
clearReviewCache();
|
|
851
|
+
reviewConfig.noCache = true;
|
|
852
|
+
}
|
|
853
|
+
const watchMode = hasFlag(args, '--watch', '-w');
|
|
854
|
+
if (watchMode) {
|
|
855
|
+
const chokidar = await import('chokidar');
|
|
856
|
+
console.log(`\n KERN review — watching ${entryFilePaths.length} entry points`);
|
|
857
|
+
let debounceTimer = null;
|
|
858
|
+
const run = async (paths) => {
|
|
859
|
+
console.clear();
|
|
860
|
+
console.log(`\n KERN review — watching (${paths.length} file${paths.length === 1 ? '' : 's'})\n`);
|
|
861
|
+
const watchModes = { ...modes, llmMode: false, enforce: false };
|
|
862
|
+
await runReviewPipeline(reviewConfig, paths, watchModes);
|
|
863
|
+
console.log('\n Watching for changes...');
|
|
864
|
+
};
|
|
865
|
+
const watcher = chokidar.watch(entryFilePaths, {
|
|
866
|
+
persistent: true,
|
|
867
|
+
awaitWriteFinish: { stabilityThreshold: 300 },
|
|
868
|
+
});
|
|
869
|
+
watcher.on('change', (path) => {
|
|
870
|
+
if (debounceTimer)
|
|
871
|
+
clearTimeout(debounceTimer);
|
|
872
|
+
debounceTimer = setTimeout(() => run([path]), 300);
|
|
873
|
+
});
|
|
874
|
+
await run(entryFilePaths);
|
|
875
|
+
}
|
|
876
|
+
else {
|
|
877
|
+
const result = await runReviewPipeline(reviewConfig, entryFilePaths, modes);
|
|
878
|
+
process.exit(result.exitCode);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
//# sourceMappingURL=review.js.map
|