@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.
Files changed (48) hide show
  1. package/dist/cli.js +32 -2440
  2. package/dist/cli.js.map +1 -1
  3. package/dist/commands/compile.d.ts +1 -0
  4. package/dist/commands/compile.js +193 -0
  5. package/dist/commands/compile.js.map +1 -0
  6. package/dist/commands/confidence.d.ts +1 -0
  7. package/dist/commands/confidence.js +105 -0
  8. package/dist/commands/confidence.js.map +1 -0
  9. package/dist/commands/dev.d.ts +1 -0
  10. package/dist/commands/dev.js +123 -0
  11. package/dist/commands/dev.js.map +1 -0
  12. package/dist/commands/evolve/backfill.d.ts +1 -0
  13. package/dist/commands/evolve/backfill.js +88 -0
  14. package/dist/commands/evolve/backfill.js.map +1 -0
  15. package/dist/commands/evolve/discover.d.ts +1 -0
  16. package/dist/commands/evolve/discover.js +158 -0
  17. package/dist/commands/evolve/discover.js.map +1 -0
  18. package/dist/commands/evolve/index.d.ts +1 -0
  19. package/dist/commands/evolve/index.js +40 -0
  20. package/dist/commands/evolve/index.js.map +1 -0
  21. package/dist/commands/evolve/lifecycle.d.ts +8 -0
  22. package/dist/commands/evolve/lifecycle.js +190 -0
  23. package/dist/commands/evolve/lifecycle.js.map +1 -0
  24. package/dist/commands/evolve/main.d.ts +1 -0
  25. package/dist/commands/evolve/main.js +76 -0
  26. package/dist/commands/evolve/main.js.map +1 -0
  27. package/dist/commands/evolve/review-v4.d.ts +1 -0
  28. package/dist/commands/evolve/review-v4.js +181 -0
  29. package/dist/commands/evolve/review-v4.js.map +1 -0
  30. package/dist/commands/evolve/review.d.ts +1 -0
  31. package/dist/commands/evolve/review.js +63 -0
  32. package/dist/commands/evolve/review.js.map +1 -0
  33. package/dist/commands/review.d.ts +1 -0
  34. package/dist/commands/review.js +881 -0
  35. package/dist/commands/review.js.map +1 -0
  36. package/dist/commands/scan.d.ts +2 -0
  37. package/dist/commands/scan.js +89 -0
  38. package/dist/commands/scan.js.map +1 -0
  39. package/dist/commands/transpile.d.ts +2 -0
  40. package/dist/commands/transpile.js +286 -0
  41. package/dist/commands/transpile.js.map +1 -0
  42. package/dist/shared.d.ts +20 -0
  43. package/dist/shared.js +270 -0
  44. package/dist/shared.js.map +1 -0
  45. package/dist/transpiler-cli.d.ts +1 -1
  46. package/dist/transpiler-cli.js +10 -9
  47. package/dist/transpiler-cli.js.map +1 -1
  48. 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