@kernlang/cli 3.2.3 → 3.3.5

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.
@@ -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 { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from 'fs';
4
- import { basename, relative, resolve } from 'path';
5
- import { collectTsFilesFlat, hasFlag, loadConfig, parseAndSurface, parseFlag } from '../shared.js';
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 ? resolve(tsconfigPath) : undefined };
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 };
@@ -86,7 +134,7 @@ async function runReviewPipeline(reviewConfig, entryFilePaths, modes) {
86
134
  };
87
135
  });
88
136
  try {
89
- const llmFindings = await runLLMReview(llmInputs);
137
+ const { findings: llmFindings } = await runLLMReview(llmInputs);
90
138
  if (llmFindings.length > 0) {
91
139
  if (!jsonOutput && !sarifOutput) {
92
140
  console.log(` LLM review (auto): ${llmFindings.length} finding(s) from AI`);
@@ -288,7 +336,7 @@ async function runReviewPipeline(reviewConfig, entryFilePaths, modes) {
288
336
  };
289
337
  });
290
338
  try {
291
- const llmFindings = await runLLMReview(llmInputs);
339
+ const { findings: llmFindings } = await runLLMReview(llmInputs);
292
340
  console.log(` LLM review: ${llmFindings.length} findings from AI`);
293
341
  for (const f of llmFindings) {
294
342
  const report = reports.find((r) => r.filePath === f.primarySpan.file);
@@ -306,7 +354,13 @@ async function runReviewPipeline(reviewConfig, entryFilePaths, modes) {
306
354
  }
307
355
  }
308
356
  else {
309
- // No API key — AI CLI tool IS the reviewer
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
- const eslintFindings = await runESLint(filePaths, process.cwd());
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 = reports.map((report) => {
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
- console.log(formatSARIF(reports));
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 reports) {
666
+ for (const report of reportsForOutput) {
560
667
  console.log('');
561
668
  console.log(formatReport(report, reviewConfig));
562
669
  }
563
- if (reports.length > 1) {
670
+ if (reportsForOutput.length > 1) {
671
+ console.log('');
672
+ console.log(formatSummary(reportsForOutput));
673
+ }
674
+ if (baselineComparison) {
564
675
  console.log('');
565
- console.log(formatSummary(reports));
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 reports) {
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
- console.log(` Enforcement: PASS (all files checked against thresholds)`);
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
- export async function runReview(args) {
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 tsconfigPath = parseFlag(args, '--tsconfig');
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('--')) {
@@ -636,10 +789,39 @@ export async function runReview(args) {
636
789
  const strictArg = args.find((a) => a === '--strict' || a.startsWith('--strict='));
637
790
  const strict = strictArg === '--strict' ? 'inline' : strictArg === '--strict=all' ? 'all' : false;
638
791
  const strictParse = hasFlag(args, '--strict-parse');
792
+ const requireConfidenceAnnotations = hasFlag(args, '--require-confidence');
639
793
  const listRules = hasFlag(args, '--list-rules');
640
- const diffBase = args.find((a) => a.startsWith('--diff'))
641
- ? parseFlag(args, '--diff') || args[args.indexOf('--diff') + 1] || 'origin/main'
794
+ let diffBase = args.some((a) => a === '--diff' || a.startsWith('--diff'))
795
+ ? parseFlagOrNext(args, '--diff') || 'origin/main'
642
796
  : undefined;
797
+ const fullMode = hasFlag(args, '--full');
798
+ if (fullMode && diffBase) {
799
+ console.error(' --full and --diff are mutually exclusive.');
800
+ process.exit(1);
801
+ }
802
+ let baseline;
803
+ if (baselinePath) {
804
+ const resolvedBaselinePath = resolve(baselinePath);
805
+ if (!existsSync(resolvedBaselinePath)) {
806
+ console.error(`Baseline not found: ${baselinePath}`);
807
+ process.exit(1);
808
+ }
809
+ let rawBaseline;
810
+ try {
811
+ rawBaseline = readFileSync(resolvedBaselinePath, 'utf-8');
812
+ }
813
+ catch (err) {
814
+ console.error(`Failed to read baseline ${baselinePath}: ${err.message}`);
815
+ process.exit(1);
816
+ }
817
+ try {
818
+ baseline = parseReviewBaseline(rawBaseline);
819
+ }
820
+ catch (err) {
821
+ console.error(`Failed to parse baseline ${baselinePath}: ${err.message}`);
822
+ process.exit(1);
823
+ }
824
+ }
643
825
  // --list-rules
644
826
  if (listRules) {
645
827
  const reviewCfg = loadConfig();
@@ -664,8 +846,60 @@ export async function runReview(args) {
664
846
  process.exit(0);
665
847
  }
666
848
  // Diff mode
667
- const reviewInputs = args.filter((a) => !a.startsWith('--') && a !== 'review');
849
+ const flagsWithValues = new Set([
850
+ '--spec',
851
+ '--diff',
852
+ '--git',
853
+ '--ref',
854
+ '--rules-dir',
855
+ '--tsconfig',
856
+ '--target',
857
+ '--max-depth',
858
+ '--batch-size',
859
+ '--min-coverage',
860
+ '--max-complexity',
861
+ '--max-errors',
862
+ '--max-warnings',
863
+ '--min-confidence',
864
+ '--baseline',
865
+ '--write-baseline',
866
+ ]);
867
+ const reviewInputs = [];
868
+ for (let i = 0; i < args.length; i++) {
869
+ const arg = args[i];
870
+ if (arg === 'review')
871
+ continue;
872
+ if (flagsWithValues.has(arg)) {
873
+ i++;
874
+ continue;
875
+ }
876
+ if (arg.startsWith('--'))
877
+ continue;
878
+ reviewInputs.push(arg);
879
+ }
668
880
  let reviewInput = reviewInputs[0];
881
+ const remoteUrl = parseFlagOrNext(args, '--git');
882
+ if (remoteUrl && !reviewInput && !diffBase) {
883
+ reviewInput = '.';
884
+ }
885
+ // Phase 5: diff-scoped by default.
886
+ // Bare `kern review` (no path, no --diff, no --full, no --git) inside a git
887
+ // repo defaults to reviewing changes vs the upstream branch. `--full` opts
888
+ // back into a cwd-wide scan. Explicit paths are unchanged — `kern review
889
+ // src/` still scans src/ in full. This keeps `kern review` quiet by default
890
+ // on large codebases without breaking CI invocations that pass a path.
891
+ if (!reviewInput && !diffBase && !remoteUrl && !fullMode) {
892
+ const autoBase = detectAutoDiffBase();
893
+ if (autoBase) {
894
+ diffBase = autoBase;
895
+ if (!hasFlag(args, '--json') && !hasFlag(args, '--sarif')) {
896
+ console.log(` No path given — reviewing changes vs ${autoBase}. Use --full to scan the whole tree, or pass a path.\n`);
897
+ }
898
+ }
899
+ }
900
+ if (fullMode && !reviewInput) {
901
+ reviewInput = '.';
902
+ }
669
903
  if (diffBase && !reviewInput) {
670
904
  try {
671
905
  const { execFileSync } = await import('child_process');
@@ -675,13 +909,17 @@ export async function runReview(args) {
675
909
  })
676
910
  .trim()
677
911
  .split('\n')
678
- .filter((f) => f.endsWith('.ts') || f.endsWith('.tsx'))
912
+ .filter((f) => f.endsWith('.ts') || f.endsWith('.tsx') || f.endsWith('.kern'))
679
913
  .filter((f) => !f.endsWith('.d.ts') && !f.endsWith('.test.ts'));
914
+ const machineOutput = hasFlag(args, '--json') || hasFlag(args, '--sarif');
680
915
  if (diffFiles.length === 0) {
681
- console.log(` No changed .ts/.tsx files since ${diffBase}`);
916
+ if (!machineOutput)
917
+ console.log(` No changed .ts/.tsx/.kern files since ${diffBase}`);
682
918
  process.exit(0);
683
919
  }
684
- console.log(` Reviewing ${diffFiles.length} changed files (diff from ${diffBase})\n`);
920
+ if (!machineOutput) {
921
+ console.log(` Reviewing ${diffFiles.length} changed files (diff from ${diffBase})\n`);
922
+ }
685
923
  const diffRanges = new Map();
686
924
  try {
687
925
  const unifiedDiff = execFileSync('git', ['diff', '--unified=0', '--diff-filter=ACMR', sanitizedBase], {
@@ -720,8 +958,11 @@ export async function runReview(args) {
720
958
  }
721
959
  }
722
960
  if (!reviewInput) {
723
- console.error('Usage: kern review <file.ts|dir> [--security] [--mcp] [--llm] [--spec file.kern] [--cloud]');
724
- console.error(' [--diff base] [--json] [--sarif] [--recursive] [--enforce] [--strict-parse] [--fix] [--autofix] [--rules-dir <dir>]');
961
+ console.error('Usage: kern review [file|dir] [--full] [--diff base] [--git=<url>] [--security] [--mcp] [--llm] [--spec file.kern] [--cloud] [--baseline=file.json] [--new-only]');
962
+ console.error(' [--write-baseline=file.json] [--json] [--sarif] [--recursive] [--enforce] [--strict-parse] [--fix] [--autofix] [--require-confidence] [--rules-dir <dir>] [--include-generated]');
963
+ console.error('');
964
+ console.error(' Default (inside git): reviews changes vs origin/main. Use --full to scan the whole tree.');
965
+ console.error(' Default skips generated files (src/generated/, files with @generated stamps). Use --include-generated to audit them.');
725
966
  process.exit(1);
726
967
  }
727
968
  if (reviewInput !== '__diff__') {
@@ -750,6 +991,7 @@ export async function runReview(args) {
750
991
  minCoverage: minCoverage ?? 0,
751
992
  enforceTemplates: enforce,
752
993
  maxComplexity: maxComplexity ?? reviewCfg.review.maxComplexity,
994
+ maxHandlerLines,
753
995
  maxErrors,
754
996
  maxWarnings,
755
997
  target: reviewCfg.target,
@@ -759,6 +1001,15 @@ export async function runReview(args) {
759
1001
  rulesDirs: rulesDirs.length > 0 ? rulesDirs : undefined,
760
1002
  strict,
761
1003
  strictParse,
1004
+ requireConfidenceAnnotations: requireConfidenceAnnotations || reviewCfg.review.requireConfidenceAnnotations,
1005
+ tsConfigFilePath: tsconfigPath,
1006
+ publicApi: reviewCfg.review.publicApi.files.length > 0 || reviewCfg.review.publicApi.symbols.length > 0
1007
+ ? {
1008
+ files: reviewCfg.review.publicApi.files,
1009
+ symbols: reviewCfg.review.publicApi.symbols,
1010
+ projectRoot: process.cwd(),
1011
+ }
1012
+ : undefined,
762
1013
  };
763
1014
  // Load templates for review
764
1015
  if (reviewCfg.templates && reviewCfg.templates.length > 0) {
@@ -831,6 +1082,7 @@ export async function runReview(args) {
831
1082
  fixMode,
832
1083
  autofixMode,
833
1084
  lintMode,
1085
+ skipGenerated,
834
1086
  exportKern,
835
1087
  enforce,
836
1088
  jsonOutput,
@@ -845,6 +1097,9 @@ export async function runReview(args) {
845
1097
  maxErrorsArg,
846
1098
  maxWarningsArg,
847
1099
  showConfidence,
1100
+ baseline,
1101
+ writeBaselinePath: writeBaselinePath ? resolve(writeBaselinePath) : undefined,
1102
+ newOnly,
848
1103
  };
849
1104
  const noCache = hasFlag(args, '--no-cache');
850
1105
  if (noCache) {
@@ -879,4 +1134,15 @@ export async function runReview(args) {
879
1134
  process.exit(result.exitCode);
880
1135
  }
881
1136
  }
1137
+ export async function runReview(args) {
1138
+ const diffBase = args.some((a) => a === '--diff' || a.startsWith('--diff'))
1139
+ ? parseFlagOrNext(args, '--diff') || 'origin/main'
1140
+ : undefined;
1141
+ await withOptionalRemoteRepo(args, {
1142
+ commandName: 'review',
1143
+ fullClone: Boolean(diffBase),
1144
+ }, async () => {
1145
+ await runReviewLocal(args);
1146
+ });
1147
+ }
882
1148
  //# sourceMappingURL=review.js.map