@kernlang/cli 3.1.9 → 3.3.4
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 +28 -16
- package/dist/cli.js.map +1 -1
- package/dist/commands/apply.d.ts +14 -0
- package/dist/commands/apply.js +167 -0
- package/dist/commands/apply.js.map +1 -0
- package/dist/commands/compile.js +59 -11
- package/dist/commands/compile.js.map +1 -1
- package/dist/commands/gaps.d.ts +1 -0
- package/dist/commands/gaps.js +178 -0
- package/dist/commands/gaps.js.map +1 -0
- package/dist/commands/migrate-class-body.d.ts +50 -0
- package/dist/commands/migrate-class-body.js +453 -0
- package/dist/commands/migrate-class-body.js.map +1 -0
- package/dist/commands/migrate.d.ts +80 -0
- package/dist/commands/migrate.js +586 -0
- package/dist/commands/migrate.js.map +1 -0
- package/dist/commands/review.d.ts +9 -0
- package/dist/commands/review.js +290 -26
- package/dist/commands/review.js.map +1 -1
- package/dist/commands/transpile.js +12 -5
- package/dist/commands/transpile.js.map +1 -1
- package/dist/remote-repo.d.ts +21 -0
- package/dist/remote-repo.js +167 -0
- package/dist/remote-repo.js.map +1 -0
- package/dist/review-baseline.d.ts +27 -0
- package/dist/review-baseline.js +120 -0
- package/dist/review-baseline.js.map +1 -0
- package/dist/shared.d.ts +23 -2
- package/dist/shared.js +213 -39
- package/dist/shared.js.map +1 -1
- package/package.json +14 -13
package/dist/commands/review.js
CHANGED
|
@@ -1,14 +1,54 @@
|
|
|
1
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 {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
2
|
+
import { analyzeTaint, buildLLMPrompt, buildReviewInstructions, checkEnforcement, checkSpecFiles, clearReviewCache, dedup, exportKernIR, formatEnforcement, formatReport, formatSARIF, formatSARIFWithMetadata, formatSummary, getRuleRegistry, isLLMAvailable, linkToNodes, ReviewHealthBuilder, resolveImportGraph, reviewFile, reviewGraph, runESLint, runLLMReview, runTSCDiagnosticsFromPaths, specViolationsToFindings, } from '@kernlang/review';
|
|
3
|
+
import { execFileSync } from 'child_process';
|
|
4
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'fs';
|
|
5
|
+
import { basename, dirname, relative, resolve } from 'path';
|
|
6
|
+
import { withOptionalRemoteRepo } from '../remote-repo.js';
|
|
7
|
+
import { compareReportsToBaseline, createReviewBaseline, filterReportsToNewFindings, getReviewBaselineKeyForFinding, parseReviewBaseline, } from '../review-baseline.js';
|
|
8
|
+
import { collectTsFilesFlat, hasFlag, loadConfig, parseAndSurface, parseFlag, parseFlagOrNext } from '../shared.js';
|
|
9
|
+
/**
|
|
10
|
+
* Pick a safe default diff base for bare `kern review` inside a git repo.
|
|
11
|
+
* Tries `origin/main`, then `origin/master`, then `HEAD~1`, returning the
|
|
12
|
+
* first ref that `git rev-parse --verify` accepts. Returns undefined when
|
|
13
|
+
* not in a git repo or no suitable ref exists (e.g. single-commit repo).
|
|
14
|
+
*
|
|
15
|
+
* Exported for testability.
|
|
16
|
+
*/
|
|
17
|
+
export function detectAutoDiffBase(cwd = process.cwd()) {
|
|
18
|
+
const verify = (ref) => {
|
|
19
|
+
try {
|
|
20
|
+
execFileSync('git', ['rev-parse', '--verify', '--quiet', ref], {
|
|
21
|
+
cwd,
|
|
22
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
23
|
+
});
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
// Require this to actually be a git repo before trying refs.
|
|
31
|
+
try {
|
|
32
|
+
execFileSync('git', ['rev-parse', '--is-inside-work-tree'], {
|
|
33
|
+
cwd,
|
|
34
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
for (const candidate of ['origin/main', 'origin/master', 'HEAD~1']) {
|
|
41
|
+
if (verify(candidate))
|
|
42
|
+
return candidate;
|
|
43
|
+
}
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
6
46
|
// ── Review pipeline ──────────────────────────────────────────────────────
|
|
7
47
|
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;
|
|
48
|
+
const { graphMode, batchMode, llmMode, cloudMode, securityMode, mcpMode, specMode, fixMode, autofixMode, lintMode, skipGenerated, exportKern, enforce, jsonOutput, sarifOutput, maxDepth, batchSize, tsconfigPath, specFile, minCoverageArg, maxComplexityArg, maxErrorsArg, maxWarningsArg, baseline, writeBaselinePath, newOnly, } = modes;
|
|
9
49
|
let reports = [];
|
|
10
50
|
if (graphMode && entryFilePaths.length > 0) {
|
|
11
|
-
const graphOpts = { maxDepth, tsConfigFilePath: tsconfigPath
|
|
51
|
+
const graphOpts = { maxDepth, tsConfigFilePath: tsconfigPath };
|
|
12
52
|
const graph = resolveImportGraph(entryFilePaths, graphOpts);
|
|
13
53
|
console.log(` Graph: ${graph.totalFiles} files resolved (${graph.skipped} skipped, depth ${maxDepth})`);
|
|
14
54
|
reports = reviewGraph(entryFilePaths, reviewConfig, graphOpts);
|
|
@@ -40,6 +80,14 @@ async function runReviewPipeline(reviewConfig, entryFilePaths, modes) {
|
|
|
40
80
|
}
|
|
41
81
|
}
|
|
42
82
|
}
|
|
83
|
+
if (skipGenerated) {
|
|
84
|
+
const before = reports.length;
|
|
85
|
+
reports = reports.filter((r) => !r.generated);
|
|
86
|
+
const dropped = before - reports.length;
|
|
87
|
+
if (dropped > 0 && !jsonOutput && !sarifOutput) {
|
|
88
|
+
console.log(` Skipped ${dropped} generated file(s). Use --include-generated to review them.`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
43
91
|
if (reports.length === 0) {
|
|
44
92
|
console.log(' No reviewable files found (.ts/.tsx/.py/.kern).');
|
|
45
93
|
return { reports, exitCode: 0 };
|
|
@@ -306,7 +354,13 @@ async function runReviewPipeline(reviewConfig, entryFilePaths, modes) {
|
|
|
306
354
|
}
|
|
307
355
|
}
|
|
308
356
|
else {
|
|
309
|
-
// No API key —
|
|
357
|
+
// No API key — emit machine-readable context for an upstream AI CLI
|
|
358
|
+
// (claude/codex/gemini) to consume as the reviewer. Without a banner
|
|
359
|
+
// this looks like "--llm did nothing" to someone running it standalone.
|
|
360
|
+
console.log(' LLM review: KERN_LLM_API_KEY not set — emitting LLM-prompt context.');
|
|
361
|
+
console.log(' Pipe to an AI CLI: kern review --llm <file> | claude');
|
|
362
|
+
console.log(' Or set an API key: export KERN_LLM_API_KEY=<key>');
|
|
363
|
+
console.log('');
|
|
310
364
|
for (const report of reports) {
|
|
311
365
|
const rel = relative(process.cwd(), report.filePath);
|
|
312
366
|
if (report.findings.length > 0) {
|
|
@@ -518,7 +572,10 @@ async function runReviewPipeline(reviewConfig, entryFilePaths, modes) {
|
|
|
518
572
|
// Lint mode
|
|
519
573
|
if (lintMode) {
|
|
520
574
|
const filePaths = reports.map((r) => r.filePath).filter((f) => existsSync(f));
|
|
521
|
-
|
|
575
|
+
// Collect lint-phase health across runESLint + runTSCDiagnosticsFromPaths; merge onto every
|
|
576
|
+
// report at the end so "ESLint not installed" shows up in the review header, not just console.
|
|
577
|
+
const lintHealth = new ReviewHealthBuilder();
|
|
578
|
+
const eslintFindings = await runESLint(filePaths, process.cwd(), lintHealth);
|
|
522
579
|
if (eslintFindings.length > 0) {
|
|
523
580
|
console.log(` ESLint: ${eslintFindings.length} findings`);
|
|
524
581
|
for (const report of reports) {
|
|
@@ -530,7 +587,7 @@ async function runReviewPipeline(reviewConfig, entryFilePaths, modes) {
|
|
|
530
587
|
else {
|
|
531
588
|
console.log(' ESLint: no findings (or not installed)');
|
|
532
589
|
}
|
|
533
|
-
const tscFindings = runTSCDiagnosticsFromPaths(filePaths);
|
|
590
|
+
const tscFindings = runTSCDiagnosticsFromPaths(filePaths, lintHealth);
|
|
534
591
|
if (tscFindings.length > 0) {
|
|
535
592
|
console.log(` tsc: ${tscFindings.length} findings`);
|
|
536
593
|
for (const report of reports) {
|
|
@@ -542,10 +599,43 @@ async function runReviewPipeline(reviewConfig, entryFilePaths, modes) {
|
|
|
542
599
|
else {
|
|
543
600
|
console.log(' tsc: no findings');
|
|
544
601
|
}
|
|
602
|
+
// Fold lint-phase health into each report's existing health (builder dedupes by key so merging
|
|
603
|
+
// a skipped-ESLint note on a report that already has an fs-project fallback keeps both).
|
|
604
|
+
const lintHealthBuilt = lintHealth.build();
|
|
605
|
+
if (lintHealthBuilt) {
|
|
606
|
+
for (const report of reports) {
|
|
607
|
+
const merged = new ReviewHealthBuilder();
|
|
608
|
+
for (const e of report.health?.entries ?? [])
|
|
609
|
+
merged.note(e);
|
|
610
|
+
for (const e of lintHealthBuilt.entries)
|
|
611
|
+
merged.note(e);
|
|
612
|
+
report.health = merged.build();
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
let baselineComparison;
|
|
617
|
+
let reportsForOutput = reports;
|
|
618
|
+
let reportsForEnforcement = reports;
|
|
619
|
+
if (baseline) {
|
|
620
|
+
baselineComparison = compareReportsToBaseline(reports, baseline);
|
|
621
|
+
reportsForEnforcement = filterReportsToNewFindings(reports, baselineComparison);
|
|
622
|
+
if (newOnly) {
|
|
623
|
+
reportsForOutput = reportsForEnforcement;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
if (writeBaselinePath) {
|
|
627
|
+
const baselineDir = dirname(writeBaselinePath);
|
|
628
|
+
if (baselineDir && baselineDir !== '.') {
|
|
629
|
+
mkdirSync(baselineDir, { recursive: true });
|
|
630
|
+
}
|
|
631
|
+
writeFileSync(writeBaselinePath, `${JSON.stringify(createReviewBaseline(reports), null, 2)}\n`);
|
|
632
|
+
if (!jsonOutput && !sarifOutput) {
|
|
633
|
+
console.log(` Baseline written: ${writeBaselinePath}`);
|
|
634
|
+
}
|
|
545
635
|
}
|
|
546
636
|
// Output
|
|
547
637
|
if (jsonOutput) {
|
|
548
|
-
const enriched =
|
|
638
|
+
const enriched = reportsForOutput.map((report) => {
|
|
549
639
|
const llmPrompt = buildLLMPrompt(report.inferred, report.templateMatches);
|
|
550
640
|
const kernIR = exportKernIR(report.inferred, report.templateMatches);
|
|
551
641
|
return { ...report, kernIR, llmPrompt };
|
|
@@ -553,16 +643,40 @@ async function runReviewPipeline(reviewConfig, entryFilePaths, modes) {
|
|
|
553
643
|
console.log(JSON.stringify(enriched.length === 1 ? enriched[0] : enriched, null, 2));
|
|
554
644
|
}
|
|
555
645
|
else if (sarifOutput) {
|
|
556
|
-
|
|
646
|
+
if (baselineComparison) {
|
|
647
|
+
console.log(formatSARIFWithMetadata(reportsForOutput, {
|
|
648
|
+
getBaselineStatus: (report, finding) => {
|
|
649
|
+
const key = getReviewBaselineKeyForFinding(report.filePath, finding);
|
|
650
|
+
if (baselineComparison.knownKeys.has(key))
|
|
651
|
+
return 'existing';
|
|
652
|
+
if (baselineComparison.newKeys.has(key))
|
|
653
|
+
return 'new';
|
|
654
|
+
return undefined;
|
|
655
|
+
},
|
|
656
|
+
}));
|
|
657
|
+
}
|
|
658
|
+
else if (reportsForOutput.some((report) => (report.suppressedFindings?.length ?? 0) > 0)) {
|
|
659
|
+
console.log(formatSARIFWithMetadata(reportsForOutput));
|
|
660
|
+
}
|
|
661
|
+
else {
|
|
662
|
+
console.log(formatSARIF(reportsForOutput));
|
|
663
|
+
}
|
|
557
664
|
}
|
|
558
665
|
else {
|
|
559
|
-
for (const report of
|
|
666
|
+
for (const report of reportsForOutput) {
|
|
560
667
|
console.log('');
|
|
561
668
|
console.log(formatReport(report, reviewConfig));
|
|
562
669
|
}
|
|
563
|
-
if (
|
|
670
|
+
if (reportsForOutput.length > 1) {
|
|
671
|
+
console.log('');
|
|
672
|
+
console.log(formatSummary(reportsForOutput));
|
|
673
|
+
}
|
|
674
|
+
if (baselineComparison) {
|
|
564
675
|
console.log('');
|
|
565
|
-
console.log(
|
|
676
|
+
console.log(` Baseline: ${baselineComparison.knownCount} existing, ${baselineComparison.newCount} new, ${baselineComparison.resolvedCount} resolved`);
|
|
677
|
+
if (newOnly) {
|
|
678
|
+
console.log(' Output: showing only new findings compared to baseline');
|
|
679
|
+
}
|
|
566
680
|
}
|
|
567
681
|
const hasThresholds = minCoverageArg !== undefined ||
|
|
568
682
|
maxComplexityArg !== undefined ||
|
|
@@ -571,7 +685,7 @@ async function runReviewPipeline(reviewConfig, entryFilePaths, modes) {
|
|
|
571
685
|
if (enforce || hasThresholds) {
|
|
572
686
|
console.log('');
|
|
573
687
|
let allPassed = true;
|
|
574
|
-
for (const report of
|
|
688
|
+
for (const report of reportsForEnforcement) {
|
|
575
689
|
const result = checkEnforcement(report, reviewConfig);
|
|
576
690
|
if (!result.passed) {
|
|
577
691
|
allPassed = false;
|
|
@@ -581,7 +695,8 @@ async function runReviewPipeline(reviewConfig, entryFilePaths, modes) {
|
|
|
581
695
|
}
|
|
582
696
|
}
|
|
583
697
|
if (allPassed) {
|
|
584
|
-
|
|
698
|
+
const suffix = baselineComparison ? ' on new findings vs baseline' : '';
|
|
699
|
+
console.log(` Enforcement: PASS (all files checked against thresholds${suffix})`);
|
|
585
700
|
}
|
|
586
701
|
else {
|
|
587
702
|
return { reports, exitCode: 1 };
|
|
@@ -591,7 +706,7 @@ async function runReviewPipeline(reviewConfig, entryFilePaths, modes) {
|
|
|
591
706
|
return { reports, exitCode: 0 };
|
|
592
707
|
}
|
|
593
708
|
// ── Review command entry point ───────────────────────────────────────────
|
|
594
|
-
|
|
709
|
+
async function runReviewLocal(args) {
|
|
595
710
|
const jsonOutput = hasFlag(args, '--json');
|
|
596
711
|
const sarifOutput = hasFlag(args, '--sarif', '--format=sarif');
|
|
597
712
|
const recursive = hasFlag(args, '--recursive', '-r');
|
|
@@ -606,15 +721,46 @@ export async function runReview(args) {
|
|
|
606
721
|
const fixMode = hasFlag(args, '--fix');
|
|
607
722
|
const autofixMode = hasFlag(args, '--autofix');
|
|
608
723
|
const lintMode = hasFlag(args, '--lint');
|
|
724
|
+
// Phase 6: generated files skipped by default — bugs in compiler output
|
|
725
|
+
// belong to the compiler, not the user, and inference re-fires every
|
|
726
|
+
// handler-size/handler-heavy rule on transpiled function bodies. Opt back
|
|
727
|
+
// in with --include-generated. --skip-generated stays accepted as a no-op
|
|
728
|
+
// so CI configs that pass it explicitly don't break.
|
|
729
|
+
const includeGenerated = hasFlag(args, '--include-generated');
|
|
730
|
+
const skipGenerated = !includeGenerated;
|
|
609
731
|
const graphMode = hasFlag(args, '--graph') || recursive;
|
|
610
732
|
const batchMode = hasFlag(args, '--batch');
|
|
611
733
|
const maxDepth = Number(parseFlag(args, '--max-depth') ?? 3);
|
|
612
734
|
const batchSize = Number(parseFlag(args, '--batch-size') ?? 20);
|
|
613
|
-
const
|
|
735
|
+
const explicitTsconfigPath = parseFlag(args, '--tsconfig');
|
|
736
|
+
// Only resolve when explicitly passed — per-file auto-discovery (via findTsConfig in the review engine)
|
|
737
|
+
// is usually right in monorepos, where the root tsconfig is a solution-only references file.
|
|
738
|
+
const tsconfigPath = explicitTsconfigPath ? resolve(explicitTsconfigPath) : undefined;
|
|
739
|
+
// Warn when the user explicitly points --tsconfig at a solution-only (references-only) file.
|
|
740
|
+
// ts-morph will load it but the resulting Project has no compilerOptions, which silently
|
|
741
|
+
// degrades review quality (no jsx, no paths, no strict). Tell the user before they waste a run.
|
|
742
|
+
if (tsconfigPath && existsSync(tsconfigPath)) {
|
|
743
|
+
try {
|
|
744
|
+
const raw = readFileSync(tsconfigPath, 'utf-8');
|
|
745
|
+
const stripped = raw.replace(/\/\*[\s\S]*?\*\/|\/\/.*$/gm, '');
|
|
746
|
+
const parsed = JSON.parse(stripped);
|
|
747
|
+
const hasCompilerOptions = parsed.compilerOptions && Object.keys(parsed.compilerOptions).length > 0;
|
|
748
|
+
const hasReferences = Array.isArray(parsed.references) && parsed.references.length > 0;
|
|
749
|
+
if (!hasCompilerOptions && hasReferences) {
|
|
750
|
+
console.warn(` Warning: --tsconfig ${tsconfigPath} is a solution-only file (references-only, no compilerOptions).`);
|
|
751
|
+
console.warn(` Review quality will be degraded (no jsx/paths/strict). Point --tsconfig at a per-package tsconfig instead, or omit --tsconfig to let kern-review discover the nearest one per file.`);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
catch {
|
|
755
|
+
// Bad JSON / unreadable — ts-morph will surface a clearer error during loading.
|
|
756
|
+
}
|
|
757
|
+
}
|
|
614
758
|
const minCoverageArg = parseFlag(args, '--min-coverage');
|
|
615
759
|
const minCoverage = minCoverageArg ? Number(minCoverageArg) : undefined;
|
|
616
760
|
const maxComplexityArg = parseFlag(args, '--max-complexity');
|
|
617
761
|
const maxComplexity = maxComplexityArg ? Number(maxComplexityArg) : 15;
|
|
762
|
+
const maxHandlerLinesArg = parseFlag(args, '--max-handler-lines');
|
|
763
|
+
const maxHandlerLines = maxHandlerLinesArg ? Number(maxHandlerLinesArg) : undefined;
|
|
618
764
|
const maxErrorsArg = parseFlag(args, '--max-errors');
|
|
619
765
|
const maxErrors = maxErrorsArg ? Number(maxErrorsArg) : 0;
|
|
620
766
|
const maxWarningsArg = parseFlag(args, '--max-warnings');
|
|
@@ -623,6 +769,13 @@ export async function runReview(args) {
|
|
|
623
769
|
const minConfidenceArg = parseFlag(args, '--min-confidence');
|
|
624
770
|
const minConfidence = minConfidenceArg ? Number(minConfidenceArg) : undefined;
|
|
625
771
|
const disableRuleArgs = args.filter((a) => a.startsWith('--disable-rule=')).map((a) => a.split('=')[1]);
|
|
772
|
+
const baselinePath = parseFlagOrNext(args, '--baseline');
|
|
773
|
+
const writeBaselinePath = parseFlagOrNext(args, '--write-baseline');
|
|
774
|
+
const newOnly = hasFlag(args, '--new-only');
|
|
775
|
+
if (newOnly && !baselinePath) {
|
|
776
|
+
console.error('--new-only requires --baseline=<file.json>');
|
|
777
|
+
process.exit(1);
|
|
778
|
+
}
|
|
626
779
|
const rulesDirs = [];
|
|
627
780
|
for (let i = 0; i < args.length; i++) {
|
|
628
781
|
if (args[i] === '--rules-dir' && args[i + 1] && !args[i + 1].startsWith('--')) {
|
|
@@ -637,9 +790,37 @@ export async function runReview(args) {
|
|
|
637
790
|
const strict = strictArg === '--strict' ? 'inline' : strictArg === '--strict=all' ? 'all' : false;
|
|
638
791
|
const strictParse = hasFlag(args, '--strict-parse');
|
|
639
792
|
const listRules = hasFlag(args, '--list-rules');
|
|
640
|
-
|
|
641
|
-
?
|
|
793
|
+
let diffBase = args.some((a) => a === '--diff' || a.startsWith('--diff'))
|
|
794
|
+
? parseFlagOrNext(args, '--diff') || 'origin/main'
|
|
642
795
|
: undefined;
|
|
796
|
+
const fullMode = hasFlag(args, '--full');
|
|
797
|
+
if (fullMode && diffBase) {
|
|
798
|
+
console.error(' --full and --diff are mutually exclusive.');
|
|
799
|
+
process.exit(1);
|
|
800
|
+
}
|
|
801
|
+
let baseline;
|
|
802
|
+
if (baselinePath) {
|
|
803
|
+
const resolvedBaselinePath = resolve(baselinePath);
|
|
804
|
+
if (!existsSync(resolvedBaselinePath)) {
|
|
805
|
+
console.error(`Baseline not found: ${baselinePath}`);
|
|
806
|
+
process.exit(1);
|
|
807
|
+
}
|
|
808
|
+
let rawBaseline;
|
|
809
|
+
try {
|
|
810
|
+
rawBaseline = readFileSync(resolvedBaselinePath, 'utf-8');
|
|
811
|
+
}
|
|
812
|
+
catch (err) {
|
|
813
|
+
console.error(`Failed to read baseline ${baselinePath}: ${err.message}`);
|
|
814
|
+
process.exit(1);
|
|
815
|
+
}
|
|
816
|
+
try {
|
|
817
|
+
baseline = parseReviewBaseline(rawBaseline);
|
|
818
|
+
}
|
|
819
|
+
catch (err) {
|
|
820
|
+
console.error(`Failed to parse baseline ${baselinePath}: ${err.message}`);
|
|
821
|
+
process.exit(1);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
643
824
|
// --list-rules
|
|
644
825
|
if (listRules) {
|
|
645
826
|
const reviewCfg = loadConfig();
|
|
@@ -664,8 +845,60 @@ export async function runReview(args) {
|
|
|
664
845
|
process.exit(0);
|
|
665
846
|
}
|
|
666
847
|
// Diff mode
|
|
667
|
-
const
|
|
848
|
+
const flagsWithValues = new Set([
|
|
849
|
+
'--spec',
|
|
850
|
+
'--diff',
|
|
851
|
+
'--git',
|
|
852
|
+
'--ref',
|
|
853
|
+
'--rules-dir',
|
|
854
|
+
'--tsconfig',
|
|
855
|
+
'--target',
|
|
856
|
+
'--max-depth',
|
|
857
|
+
'--batch-size',
|
|
858
|
+
'--min-coverage',
|
|
859
|
+
'--max-complexity',
|
|
860
|
+
'--max-errors',
|
|
861
|
+
'--max-warnings',
|
|
862
|
+
'--min-confidence',
|
|
863
|
+
'--baseline',
|
|
864
|
+
'--write-baseline',
|
|
865
|
+
]);
|
|
866
|
+
const reviewInputs = [];
|
|
867
|
+
for (let i = 0; i < args.length; i++) {
|
|
868
|
+
const arg = args[i];
|
|
869
|
+
if (arg === 'review')
|
|
870
|
+
continue;
|
|
871
|
+
if (flagsWithValues.has(arg)) {
|
|
872
|
+
i++;
|
|
873
|
+
continue;
|
|
874
|
+
}
|
|
875
|
+
if (arg.startsWith('--'))
|
|
876
|
+
continue;
|
|
877
|
+
reviewInputs.push(arg);
|
|
878
|
+
}
|
|
668
879
|
let reviewInput = reviewInputs[0];
|
|
880
|
+
const remoteUrl = parseFlagOrNext(args, '--git');
|
|
881
|
+
if (remoteUrl && !reviewInput && !diffBase) {
|
|
882
|
+
reviewInput = '.';
|
|
883
|
+
}
|
|
884
|
+
// Phase 5: diff-scoped by default.
|
|
885
|
+
// Bare `kern review` (no path, no --diff, no --full, no --git) inside a git
|
|
886
|
+
// repo defaults to reviewing changes vs the upstream branch. `--full` opts
|
|
887
|
+
// back into a cwd-wide scan. Explicit paths are unchanged — `kern review
|
|
888
|
+
// src/` still scans src/ in full. This keeps `kern review` quiet by default
|
|
889
|
+
// on large codebases without breaking CI invocations that pass a path.
|
|
890
|
+
if (!reviewInput && !diffBase && !remoteUrl && !fullMode) {
|
|
891
|
+
const autoBase = detectAutoDiffBase();
|
|
892
|
+
if (autoBase) {
|
|
893
|
+
diffBase = autoBase;
|
|
894
|
+
if (!hasFlag(args, '--json') && !hasFlag(args, '--sarif')) {
|
|
895
|
+
console.log(` No path given — reviewing changes vs ${autoBase}. Use --full to scan the whole tree, or pass a path.\n`);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
if (fullMode && !reviewInput) {
|
|
900
|
+
reviewInput = '.';
|
|
901
|
+
}
|
|
669
902
|
if (diffBase && !reviewInput) {
|
|
670
903
|
try {
|
|
671
904
|
const { execFileSync } = await import('child_process');
|
|
@@ -675,13 +908,17 @@ export async function runReview(args) {
|
|
|
675
908
|
})
|
|
676
909
|
.trim()
|
|
677
910
|
.split('\n')
|
|
678
|
-
.filter((f) => f.endsWith('.ts') || f.endsWith('.tsx'))
|
|
911
|
+
.filter((f) => f.endsWith('.ts') || f.endsWith('.tsx') || f.endsWith('.kern'))
|
|
679
912
|
.filter((f) => !f.endsWith('.d.ts') && !f.endsWith('.test.ts'));
|
|
913
|
+
const machineOutput = hasFlag(args, '--json') || hasFlag(args, '--sarif');
|
|
680
914
|
if (diffFiles.length === 0) {
|
|
681
|
-
|
|
915
|
+
if (!machineOutput)
|
|
916
|
+
console.log(` No changed .ts/.tsx/.kern files since ${diffBase}`);
|
|
682
917
|
process.exit(0);
|
|
683
918
|
}
|
|
684
|
-
|
|
919
|
+
if (!machineOutput) {
|
|
920
|
+
console.log(` Reviewing ${diffFiles.length} changed files (diff from ${diffBase})\n`);
|
|
921
|
+
}
|
|
685
922
|
const diffRanges = new Map();
|
|
686
923
|
try {
|
|
687
924
|
const unifiedDiff = execFileSync('git', ['diff', '--unified=0', '--diff-filter=ACMR', sanitizedBase], {
|
|
@@ -720,8 +957,11 @@ export async function runReview(args) {
|
|
|
720
957
|
}
|
|
721
958
|
}
|
|
722
959
|
if (!reviewInput) {
|
|
723
|
-
console.error('Usage: kern review
|
|
724
|
-
console.error(' [--
|
|
960
|
+
console.error('Usage: kern review [file|dir] [--full] [--diff base] [--git=<url>] [--security] [--mcp] [--llm] [--spec file.kern] [--cloud] [--baseline=file.json] [--new-only]');
|
|
961
|
+
console.error(' [--write-baseline=file.json] [--json] [--sarif] [--recursive] [--enforce] [--strict-parse] [--fix] [--autofix] [--rules-dir <dir>] [--include-generated]');
|
|
962
|
+
console.error('');
|
|
963
|
+
console.error(' Default (inside git): reviews changes vs origin/main. Use --full to scan the whole tree.');
|
|
964
|
+
console.error(' Default skips generated files (src/generated/, files with @generated stamps). Use --include-generated to audit them.');
|
|
725
965
|
process.exit(1);
|
|
726
966
|
}
|
|
727
967
|
if (reviewInput !== '__diff__') {
|
|
@@ -750,6 +990,7 @@ export async function runReview(args) {
|
|
|
750
990
|
minCoverage: minCoverage ?? 0,
|
|
751
991
|
enforceTemplates: enforce,
|
|
752
992
|
maxComplexity: maxComplexity ?? reviewCfg.review.maxComplexity,
|
|
993
|
+
maxHandlerLines,
|
|
753
994
|
maxErrors,
|
|
754
995
|
maxWarnings,
|
|
755
996
|
target: reviewCfg.target,
|
|
@@ -759,6 +1000,14 @@ export async function runReview(args) {
|
|
|
759
1000
|
rulesDirs: rulesDirs.length > 0 ? rulesDirs : undefined,
|
|
760
1001
|
strict,
|
|
761
1002
|
strictParse,
|
|
1003
|
+
tsConfigFilePath: tsconfigPath,
|
|
1004
|
+
publicApi: reviewCfg.review.publicApi.files.length > 0 || reviewCfg.review.publicApi.symbols.length > 0
|
|
1005
|
+
? {
|
|
1006
|
+
files: reviewCfg.review.publicApi.files,
|
|
1007
|
+
symbols: reviewCfg.review.publicApi.symbols,
|
|
1008
|
+
projectRoot: process.cwd(),
|
|
1009
|
+
}
|
|
1010
|
+
: undefined,
|
|
762
1011
|
};
|
|
763
1012
|
// Load templates for review
|
|
764
1013
|
if (reviewCfg.templates && reviewCfg.templates.length > 0) {
|
|
@@ -831,6 +1080,7 @@ export async function runReview(args) {
|
|
|
831
1080
|
fixMode,
|
|
832
1081
|
autofixMode,
|
|
833
1082
|
lintMode,
|
|
1083
|
+
skipGenerated,
|
|
834
1084
|
exportKern,
|
|
835
1085
|
enforce,
|
|
836
1086
|
jsonOutput,
|
|
@@ -845,6 +1095,9 @@ export async function runReview(args) {
|
|
|
845
1095
|
maxErrorsArg,
|
|
846
1096
|
maxWarningsArg,
|
|
847
1097
|
showConfidence,
|
|
1098
|
+
baseline,
|
|
1099
|
+
writeBaselinePath: writeBaselinePath ? resolve(writeBaselinePath) : undefined,
|
|
1100
|
+
newOnly,
|
|
848
1101
|
};
|
|
849
1102
|
const noCache = hasFlag(args, '--no-cache');
|
|
850
1103
|
if (noCache) {
|
|
@@ -879,4 +1132,15 @@ export async function runReview(args) {
|
|
|
879
1132
|
process.exit(result.exitCode);
|
|
880
1133
|
}
|
|
881
1134
|
}
|
|
1135
|
+
export async function runReview(args) {
|
|
1136
|
+
const diffBase = args.some((a) => a === '--diff' || a.startsWith('--diff'))
|
|
1137
|
+
? parseFlagOrNext(args, '--diff') || 'origin/main'
|
|
1138
|
+
: undefined;
|
|
1139
|
+
await withOptionalRemoteRepo(args, {
|
|
1140
|
+
commandName: 'review',
|
|
1141
|
+
fullClone: Boolean(diffBase),
|
|
1142
|
+
}, async () => {
|
|
1143
|
+
await runReviewLocal(args);
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
882
1146
|
//# sourceMappingURL=review.js.map
|