@kernlang/cli 2.0.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/cli.js +1301 -67
  2. package/dist/cli.js.map +1 -1
  3. package/package.json +11 -9
package/dist/cli.js CHANGED
@@ -2,16 +2,18 @@
2
2
  import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSync, unlinkSync } from 'fs';
3
3
  import { resolve, basename, dirname, relative } from 'path';
4
4
  import { createJiti } from 'jiti';
5
- import { parse, decompile, resolveConfig, VALID_TARGETS, VALID_STRUCTURES, generateCoreNode, isCoreNode, detectVersionsFromPackageJson, scanProject, generateConfigSource, formatScanSummary, registerTemplate, isTemplateNode, expandTemplateNode, clearTemplates, detectTemplates, COMMON_TEMPLATES } from '@kernlang/core';
5
+ import { parse, decompile, resolveConfig, VALID_TARGETS, VALID_STRUCTURES, generateCoreNode, isCoreNode, detectVersionsFromPackageJson, scanProject, generateConfigSource, formatScanSummary, registerTemplate, isTemplateNode, expandTemplateNode, clearTemplates, detectTemplates, COMMON_TEMPLATES, collectCoverageGaps, writeCoverageGaps, NODE_TYPES } from '@kernlang/core';
6
6
  import { generateReactNode, isReactNode } from '@kernlang/react';
7
7
  import { transpile } from '@kernlang/native';
8
8
  import { transpileWeb, transpileTailwind, transpileNextjs } from '@kernlang/react';
9
9
  import { transpileExpress } from '@kernlang/express';
10
+ import { transpileFastAPI } from '@kernlang/fastapi';
10
11
  import { transpileCliApp } from './transpiler-cli.js';
11
- import { transpileTerminal } from '@kernlang/terminal';
12
+ import { transpileTerminal, transpileInk } from '@kernlang/terminal';
12
13
  import { transpileVue, transpileNuxt } from '@kernlang/vue';
13
14
  import { collectLanguageMetrics } from '@kernlang/metrics';
14
- import { reviewFile, reviewDirectory, formatReport, formatSummary, checkEnforcement, formatEnforcement, exportKernIR, buildLLMPrompt, dedup, runESLint, runTSCDiagnosticsFromPaths, linkToNodes } from '@kernlang/review';
15
+ import { reviewFile, reviewGraph, resolveImportGraph, formatReport, formatSARIF, formatSummary, checkEnforcement, formatEnforcement, exportKernIR, buildLLMPrompt, dedup, runESLint, runTSCDiagnosticsFromPaths, linkToNodes, runLLMReview, isLLMAvailable, analyzeTaint, checkSpecFiles, specViolationsToFindings } from '@kernlang/review';
16
+ import { evolve, loadBuiltinDetectors, listStaged, updateStagedStatus, promoteLocal, cleanRejected, formatSplitView, loadEvolvedNodes, runGoldenTests, formatGoldenTestResults, rollbackNode, restoreNode, readEvolvedManifest, buildDiscoveryPrompt, parseDiscoveryResponse, selectRepresentativeFiles, collectTsFiles, estimateTokens, createLLMProvider, TokenBudget, validateEvolveProposal, graduateNode, compileCodegenToJS, stageEvolveV4Proposal, listStagedEvolveV4, getStagedEvolveV4, updateStagedEvolveV4Status, cleanRejectedEvolveV4, cleanApprovedEvolveV4, formatEvolveV4SplitView, promoteNode, pruneNodes, detectCollisions, renameEvolvedNode, readNodeDefinition, buildBackfillPrompt, buildRetryPrompt, rebuildEvolvedManifest } from '@kernlang/evolve';
15
17
  const args = process.argv.slice(2);
16
18
  const GENERATED_HEADER = '// Generated by KERN — do not edit. Source: ';
17
19
  // ── kern dev <dir|file> [--target=...] [--outdir=...] ─────────────────
@@ -57,10 +59,17 @@ if (args[0] === 'dev') {
57
59
  console.log(` Auto-detected: ${parts.join(', ')}`);
58
60
  }
59
61
  }
60
- catch { }
62
+ catch {
63
+ // package.json may not exist or may be malformed
64
+ }
61
65
  }
62
66
  // Load templates before compilation
63
67
  loadTemplates(devConfig);
68
+ // Load evolved nodes (v4) — graduated nodes from .kern/evolved/
69
+ const evolvedResult = loadEvolvedNodes(process.cwd(), args.includes('--verify'));
70
+ if (evolvedResult.loaded > 0) {
71
+ console.log(` Evolved nodes: ${evolvedResult.loaded} loaded`);
72
+ }
64
73
  console.log(`\n KERN dev — watching for changes`);
65
74
  console.log(` Target: ${devConfig.target}`);
66
75
  console.log(` Watch: ${relative(process.cwd(), watchDir) || '.'}`);
@@ -68,7 +77,7 @@ if (args[0] === 'dev') {
68
77
  // Initial build of all .kern files
69
78
  const initialFiles = findKernFiles(watchDir, watchPattern);
70
79
  for (const file of initialFiles) {
71
- transpileAndWrite(file, devConfig, devOutDir);
80
+ transpileAndWrite(file, devConfig, devOutDir, watchDir);
72
81
  }
73
82
  if (initialFiles.length > 0) {
74
83
  console.log(` ${initialFiles.length} file(s) compiled.\n`);
@@ -86,7 +95,7 @@ if (args[0] === 'dev') {
86
95
  const rel = relative(process.cwd(), filePath);
87
96
  const start = performance.now();
88
97
  try {
89
- transpileAndWrite(filePath, devConfig, devOutDir);
98
+ transpileAndWrite(filePath, devConfig, devOutDir, watchDir);
90
99
  const ms = Math.round(performance.now() - start);
91
100
  console.log(` ${rel} → compiled (${ms}ms)`);
92
101
  }
@@ -98,7 +107,7 @@ if (args[0] === 'dev') {
98
107
  const rel = relative(process.cwd(), filePath);
99
108
  const start = performance.now();
100
109
  try {
101
- transpileAndWrite(filePath, devConfig, devOutDir);
110
+ transpileAndWrite(filePath, devConfig, devOutDir, watchDir);
102
111
  const ms = Math.round(performance.now() - start);
103
112
  console.log(` ${rel} → compiled (${ms}ms)`);
104
113
  }
@@ -110,9 +119,12 @@ if (args[0] === 'dev') {
110
119
  const rel = relative(process.cwd(), filePath);
111
120
  const ext = filePath.endsWith('.kern') ? '.kern' : '.ir';
112
121
  const fileBaseName = basename(filePath, ext);
113
- const outDir = resolve(devOutDir ? resolve(devOutDir) : dirname(filePath), devConfig.output.outDir);
114
- const outExt = (devConfig.target === 'vue' || devConfig.target === 'nuxt') ? '.vue'
115
- : (devConfig.target === 'express' || devConfig.target === 'cli' || devConfig.target === 'terminal') ? '.ts' : '.tsx';
122
+ const unlinkRelDir = relative(resolve(watchDir), dirname(filePath));
123
+ const unlinkBaseDir = devOutDir ? resolve(resolve(devOutDir), unlinkRelDir) : dirname(filePath);
124
+ const outDir = resolve(unlinkBaseDir, devConfig.output.outDir);
125
+ const outExt = devConfig.target === 'fastapi' ? '.py'
126
+ : (devConfig.target === 'vue' || devConfig.target === 'nuxt') ? '.vue'
127
+ : (devConfig.target === 'express' || devConfig.target === 'cli' || devConfig.target === 'terminal') ? '.ts' : '.tsx';
116
128
  const outFile = resolve(outDir, `${fileBaseName}${outExt}`);
117
129
  try {
118
130
  if (existsSync(outFile)) {
@@ -169,7 +181,7 @@ function findKernFiles(dir, singleFile) {
169
181
  walk(dir);
170
182
  return files;
171
183
  }
172
- function transpileAndWrite(file, cfg, outDirOverride) {
184
+ function transpileAndWrite(file, cfg, outDirOverride, inputBase) {
173
185
  const source = readFileSync(file, 'utf-8');
174
186
  const ast = parse(source);
175
187
  const ext = file.endsWith('.kern') ? '.kern' : '.ir';
@@ -177,6 +189,13 @@ function transpileAndWrite(file, cfg, outDirOverride) {
177
189
  const target = cfg.target;
178
190
  const relSource = relative(process.cwd(), file);
179
191
  const header = GENERATED_HEADER + relSource + '\n\n';
192
+ // Emit coverage gaps unless --no-gaps
193
+ if (!args.includes('--no-gaps')) {
194
+ const gaps = collectCoverageGaps(ast, file);
195
+ if (gaps.length > 0) {
196
+ writeCoverageGaps(gaps, resolve(process.cwd(), '.kern-gaps'));
197
+ }
198
+ }
180
199
  const result = target === 'native'
181
200
  ? transpile(ast, cfg)
182
201
  : target === 'web'
@@ -185,16 +204,24 @@ function transpileAndWrite(file, cfg, outDirOverride) {
185
204
  ? transpileTailwind(ast, cfg)
186
205
  : target === 'express'
187
206
  ? transpileExpress(ast, cfg)
188
- : target === 'cli'
189
- ? transpileCliApp(ast, cfg)
190
- : target === 'terminal'
191
- ? transpileTerminal(ast, cfg)
192
- : target === 'vue'
193
- ? transpileVue(ast, cfg)
194
- : target === 'nuxt'
195
- ? transpileNuxt(ast, cfg)
196
- : transpileNextjs(ast, cfg);
197
- const outDir = resolve(outDirOverride ? resolve(outDirOverride) : dirname(file), cfg.output.outDir);
207
+ : target === 'fastapi'
208
+ ? transpileFastAPI(ast, cfg)
209
+ : target === 'cli'
210
+ ? transpileCliApp(ast, cfg)
211
+ : target === 'terminal'
212
+ ? transpileTerminal(ast, cfg)
213
+ : target === 'ink'
214
+ ? transpileInk(ast, cfg)
215
+ : target === 'vue'
216
+ ? transpileVue(ast, cfg)
217
+ : target === 'nuxt'
218
+ ? transpileNuxt(ast, cfg)
219
+ : transpileNextjs(ast, cfg);
220
+ // Preserve relative directory structure when outDirOverride is set
221
+ // e.g. kern/llm/patterns.kern with --outdir=app/ → app/llm/page.tsx (not app/page.tsx)
222
+ const relDir = inputBase ? relative(resolve(inputBase), dirname(file)) : '';
223
+ const baseDir = outDirOverride ? resolve(resolve(outDirOverride), relDir) : dirname(file);
224
+ const outDir = resolve(baseDir, cfg.output.outDir);
198
225
  mkdirSync(outDir, { recursive: true });
199
226
  if (result.artifacts && result.artifacts.length > 0 && cfg.structure !== 'flat') {
200
227
  for (const artifact of result.artifacts) {
@@ -204,14 +231,18 @@ function transpileAndWrite(file, cfg, outDirOverride) {
204
231
  }
205
232
  }
206
233
  else {
207
- const outExt = (target === 'vue' || target === 'nuxt') ? '.vue'
208
- : (target === 'express' || target === 'cli' || target === 'terminal') ? '.ts' : '.tsx';
234
+ const outExt = target === 'fastapi' ? '.py'
235
+ : (target === 'vue' || target === 'nuxt') ? '.vue'
236
+ : (target === 'express' || target === 'cli' || target === 'terminal') ? '.ts'
237
+ : '.tsx';
209
238
  // For Next.js target, use the file convention name from the transpiler result (page.tsx, layout.tsx, etc.)
210
239
  const resultWithFiles = result;
211
240
  const outFileName = (target === 'nextjs' && resultWithFiles.files && resultWithFiles.files.length > 0)
212
241
  ? resultWithFiles.files[0].path
213
242
  : `${name}${outExt}`;
214
- writeFileSync(resolve(outDir, outFileName), header + result.code);
243
+ const outFilePath = resolve(outDir, outFileName);
244
+ mkdirSync(dirname(outFilePath), { recursive: true });
245
+ writeFileSync(outFilePath, header + result.code);
215
246
  if (result.artifacts) {
216
247
  for (const artifact of result.artifacts) {
217
248
  const artifactPath = resolve(outDir, artifact.path);
@@ -232,10 +263,41 @@ function loadConfig() {
232
263
  }
233
264
  catch (err) {
234
265
  console.error(`Warning: Failed to load kern.config.ts: ${err.message}`);
235
- return resolveConfig({});
236
266
  }
237
267
  }
238
- return resolveConfig({});
268
+ // No kern.config.ts — auto-detect target from package.json
269
+ const autoDetected = autoDetectTarget();
270
+ return resolveConfig(autoDetected ? { target: autoDetected } : {});
271
+ }
272
+ function autoDetectTarget() {
273
+ const pkgPath = resolve(process.cwd(), 'package.json');
274
+ if (!existsSync(pkgPath))
275
+ return null;
276
+ try {
277
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
278
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
279
+ // Priority: most specific first
280
+ if (allDeps['next'])
281
+ return 'nextjs';
282
+ if (allDeps['nuxt'])
283
+ return 'nuxt';
284
+ if (allDeps['vue'])
285
+ return 'vue';
286
+ if (allDeps['react-native'])
287
+ return 'native';
288
+ if (allDeps['fastapi'] || allDeps['flask'] || allDeps['django'])
289
+ return 'express'; // Python web → express rules are closest
290
+ if (allDeps['express'] || allDeps['fastify'] || allDeps['koa'] || allDeps['hono'])
291
+ return 'express';
292
+ if (allDeps['tailwindcss'] && allDeps['react'])
293
+ return 'tailwind';
294
+ if (allDeps['react'])
295
+ return 'web';
296
+ return null;
297
+ }
298
+ catch {
299
+ return null;
300
+ }
239
301
  }
240
302
  function loadTemplates(cfg) {
241
303
  clearTemplates();
@@ -448,17 +510,41 @@ if (args[0] === 'init-templates') {
448
510
  console.log('');
449
511
  process.exit(0);
450
512
  }
451
- // ── kern review <file|dir|--diff base> [--json] [--recursive] [--enforce] [--min-coverage=N] [--export-kern] [--llm] [--fix] [--lint] ──
513
+ // ── kern review <file|dir|--diff base> [--json] [--sarif] [--recursive] [--enforce] [--min-coverage=N] [--max-complexity=N] [--export-kern] [--llm] [--fix] [--lint] ──
452
514
  if (args[0] === 'review') {
453
515
  const jsonOutput = args.includes('--json');
516
+ const sarifOutput = args.includes('--sarif') || args.includes('--format=sarif');
454
517
  const recursive = args.includes('--recursive') || args.includes('-r');
455
518
  const enforce = args.includes('--enforce');
456
519
  const exportKern = args.includes('--export-kern');
457
520
  const llmMode = args.includes('--llm');
521
+ const cloudMode = args.includes('--cloud');
522
+ const securityMode = args.includes('--security');
523
+ const specMode = args.includes('--spec');
524
+ const specFile = args.find(a => a.endsWith('.kern') && a !== 'review');
458
525
  const fixMode = args.includes('--fix');
459
526
  const lintMode = args.includes('--lint');
527
+ const graphMode = args.includes('--graph');
528
+ const batchMode = args.includes('--batch');
529
+ const maxDepthArg = args.find(a => a.startsWith('--max-depth='))?.split('=')[1];
530
+ const maxDepth = maxDepthArg ? Number(maxDepthArg) : 3;
531
+ const batchSizeArg = args.find(a => a.startsWith('--batch-size='))?.split('=')[1];
532
+ const batchSize = batchSizeArg ? Number(batchSizeArg) : 20;
533
+ const tsconfigPath = args.find(a => a.startsWith('--tsconfig='))?.split('=')[1];
460
534
  const minCoverageArg = args.find(a => a.startsWith('--min-coverage='))?.split('=')[1];
461
535
  const minCoverage = minCoverageArg ? Number(minCoverageArg) : undefined;
536
+ const maxComplexityArg = args.find(a => a.startsWith('--max-complexity='))?.split('=')[1];
537
+ const maxComplexity = maxComplexityArg ? Number(maxComplexityArg) : 15;
538
+ const maxErrorsArg = args.find(a => a.startsWith('--max-errors='))?.split('=')[1];
539
+ const maxErrors = maxErrorsArg ? Number(maxErrorsArg) : 0;
540
+ const maxWarningsArg = args.find(a => a.startsWith('--max-warnings='))?.split('=')[1];
541
+ const maxWarnings = maxWarningsArg ? Number(maxWarningsArg) : undefined;
542
+ const showConfidence = args.includes('--confidence');
543
+ const minConfidenceArg = args.find(a => a.startsWith('--min-confidence='))?.split('=')[1];
544
+ const minConfidence = minConfidenceArg ? Number(minConfidenceArg) : undefined;
545
+ const disableRuleArgs = args.filter(a => a.startsWith('--disable-rule=')).map(a => a.split('=')[1]);
546
+ const strictArg = args.find(a => a === '--strict' || a.startsWith('--strict='));
547
+ const strict = strictArg === '--strict' ? 'inline' : strictArg === '--strict=all' ? 'all' : false;
462
548
  const diffBase = args.find(a => a.startsWith('--diff'))
463
549
  ? (args.find(a => a.startsWith('--diff='))?.split('=')[1] || args[args.indexOf('--diff') + 1] || 'origin/main')
464
550
  : undefined;
@@ -467,8 +553,9 @@ if (args[0] === 'review') {
467
553
  let reviewInput = reviewInputs[0];
468
554
  if (diffBase && !reviewInput) {
469
555
  try {
470
- const { execSync } = await import('child_process');
471
- const diffFiles = execSync(`git diff --name-only --diff-filter=ACMR ${diffBase}`, { encoding: 'utf-8' })
556
+ const { execFileSync } = await import('child_process');
557
+ const sanitizedBase = diffBase.replace(/[^a-zA-Z0-9_.\/\-~]/g, '');
558
+ const diffFiles = execFileSync('git', ['diff', '--name-only', '--diff-filter=ACMR', sanitizedBase], { encoding: 'utf-8' })
472
559
  .trim()
473
560
  .split('\n')
474
561
  .filter(f => f.endsWith('.ts') || f.endsWith('.tsx'))
@@ -488,7 +575,8 @@ if (args[0] === 'review') {
488
575
  }
489
576
  }
490
577
  if (!reviewInput) {
491
- console.error('Usage: kern review <file.ts|dir> [--diff base] [--json] [--recursive] [--enforce] [--min-coverage=N] [--export-kern] [--llm] [--fix]');
578
+ console.error('Usage: kern review <file.ts|dir> [--security] [--llm] [--spec file.kern] [--cloud]');
579
+ console.error(' [--diff base] [--json] [--sarif] [--recursive] [--enforce] [--fix]');
492
580
  process.exit(1);
493
581
  }
494
582
  // Skip stat check for --diff mode (files are resolved individually below)
@@ -501,12 +589,29 @@ if (args[0] === 'review') {
501
589
  }
502
590
  }
503
591
  // Load kern.config.ts to get registered templates and target
592
+ // Auto-detects target from package.json if no config exists
504
593
  const reviewCfg = loadConfig();
594
+ if (!jsonOutput && !sarifOutput) {
595
+ const configExists = existsSync(resolve(process.cwd(), 'kern.config.ts'));
596
+ if (!configExists) {
597
+ console.log(` Target: ${reviewCfg.target} (auto-detected from package.json)`);
598
+ }
599
+ }
600
+ // Merge disabledRules from config + CLI flags
601
+ const cfgDisabledRules = reviewCfg.review.disabledRules ?? [];
602
+ const mergedDisabledRules = [...new Set([...cfgDisabledRules, ...disableRuleArgs])];
505
603
  const reviewConfig = {
506
604
  registeredTemplates: [],
507
605
  minCoverage: minCoverage ?? 0,
508
606
  enforceTemplates: enforce,
607
+ maxComplexity: maxComplexity ?? reviewCfg.review.maxComplexity,
608
+ maxErrors,
609
+ maxWarnings,
509
610
  target: reviewCfg.target,
611
+ showConfidence: showConfidence || reviewCfg.review.showConfidence,
612
+ minConfidence: minConfidence ?? reviewCfg.review.minConfidence,
613
+ disabledRules: mergedDisabledRules.length > 0 ? mergedDisabledRules : undefined,
614
+ strict,
510
615
  };
511
616
  // Load templates and collect their names
512
617
  if (reviewCfg.templates && reviewCfg.templates.length > 0) {
@@ -538,7 +643,9 @@ if (args[0] === 'review') {
538
643
  registerTemplate(node, file);
539
644
  }
540
645
  }
541
- catch { }
646
+ catch (e) {
647
+ console.error(` Warning: Failed to parse template ${basename(file)}: ${e.message}`);
648
+ }
542
649
  }
543
650
  }
544
651
  if (reviewConfig.registeredTemplates.length > 0) {
@@ -547,20 +654,13 @@ if (args[0] === 'review') {
547
654
  }
548
655
  // Collect reports from diff, directory, or single file
549
656
  let reports = [];
657
+ // Collect entry file paths for --graph mode
658
+ let entryFilePaths = [];
550
659
  if (reviewInput === '__diff__') {
551
660
  const diffFiles = globalThis.__diffFiles;
552
- for (const f of diffFiles) {
553
- const fullPath = resolve(f);
554
- if (existsSync(fullPath)) {
555
- try {
556
- reports.push(reviewFile(fullPath, reviewConfig));
557
- }
558
- catch { }
559
- }
560
- }
661
+ entryFilePaths = diffFiles.map(f => resolve(f)).filter(f => existsSync(f));
561
662
  }
562
663
  else {
563
- // Support multiple positional paths: kern review file1.ts file2.ts dir/
564
664
  const paths = reviewInputs.length > 0 ? reviewInputs : [reviewInput];
565
665
  for (const p of paths) {
566
666
  const rPath = resolve(p);
@@ -568,18 +668,52 @@ if (args[0] === 'review') {
568
668
  continue;
569
669
  const rStat = statSync(rPath);
570
670
  if (rStat.isDirectory()) {
571
- reports.push(...reviewDirectory(rPath, recursive, reviewConfig));
671
+ // Collect files from directory for graph seeding
672
+ entryFilePaths.push(...collectTsFilesFlat(rPath, recursive));
572
673
  }
573
674
  else {
675
+ entryFilePaths.push(rPath);
676
+ }
677
+ }
678
+ }
679
+ if (graphMode && entryFilePaths.length > 0) {
680
+ // --graph: resolve import graph, review all files with provenance
681
+ const graphOpts = { maxDepth, tsConfigFilePath: tsconfigPath ? resolve(tsconfigPath) : undefined };
682
+ const graph = resolveImportGraph(entryFilePaths, graphOpts);
683
+ console.log(` Graph: ${graph.totalFiles} files resolved (${graph.skipped} skipped, depth ${maxDepth})`);
684
+ reports = reviewGraph(entryFilePaths, reviewConfig, graphOpts);
685
+ }
686
+ else if (batchMode && entryFilePaths.length > batchSize) {
687
+ // --batch: process in chunks for large repos
688
+ const totalBatches = Math.ceil(entryFilePaths.length / batchSize);
689
+ for (let i = 0; i < entryFilePaths.length; i += batchSize) {
690
+ const batch = entryFilePaths.slice(i, i + batchSize);
691
+ const batchNum = Math.floor(i / batchSize) + 1;
692
+ for (const f of batch) {
574
693
  try {
575
- reports.push(reviewFile(rPath, reviewConfig));
694
+ reports.push(reviewFile(f, reviewConfig));
695
+ }
696
+ catch (e) {
697
+ console.error(` Review error in ${f}: ${e.message}`);
576
698
  }
577
- catch { }
699
+ }
700
+ const batchFindings = reports.slice(-batch.length).reduce((sum, r) => sum + r.findings.length, 0);
701
+ console.log(` Batch ${batchNum}/${totalBatches}: ${batch.length} files reviewed (${batchFindings} findings)`);
702
+ }
703
+ }
704
+ else {
705
+ // Standard mode: review each file individually
706
+ for (const f of entryFilePaths) {
707
+ try {
708
+ reports.push(reviewFile(f, reviewConfig));
709
+ }
710
+ catch (e) {
711
+ console.error(` Review error in ${f}: ${e.message}`);
578
712
  }
579
713
  }
580
714
  }
581
715
  if (reports.length === 0) {
582
- console.log(' No .ts/.tsx files found to review.');
716
+ console.log(' No reviewable files found (.ts/.tsx/.py/.kern).');
583
717
  process.exit(0);
584
718
  }
585
719
  // --export-kern: output KERN IR for AI review (v1 compat)
@@ -590,15 +724,186 @@ if (args[0] === 'review') {
590
724
  }
591
725
  process.exit(0);
592
726
  }
593
- // --llm: output structured LLM prompt with nodeId aliases
594
- if (llmMode) {
727
+ // --spec: verify .kern spec contracts against .ts implementation
728
+ if (specMode && specFile) {
729
+ const kernFilePath = resolve(specFile);
730
+ if (!existsSync(kernFilePath)) {
731
+ console.error(` .kern spec file not found: ${specFile}`);
732
+ process.exit(1);
733
+ }
734
+ console.log(`\n KERN spec check: ${specFile} → ${reports.length} implementation files\n`);
735
+ let totalViolations = 0;
595
736
  for (const report of reports) {
596
- console.log(`\n// ── ${report.filePath} ──`);
597
- console.log(buildLLMPrompt(report.inferred, report.templateMatches));
737
+ const result = checkSpecFiles(kernFilePath, report.filePath);
738
+ if (result.violations.length > 0) {
739
+ const findings = specViolationsToFindings(result);
740
+ totalViolations += findings.length;
741
+ report.findings.push(...findings);
742
+ report.findings = dedup(report.findings);
743
+ for (const v of result.violations) {
744
+ const icon = v.kind.includes('missing') || v.kind === 'spec-unimplemented' ? '✗' : '~';
745
+ const sev = v.kind === 'spec-auth-missing' || v.kind === 'spec-unimplemented' ? 'ERROR' : v.kind === 'spec-undeclared' ? 'INFO' : 'WARN';
746
+ console.log(` ${icon} [${sev}] ${v.kind}: ${v.detail}`);
747
+ if (v.suggestion)
748
+ console.log(` → ${v.suggestion}`);
749
+ }
750
+ }
751
+ if (result.matched.length > 0) {
752
+ const satisfied = result.matched.length - result.violations.filter(v => v.kind !== 'spec-undeclared' && v.kind !== 'spec-unimplemented').length;
753
+ console.log(`\n Matched: ${result.matched.length} routes | Satisfied: ${satisfied} | Violations: ${totalViolations}`);
754
+ if (result.unmatchedSpecs.length > 0)
755
+ console.log(` Unimplemented: ${result.unmatchedSpecs.map(s => s.routeKey).join(', ')}`);
756
+ if (result.unmatchedImpls.length > 0)
757
+ console.log(` Undeclared: ${result.unmatchedImpls.map(i => i.routeKey).join(', ')}`);
758
+ }
759
+ }
760
+ if (totalViolations === 0) {
761
+ console.log(' All spec contracts satisfied.');
762
+ }
763
+ console.log('');
764
+ // Fall through to normal output
765
+ }
766
+ // --security: show only security-related findings
767
+ if (securityMode) {
768
+ const SECURITY_RULES = new Set([
769
+ 'xss-unsafe-html', 'hardcoded-secret', 'command-injection', 'no-eval',
770
+ 'insecure-random', 'cors-wildcard', 'helmet-missing', 'open-redirect',
771
+ 'jwt-weak-verification', 'cookie-hardening', 'csrf-detection', 'csp-strength',
772
+ 'path-traversal', 'weak-password-hashing', 'regex-dos', 'missing-input-validation',
773
+ 'prototype-pollution', 'information-exposure', 'prompt-injection',
774
+ 'taint-command', 'taint-fs', 'taint-sql', 'taint-redirect', 'taint-eval',
775
+ 'taint-insufficient-sanitizer', 'taint-crossfile-command', 'taint-crossfile-fs',
776
+ 'taint-crossfile-sql', 'taint-crossfile-redirect', 'taint-crossfile-eval',
777
+ 'spec-auth-missing', 'spec-validate-missing', 'spec-guard-missing',
778
+ 'spec-middleware-missing', 'spec-unimplemented',
779
+ ]);
780
+ console.log('\n KERN Security Report\n');
781
+ let totalSec = 0;
782
+ for (const report of reports) {
783
+ const secFindings = report.findings.filter(f => SECURITY_RULES.has(f.ruleId));
784
+ if (secFindings.length === 0)
785
+ continue;
786
+ totalSec += secFindings.length;
787
+ const rel = relative(process.cwd(), report.filePath);
788
+ console.log(` ${rel}:`);
789
+ for (const f of secFindings) {
790
+ const icon = f.severity === 'error' ? '✗' : f.severity === 'warning' ? '~' : '-';
791
+ console.log(` ${icon} L${f.primarySpan.startLine}: [${f.ruleId}] ${f.message}`);
792
+ if (f.suggestion)
793
+ console.log(` → ${f.suggestion}`);
794
+ }
795
+ console.log('');
796
+ }
797
+ if (totalSec === 0) {
798
+ console.log(' No security issues found.');
799
+ }
800
+ else {
801
+ const errors = reports.flatMap(r => r.findings).filter(f => SECURITY_RULES.has(f.ruleId) && f.severity === 'error').length;
802
+ const warnings = reports.flatMap(r => r.findings).filter(f => SECURITY_RULES.has(f.ruleId) && f.severity === 'warning').length;
803
+ console.log(` Total: ${totalSec} security findings (${errors} errors, ${warnings} warnings)`);
598
804
  }
599
- console.log('\n// Paste the JSON response from your AI to validate and map findings back to TS.');
805
+ console.log(' Rules: OWASP Top 10, OWASP LLM Top 10, Taint Tracking, Spec Contracts');
806
+ console.log('');
807
+ process.exit(0);
808
+ }
809
+ // --cloud: KERN Pro cloud review (coming soon)
810
+ if (cloudMode) {
811
+ console.log('');
812
+ console.log(' KERN Pro — Cloud-powered AI review');
813
+ console.log('');
814
+ console.log(' Coming soon. Cloud review will provide:');
815
+ console.log(' • LLM-powered security analysis without an AI IDE');
816
+ console.log(' • Team dashboard with trend tracking');
817
+ console.log(' • Custom rule engine for enterprise');
818
+ console.log(' • CI/CD integration with quality gates');
819
+ console.log('');
820
+ console.log(' For now, use --llm with your AI assistant (Claude Code, Cursor, etc.)');
821
+ console.log(' The assistant reads the KERN IR output and performs the AI review.');
822
+ console.log('');
823
+ console.log(' → kern review src/ --llm');
824
+ console.log('');
825
+ console.log(' Join the waitlist: https://kernlang.dev/pro');
826
+ console.log('');
600
827
  process.exit(0);
601
828
  }
829
+ // --llm: LLM-assisted security review (batch file check)
830
+ if (llmMode) {
831
+ // Build graph context for LLM markers if --graph is active
832
+ const llmGraphContext = graphMode ? (() => {
833
+ const fileDistances = new Map();
834
+ for (const report of reports) {
835
+ const finding = report.findings[0];
836
+ const distance = finding?.distance ?? 0;
837
+ fileDistances.set(report.filePath, distance);
838
+ }
839
+ for (const ep of entryFilePaths) {
840
+ fileDistances.set(ep, 0);
841
+ }
842
+ return { fileDistances };
843
+ })() : undefined;
844
+ if (isLLMAvailable()) {
845
+ // Phase 3: actual LLM API call with taint context
846
+ console.log(' LLM review: calling API...');
847
+ const llmInputs = reports.map(report => ({
848
+ filePath: report.filePath,
849
+ inferred: report.inferred,
850
+ templateMatches: report.templateMatches,
851
+ taintResults: analyzeTaint(report.inferred, report.filePath),
852
+ graphContext: llmGraphContext,
853
+ }));
854
+ try {
855
+ const llmFindings = await runLLMReview(llmInputs);
856
+ console.log(` LLM review: ${llmFindings.length} findings from AI`);
857
+ // Merge LLM findings into reports
858
+ for (const f of llmFindings) {
859
+ const report = reports.find(r => r.filePath === f.primarySpan.file);
860
+ if (report) {
861
+ report.findings.push(f);
862
+ }
863
+ else if (reports.length > 0) {
864
+ reports[0].findings.push(f);
865
+ }
866
+ }
867
+ // Dedup after merge
868
+ for (const report of reports) {
869
+ report.findings = dedup(report.findings);
870
+ }
871
+ }
872
+ catch (err) {
873
+ console.error(` LLM review failed: ${err.message}`);
874
+ }
875
+ // Fall through to normal output (don't exit — show merged findings)
876
+ }
877
+ else {
878
+ // No cloud API key — output static findings + KERN IR for the AI assistant
879
+ // The LLM running this command (Claude Code, Cursor, etc.) IS the reviewer
880
+ console.log('\n ── KERN IR for LLM review ──\n');
881
+ for (const report of reports) {
882
+ console.log(`// ── ${report.filePath} ──`);
883
+ console.log(buildLLMPrompt(report.inferred, report.templateMatches, llmGraphContext));
884
+ // Include taint analysis context
885
+ const taintResults = analyzeTaint(report.inferred, report.filePath);
886
+ if (taintResults.length > 0) {
887
+ console.log('\n// Taint analysis:');
888
+ for (const t of taintResults) {
889
+ for (const p of t.paths) {
890
+ const status = p.sanitized ? `SANITIZED (${p.sanitizer})` : 'UNSANITIZED';
891
+ console.log(`// ${t.fnName}: ${p.source.origin} → ${p.sink.name}() [${status}]`);
892
+ }
893
+ }
894
+ }
895
+ console.log('');
896
+ }
897
+ // Show static findings summary
898
+ const totalFindings = reports.reduce((sum, r) => sum + r.findings.length, 0);
899
+ const errors = reports.reduce((sum, r) => sum + r.findings.filter(f => f.severity === 'error').length, 0);
900
+ const warnings = reports.reduce((sum, r) => sum + r.findings.filter(f => f.severity === 'warning').length, 0);
901
+ console.log(` Static analysis: ${totalFindings} findings (${errors} errors, ${warnings} warnings)`);
902
+ console.log(' Review the KERN IR above for security issues the static rules may have missed.');
903
+ console.log('');
904
+ }
905
+ // Fall through to normal output — show full report with static findings
906
+ }
602
907
  // --fix: auto-migration — write .kern files from template suggestions, verify roundtrip
603
908
  if (fixMode) {
604
909
  let fixed = 0;
@@ -665,30 +970,42 @@ if (args[0] === 'review') {
665
970
  }
666
971
  }
667
972
  if (jsonOutput) {
668
- console.log(JSON.stringify(reports.length === 1 ? reports[0] : reports, null, 2));
973
+ // Include KERN IR + LLM prompt in JSON so the calling AI can review
974
+ const enriched = reports.map(report => {
975
+ const llmPrompt = buildLLMPrompt(report.inferred, report.templateMatches);
976
+ const kernIR = exportKernIR(report.inferred, report.templateMatches);
977
+ return { ...report, kernIR, llmPrompt };
978
+ });
979
+ console.log(JSON.stringify(enriched.length === 1 ? enriched[0] : enriched, null, 2));
980
+ }
981
+ else if (sarifOutput) {
982
+ console.log(formatSARIF(reports));
669
983
  }
670
984
  else {
671
985
  for (const report of reports) {
672
986
  console.log('');
673
- console.log(formatReport(report));
987
+ console.log(formatReport(report, reviewConfig));
674
988
  }
675
989
  if (reports.length > 1) {
676
990
  console.log('');
677
991
  console.log(formatSummary(reports));
678
992
  }
679
993
  // Enforcement
680
- if (enforce || minCoverage !== undefined) {
994
+ const hasThresholds = minCoverage !== undefined || maxComplexityArg !== undefined || maxErrorsArg !== undefined || maxWarningsArg !== undefined;
995
+ if (enforce || hasThresholds) {
681
996
  console.log('');
682
997
  let allPassed = true;
683
998
  for (const report of reports) {
684
999
  const result = checkEnforcement(report, reviewConfig);
685
1000
  if (!result.passed) {
686
1001
  allPassed = false;
1002
+ console.log(` File: ${report.filePath}`);
687
1003
  console.log(formatEnforcement(result));
1004
+ console.log('');
688
1005
  }
689
1006
  }
690
1007
  if (allPassed) {
691
- console.log(` Enforcement: PASS (all files)`);
1008
+ console.log(` Enforcement: PASS (all files checked against thresholds)`);
692
1009
  }
693
1010
  else {
694
1011
  process.exit(1);
@@ -697,6 +1014,886 @@ if (args[0] === 'review') {
697
1014
  }
698
1015
  process.exit(0);
699
1016
  }
1017
+ // ── kern evolve <dir|file> [options] ──────────────────────────────────
1018
+ if (args[0] === 'evolve' && args[1] !== undefined && !args[1].startsWith('evolve:')) {
1019
+ const evolveInput = args[1];
1020
+ if (!evolveInput || evolveInput.startsWith('--')) {
1021
+ console.error('Usage: kern evolve <dir|file> [--recursive] [--preview] [--min-confidence=N] [--min-support=N] [--json]');
1022
+ process.exit(1);
1023
+ }
1024
+ const evolvePath = resolve(evolveInput);
1025
+ const stat = existsSync(evolvePath) ? statSync(evolvePath) : null;
1026
+ if (!stat) {
1027
+ console.error(`Not found: ${evolveInput}`);
1028
+ process.exit(1);
1029
+ }
1030
+ const recursive = args.includes('--recursive') || args.includes('-r');
1031
+ const preview = args.includes('--preview');
1032
+ const jsonOutput = args.includes('--json');
1033
+ const minConfArg = args.find(a => a.startsWith('--min-confidence='))?.split('=')[1];
1034
+ const minSupportArg = args.find(a => a.startsWith('--min-support='))?.split('=')[1];
1035
+ const enableNodes = args.includes('--nodes') || args.includes('--from-gaps');
1036
+ const evolveOptions = {
1037
+ recursive,
1038
+ preview,
1039
+ enableNodeProposals: enableNodes,
1040
+ thresholds: {
1041
+ ...(minConfArg ? { minConfidence: Number(minConfArg) } : {}),
1042
+ ...(minSupportArg ? { minSupport: Number(minSupportArg) } : {}),
1043
+ },
1044
+ };
1045
+ // Load built-in detectors
1046
+ await loadBuiltinDetectors();
1047
+ console.log(`\n KERN evolve — scanning for template gaps\n`);
1048
+ console.log(` Input: ${relative(process.cwd(), evolvePath) || '.'}`);
1049
+ console.log(` Mode: ${preview ? 'preview (no staging)' : 'detect + stage'}`);
1050
+ console.log('');
1051
+ const result = evolve(evolvePath, evolveOptions);
1052
+ if (jsonOutput) {
1053
+ console.log(JSON.stringify(result, null, 2));
1054
+ }
1055
+ else {
1056
+ console.log(` Gaps detected: ${result.gaps.length}`);
1057
+ if (result.conceptSummary) {
1058
+ console.log(` ${result.conceptSummary.formatted}`);
1059
+ }
1060
+ console.log(` Patterns analyzed: ${result.analyzed.length}`);
1061
+ console.log(` Templates proposed: ${result.proposals.length}`);
1062
+ console.log(` Validated: ${result.validated.filter(v => v.validation.parseOk && v.validation.expansionOk).length}/${result.validated.length}`);
1063
+ if (!preview && result.staged.length > 0) {
1064
+ console.log(` Staged for review: ${result.staged.length}`);
1065
+ console.log(`\n Run 'kern evolve:review --list' to review proposals.`);
1066
+ }
1067
+ if (result.proposals.length > 0 && !jsonOutput) {
1068
+ console.log('\n Proposed templates:');
1069
+ for (const p of result.proposals) {
1070
+ const v = result.validated.find(v => v.proposal.id === p.id);
1071
+ const status = v ? (v.validation.parseOk && v.validation.expansionOk ? '✓' : '✗') : '?';
1072
+ console.log(` ${status} ${p.templateName} (${p.namespace}) — score: ${p.qualityScore.overallScore}, instances: ${p.instanceCount}`);
1073
+ }
1074
+ }
1075
+ // v3: Node proposals
1076
+ if (result.nodeProposals && result.nodeProposals.length > 0 && !jsonOutput) {
1077
+ console.log(`\n Node proposals (v3): ${result.nodeProposals.length}`);
1078
+ for (const np of result.nodeProposals) {
1079
+ const nv = result.nodeValidated?.find(v => v.proposal.id === np.id);
1080
+ const status = nv ? (nv.validation.parseOk && nv.validation.codegenOk ? '✓' : '✗') : '?';
1081
+ console.log(` ${status} ${np.nodeName} — express: ${np.expressibilityScore.overall}, freq: ${np.frequency}, score: ${np.qualityScore}`);
1082
+ }
1083
+ if (result.stagedNodes && result.stagedNodes.length > 0) {
1084
+ console.log(` Staged nodes: ${result.stagedNodes.length}`);
1085
+ }
1086
+ }
1087
+ }
1088
+ console.log('');
1089
+ process.exit(0);
1090
+ }
1091
+ // ── kern evolve:review [options] ─────────────────────────────────────
1092
+ if (args[0] === 'evolve:review') {
1093
+ const listMode = args.includes('--list') || args.length === 1;
1094
+ const approveId = (() => {
1095
+ const eqArg = args.find(a => a.startsWith('--approve='));
1096
+ if (eqArg)
1097
+ return eqArg.split('=')[1];
1098
+ const idx = args.indexOf('--approve');
1099
+ return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : undefined;
1100
+ })();
1101
+ const rejectId = (() => {
1102
+ const eqArg = args.find(a => a.startsWith('--reject='));
1103
+ if (eqArg)
1104
+ return eqArg.split('=')[1];
1105
+ const idx = args.indexOf('--reject');
1106
+ return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : undefined;
1107
+ })();
1108
+ const promoteMode = args.includes('--promote');
1109
+ const isLocal = args.includes('--local') || !args.includes('--catalog');
1110
+ if (approveId) {
1111
+ const updated = updateStagedStatus(approveId, 'approved');
1112
+ if (updated) {
1113
+ console.log(` Approved: ${approveId}`);
1114
+ }
1115
+ else {
1116
+ console.error(` Not found: ${approveId}`);
1117
+ process.exit(1);
1118
+ }
1119
+ process.exit(0);
1120
+ }
1121
+ if (rejectId) {
1122
+ const updated = updateStagedStatus(rejectId, 'rejected');
1123
+ if (updated) {
1124
+ console.log(` Rejected: ${rejectId}`);
1125
+ cleanRejected();
1126
+ }
1127
+ else {
1128
+ console.error(` Not found: ${rejectId}`);
1129
+ process.exit(1);
1130
+ }
1131
+ process.exit(0);
1132
+ }
1133
+ if (promoteMode) {
1134
+ if (!isLocal) {
1135
+ console.log(' Catalog promotion is for contributors who want to upstream templates.');
1136
+ console.log(' Use --local (default) to write templates to your project.');
1137
+ process.exit(0);
1138
+ }
1139
+ const promoted = promoteLocal();
1140
+ if (promoted.length === 0) {
1141
+ console.log(' No approved proposals to promote.');
1142
+ }
1143
+ else {
1144
+ console.log(` Promoted ${promoted.length} template(s) to templates/:`);
1145
+ for (const name of promoted) {
1146
+ console.log(` ${name}`);
1147
+ }
1148
+ }
1149
+ process.exit(0);
1150
+ }
1151
+ // Default: list mode
1152
+ const staged = listStaged();
1153
+ if (staged.length === 0) {
1154
+ console.log(' No staged proposals. Run \'kern evolve <dir>\' to detect gaps.');
1155
+ process.exit(0);
1156
+ }
1157
+ console.log(`\n KERN evolve:review — ${staged.length} proposal(s)\n`);
1158
+ for (const s of staged) {
1159
+ console.log(formatSplitView(s));
1160
+ console.log('');
1161
+ }
1162
+ process.exit(0);
1163
+ }
1164
+ // ── kern evolve:discover <dir> [--recursive] [--provider=openai|ollama] [--max-tokens=N] ──
1165
+ if (args[0] === 'evolve:discover') {
1166
+ const discoverInput = args[1];
1167
+ if (!discoverInput || discoverInput.startsWith('--')) {
1168
+ console.error('Usage: kern evolve:discover <dir> [--recursive] [--provider=openai|anthropic|ollama] [--max-tokens=N]');
1169
+ process.exit(1);
1170
+ }
1171
+ const discoverPath = resolve(discoverInput);
1172
+ if (!existsSync(discoverPath)) {
1173
+ console.error(`Not found: ${discoverInput}`);
1174
+ process.exit(1);
1175
+ }
1176
+ const recursive = args.includes('--recursive') || args.includes('-r');
1177
+ const providerArg = args.find(a => a.startsWith('--provider='))?.split('=')[1];
1178
+ const maxTokensArg = args.find(a => a.startsWith('--max-tokens='))?.split('=')[1];
1179
+ const maxTokens = maxTokensArg ? Number(maxTokensArg) : 100000;
1180
+ console.log(`\n KERN evolve:discover — LLM pattern discovery\n`);
1181
+ console.log(` Input: ${relative(process.cwd(), discoverPath) || '.'}`);
1182
+ // Collect files and batch
1183
+ const tsFiles = collectTsFiles(discoverPath, recursive);
1184
+ console.log(` Files found: ${tsFiles.length}`);
1185
+ if (tsFiles.length === 0) {
1186
+ console.log(' No TypeScript files to analyze.');
1187
+ process.exit(0);
1188
+ }
1189
+ const batches = selectRepresentativeFiles(tsFiles);
1190
+ console.log(` Batches: ${batches.length} (sampling representative files)`);
1191
+ // Load existing evolved keywords for dedup
1192
+ const manifest = readEvolvedManifest();
1193
+ const evolvedKeywords = manifest ? Object.keys(manifest.nodes) : [];
1194
+ // Create LLM provider
1195
+ let provider;
1196
+ try {
1197
+ provider = createLLMProvider({ provider: providerArg });
1198
+ }
1199
+ catch (err) {
1200
+ console.error(` ${err.message}`);
1201
+ process.exit(1);
1202
+ }
1203
+ console.log(` Provider: ${provider.name}`);
1204
+ const budget = new TokenBudget(maxTokens);
1205
+ const allProposals = [];
1206
+ const runId = `run-${Date.now()}`;
1207
+ for (let i = 0; i < batches.length; i++) {
1208
+ if (budget.exhausted) {
1209
+ console.log(` Token budget exhausted (${budget}). Stopping.`);
1210
+ break;
1211
+ }
1212
+ const batch = batches[i];
1213
+ const files = batch.map(fp => ({
1214
+ path: relative(process.cwd(), fp),
1215
+ content: readFileSync(fp, 'utf-8'),
1216
+ }));
1217
+ const prompt = buildDiscoveryPrompt(files, NODE_TYPES, evolvedKeywords);
1218
+ budget.add(estimateTokens(prompt));
1219
+ console.log(` Batch ${i + 1}/${batches.length}: ${files.map(f => f.path).join(', ')}`);
1220
+ try {
1221
+ const response = await provider.complete(prompt);
1222
+ budget.add(estimateTokens(response));
1223
+ const proposals = parseDiscoveryResponse(response, runId);
1224
+ allProposals.push(...proposals);
1225
+ if (proposals.length > 0) {
1226
+ console.log(` → ${proposals.length} pattern(s) found: ${proposals.map(p => p.keyword).join(', ')}`);
1227
+ }
1228
+ }
1229
+ catch (err) {
1230
+ console.error(` → Error: ${err.message}`);
1231
+ }
1232
+ }
1233
+ // Dedup across batches
1234
+ const seen = new Set();
1235
+ const uniqueProposals = allProposals.filter(p => {
1236
+ if (seen.has(p.keyword))
1237
+ return false;
1238
+ seen.add(p.keyword);
1239
+ return true;
1240
+ });
1241
+ console.log(`\n Discovery complete.`);
1242
+ console.log(` Tokens used: ${budget}`);
1243
+ console.log(` Proposals: ${uniqueProposals.length}\n`);
1244
+ // Validate and stage proposals (with LLM retry on failure, max 2 retries)
1245
+ const existingKw = [...NODE_TYPES, ...evolvedKeywords];
1246
+ let stagedCount = 0;
1247
+ const maxRetries = 2;
1248
+ for (let pi = 0; pi < uniqueProposals.length; pi++) {
1249
+ let proposal = uniqueProposals[pi];
1250
+ // Assign ID if missing
1251
+ if (!proposal.id) {
1252
+ proposal.id = `${proposal.keyword}-${Date.now()}`;
1253
+ }
1254
+ let validation = validateEvolveProposal(proposal, existingKw);
1255
+ let allOk = validation.schemaOk && validation.keywordOk && validation.parseOk && validation.codegenCompileOk && validation.codegenRunOk;
1256
+ // Retry on fixable failures (parse, codegen, typescript, golden diff)
1257
+ const isFixable = validation.schemaOk && validation.keywordOk && !allOk;
1258
+ if (!allOk && isFixable && provider) {
1259
+ for (let retry = 1; retry <= maxRetries; retry++) {
1260
+ console.log(` \u21BB ${proposal.keyword} — retry ${retry}/${maxRetries} (feeding errors to LLM)`);
1261
+ try {
1262
+ const retryPrompt = buildRetryPrompt(proposal, validation.errors);
1263
+ budget.add(estimateTokens(retryPrompt));
1264
+ const retryResponse = await provider.complete(retryPrompt);
1265
+ if (!retryResponse || typeof retryResponse !== 'string')
1266
+ throw new Error('LLM returned empty or invalid response');
1267
+ budget.add(estimateTokens(retryResponse));
1268
+ // Parse retry response as single object
1269
+ let json = retryResponse.trim();
1270
+ const fenceMatch = json.match(/```(?:json)?\s*\n?([\s\S]*?)```/);
1271
+ if (fenceMatch)
1272
+ json = fenceMatch[1].trim();
1273
+ const objStart = json.indexOf('{');
1274
+ const objEnd = json.lastIndexOf('}');
1275
+ if (objStart !== -1 && objEnd > objStart)
1276
+ json = json.slice(objStart, objEnd + 1);
1277
+ const fixed = JSON.parse(json);
1278
+ if (typeof fixed !== 'object' || fixed === null)
1279
+ throw new Error('LLM retry response is not a JSON object');
1280
+ // Merge fixes into proposal
1281
+ if (typeof fixed.kernExample === 'string')
1282
+ proposal.kernExample = fixed.kernExample;
1283
+ if (typeof fixed.expectedOutput === 'string')
1284
+ proposal.expectedOutput = fixed.expectedOutput;
1285
+ if (typeof fixed.codegenSource === 'string')
1286
+ proposal.codegenSource = fixed.codegenSource;
1287
+ validation = validateEvolveProposal(proposal, existingKw);
1288
+ allOk = validation.schemaOk && validation.keywordOk && validation.parseOk && validation.codegenCompileOk && validation.codegenRunOk;
1289
+ if (allOk)
1290
+ break;
1291
+ }
1292
+ catch (e) {
1293
+ console.error(` Retry ${retry} failed: ${e.message}`);
1294
+ }
1295
+ }
1296
+ }
1297
+ const status = allOk ? '\u2713' : '\u2717';
1298
+ console.log(` ${status} ${proposal.keyword} — ${proposal.reason.observation}`);
1299
+ if (validation.errors.length > 0) {
1300
+ for (const err of validation.errors.slice(0, 3)) {
1301
+ console.log(` ${err}`);
1302
+ }
1303
+ }
1304
+ // Stage proposals for review (including failed ones — user can inspect)
1305
+ validation.retryCount = allOk ? 0 : maxRetries;
1306
+ stageEvolveV4Proposal(proposal, validation);
1307
+ stagedCount++;
1308
+ }
1309
+ if (stagedCount > 0) {
1310
+ console.log(`\n Staged ${stagedCount} proposal(s).`);
1311
+ console.log(` Run 'kern evolve:review-v4' to review and graduate proposals.`);
1312
+ }
1313
+ console.log('');
1314
+ process.exit(0);
1315
+ }
1316
+ // ── kern evolve:review-v4 [--list] [--approve=<id>] [--reject=<id>] [--detail=<id>] ──
1317
+ if (args[0] === 'evolve:review-v4') {
1318
+ const approveV4Id = (() => {
1319
+ const eqArg = args.find(a => a.startsWith('--approve='));
1320
+ if (eqArg)
1321
+ return eqArg.split('=')[1];
1322
+ const idx = args.indexOf('--approve');
1323
+ return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : undefined;
1324
+ })();
1325
+ const rejectV4Id = (() => {
1326
+ const eqArg = args.find(a => a.startsWith('--reject='));
1327
+ if (eqArg)
1328
+ return eqArg.split('=')[1];
1329
+ const idx = args.indexOf('--reject');
1330
+ return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : undefined;
1331
+ })();
1332
+ const detailV4Id = (() => {
1333
+ const eqArg = args.find(a => a.startsWith('--detail='));
1334
+ if (eqArg)
1335
+ return eqArg.split('=')[1];
1336
+ const idx = args.indexOf('--detail');
1337
+ return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : undefined;
1338
+ })();
1339
+ if (approveV4Id) {
1340
+ // Approve → validate → compile → graduate
1341
+ const staged = getStagedEvolveV4(approveV4Id);
1342
+ if (!staged) {
1343
+ console.error(` Not found: ${approveV4Id}`);
1344
+ process.exit(1);
1345
+ }
1346
+ const { proposal, validation } = staged;
1347
+ const allOk = validation.schemaOk && validation.keywordOk && validation.parseOk && validation.codegenCompileOk && validation.codegenRunOk;
1348
+ if (!allOk) {
1349
+ console.error(` Cannot approve — validation failed for '${proposal.keyword}':`);
1350
+ for (const err of validation.errors) {
1351
+ console.error(` ${err}`);
1352
+ }
1353
+ process.exit(1);
1354
+ }
1355
+ // Compile codegen source to JS
1356
+ let compiledJs;
1357
+ try {
1358
+ compiledJs = compileCodegenToJS(proposal.codegenSource);
1359
+ }
1360
+ catch (err) {
1361
+ console.error(` Failed to compile codegen for '${proposal.keyword}': ${err.message}`);
1362
+ process.exit(1);
1363
+ }
1364
+ // Graduate the node
1365
+ const result = graduateNode(proposal, compiledJs);
1366
+ if (result.success) {
1367
+ updateStagedEvolveV4Status(approveV4Id, 'approved');
1368
+ cleanApprovedEvolveV4(approveV4Id);
1369
+ console.log(` Graduated '${proposal.keyword}' → ${result.path}`);
1370
+ console.log(` The node is now available in kern compile and kern dev.`);
1371
+ }
1372
+ else {
1373
+ console.error(` Graduation failed: ${result.error}`);
1374
+ process.exit(1);
1375
+ }
1376
+ process.exit(0);
1377
+ }
1378
+ if (rejectV4Id) {
1379
+ const updated = updateStagedEvolveV4Status(rejectV4Id, 'rejected');
1380
+ if (updated) {
1381
+ console.log(` Rejected: ${updated.proposal.keyword} (${rejectV4Id})`);
1382
+ cleanRejectedEvolveV4();
1383
+ }
1384
+ else {
1385
+ console.error(` Not found: ${rejectV4Id}`);
1386
+ process.exit(1);
1387
+ }
1388
+ process.exit(0);
1389
+ }
1390
+ if (detailV4Id) {
1391
+ const staged = getStagedEvolveV4(detailV4Id);
1392
+ if (!staged) {
1393
+ console.error(` Not found: ${detailV4Id}`);
1394
+ process.exit(1);
1395
+ }
1396
+ const { proposal, validation } = staged;
1397
+ console.log(`\n DETAIL: ${proposal.keyword} (${proposal.displayName})\n`);
1398
+ console.log(` Description: ${proposal.description}`);
1399
+ console.log(` Props: ${proposal.props.map(p => `${p.name}:${p.type}${p.required ? '*' : ''}`).join(', ')}`);
1400
+ console.log(` Child types: ${proposal.childTypes.join(', ') || '(none)'}`);
1401
+ console.log(` Codegen tier: ${proposal.codegenTier}`);
1402
+ console.log(` Run ID: ${proposal.evolveRunId}`);
1403
+ if (proposal.parserHints) {
1404
+ console.log(` Parser hints: ${JSON.stringify(proposal.parserHints)}`);
1405
+ }
1406
+ console.log(`\n --- Codegen Source ---`);
1407
+ console.log(proposal.codegenSource);
1408
+ console.log(` --- Instances (${proposal.reason.instances.length}) ---`);
1409
+ for (const inst of proposal.reason.instances.slice(0, 10)) {
1410
+ console.log(` ${inst}`);
1411
+ }
1412
+ if (validation.errors.length > 0) {
1413
+ console.log(`\n --- Validation Errors ---`);
1414
+ for (const err of validation.errors) {
1415
+ console.log(` ${err}`);
1416
+ }
1417
+ }
1418
+ console.log('');
1419
+ process.exit(0);
1420
+ }
1421
+ // Default: interactive review or list mode
1422
+ const stagedV4 = listStagedEvolveV4();
1423
+ const pendingV4 = stagedV4.filter(s => s.status === 'pending');
1424
+ if (pendingV4.length === 0) {
1425
+ console.log(' No pending v4 proposals. Run \'kern evolve:discover <dir>\' to find patterns.');
1426
+ process.exit(0);
1427
+ }
1428
+ const listOnly = args.includes('--list');
1429
+ if (listOnly) {
1430
+ console.log(`\n KERN evolve:review-v4 — ${pendingV4.length} proposal(s)\n`);
1431
+ for (const s of pendingV4) {
1432
+ console.log(formatEvolveV4SplitView(s));
1433
+ console.log('');
1434
+ }
1435
+ process.exit(0);
1436
+ }
1437
+ // Interactive review — walk through each proposal
1438
+ const { createInterface } = await import('readline');
1439
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
1440
+ const ask = (q) => new Promise(res => rl.question(q, res));
1441
+ console.log(`\n KERN evolve:review-v4 — ${pendingV4.length} proposal(s)\n`);
1442
+ for (const staged of pendingV4) {
1443
+ console.log(formatEvolveV4SplitView(staged));
1444
+ console.log('');
1445
+ let decided = false;
1446
+ while (!decided) {
1447
+ const answer = (await ask(' [a]pprove [r]eject [s]kip [d]etail [q]uit > ')).trim().toLowerCase();
1448
+ if (answer === 'a' || answer === 'approve') {
1449
+ const { proposal, validation } = staged;
1450
+ const allOk = validation.schemaOk && validation.keywordOk && validation.parseOk && validation.codegenCompileOk && validation.codegenRunOk;
1451
+ if (!allOk) {
1452
+ console.log(` Cannot approve — validation failed. Use [d]etail to see errors.\n`);
1453
+ continue;
1454
+ }
1455
+ try {
1456
+ const compiledJs = compileCodegenToJS(proposal.codegenSource);
1457
+ const result = graduateNode(proposal, compiledJs);
1458
+ if (result.success) {
1459
+ updateStagedEvolveV4Status(staged.id, 'approved');
1460
+ cleanApprovedEvolveV4(staged.id);
1461
+ console.log(` \u2713 Graduated '${proposal.keyword}'\n`);
1462
+ }
1463
+ else {
1464
+ console.log(` Graduation failed: ${result.error}\n`);
1465
+ }
1466
+ }
1467
+ catch (err) {
1468
+ console.log(` Error: ${err.message}\n`);
1469
+ }
1470
+ decided = true;
1471
+ }
1472
+ else if (answer === 'r' || answer === 'reject') {
1473
+ updateStagedEvolveV4Status(staged.id, 'rejected');
1474
+ cleanRejectedEvolveV4();
1475
+ console.log(` \u2717 Rejected '${staged.proposal.keyword}'\n`);
1476
+ decided = true;
1477
+ }
1478
+ else if (answer === 's' || answer === 'skip') {
1479
+ console.log(` Skipped.\n`);
1480
+ decided = true;
1481
+ }
1482
+ else if (answer === 'd' || answer === 'detail') {
1483
+ const { proposal, validation } = staged;
1484
+ console.log(`\n --- Codegen Source ---`);
1485
+ console.log(proposal.codegenSource);
1486
+ console.log(` --- Instances (${proposal.reason.instances.length}) ---`);
1487
+ for (const inst of proposal.reason.instances.slice(0, 5)) {
1488
+ console.log(` ${inst}`);
1489
+ }
1490
+ if (validation.errors.length > 0) {
1491
+ console.log(` --- Errors ---`);
1492
+ for (const err of validation.errors) {
1493
+ console.log(` ${err}`);
1494
+ }
1495
+ }
1496
+ console.log('');
1497
+ }
1498
+ else if (answer === 'q' || answer === 'quit') {
1499
+ rl.close();
1500
+ process.exit(0);
1501
+ }
1502
+ }
1503
+ }
1504
+ rl.close();
1505
+ console.log(' Review complete.\n');
1506
+ process.exit(0);
1507
+ }
1508
+ // ── kern evolve:test ─────────────────────────────────────────────────
1509
+ if (args[0] === 'evolve:test') {
1510
+ console.log('\n KERN evolve:test — golden test runner\n');
1511
+ const results = runGoldenTests();
1512
+ console.log(formatGoldenTestResults(results));
1513
+ const failed = results.filter(r => !r.pass).length;
1514
+ console.log('');
1515
+ process.exit(failed > 0 ? 1 : 0);
1516
+ }
1517
+ // ── kern evolve:rollback <keyword> [--force] ─────────────────────────
1518
+ if (args[0] === 'evolve:rollback') {
1519
+ const keyword = args[1];
1520
+ if (!keyword || keyword.startsWith('--')) {
1521
+ console.error('Usage: kern evolve:rollback <keyword> [--force]');
1522
+ process.exit(1);
1523
+ }
1524
+ const force = args.includes('--force');
1525
+ const result = rollbackNode(keyword, process.cwd(), force);
1526
+ if (result.success) {
1527
+ console.log(` Rolled back '${keyword}' (moved to .trash/).`);
1528
+ console.log(` Restore with: kern evolve:restore ${keyword}`);
1529
+ }
1530
+ else {
1531
+ console.error(` Failed: ${result.error}`);
1532
+ if (result.usageFiles) {
1533
+ console.error(' Used in:');
1534
+ for (const f of result.usageFiles.slice(0, 5)) {
1535
+ console.error(` ${relative(process.cwd(), f)}`);
1536
+ }
1537
+ }
1538
+ process.exit(1);
1539
+ }
1540
+ process.exit(0);
1541
+ }
1542
+ // ── kern evolve:restore <keyword> ────────────────────────────────────
1543
+ if (args[0] === 'evolve:restore') {
1544
+ const keyword = args[1];
1545
+ if (!keyword) {
1546
+ console.error('Usage: kern evolve:restore <keyword>');
1547
+ process.exit(1);
1548
+ }
1549
+ const result = restoreNode(keyword);
1550
+ if (result.success) {
1551
+ console.log(` Restored '${keyword}'.`);
1552
+ }
1553
+ else {
1554
+ console.error(` Failed: ${result.error}`);
1555
+ process.exit(1);
1556
+ }
1557
+ process.exit(0);
1558
+ }
1559
+ // ── kern evolve:list ─────────────────────────────────────────────────
1560
+ if (args[0] === 'evolve:list') {
1561
+ const manifest = readEvolvedManifest();
1562
+ if (!manifest || Object.keys(manifest.nodes).length === 0) {
1563
+ console.log('\n No evolved nodes graduated. Run \'kern evolve:discover\' to start.\n');
1564
+ process.exit(0);
1565
+ }
1566
+ console.log(`\n KERN evolved nodes — ${Object.keys(manifest.nodes).length} graduated\n`);
1567
+ for (const [keyword, entry] of Object.entries(manifest.nodes)) {
1568
+ console.log(` ${keyword} — ${entry.displayName} (graduated ${entry.graduatedAt.split('T')[0]} by ${entry.graduatedBy})`);
1569
+ }
1570
+ console.log('');
1571
+ process.exit(0);
1572
+ }
1573
+ // ── kern evolve:promote <keyword> ────────────────────────────────────
1574
+ if (args[0] === 'evolve:promote') {
1575
+ const promoteKeyword = args[1];
1576
+ if (!promoteKeyword || promoteKeyword.startsWith('--')) {
1577
+ console.error('Usage: kern evolve:promote <keyword>');
1578
+ console.error(' Reads codegen from .kern/evolved/<keyword>/ and outputs what to add to core.');
1579
+ process.exit(1);
1580
+ }
1581
+ const result = promoteNode(promoteKeyword);
1582
+ if (!result.success) {
1583
+ console.error(` Failed: ${result.error}`);
1584
+ process.exit(1);
1585
+ }
1586
+ const fnName = 'generate' + promoteKeyword.split('-').map(w => w[0].toUpperCase() + w.slice(1)).join('');
1587
+ console.log(`\n KERN evolve:promote — ${promoteKeyword}\n`);
1588
+ console.log(' To promote this node to core, apply these changes:\n');
1589
+ console.log(` 1. Add '${promoteKeyword}' to NODE_TYPES in packages/core/src/spec.ts`);
1590
+ console.log(` 2. Create packages/core/src/generators/${fnName}.ts with:`);
1591
+ console.log(` ──────────────────────────────`);
1592
+ for (const line of (result.codegenTs || '').split('\n').slice(0, 20)) {
1593
+ console.log(` ${line}`);
1594
+ }
1595
+ if ((result.codegenTs || '').split('\n').length > 20) {
1596
+ console.log(` ... (${(result.codegenTs || '').split('\n').length - 20} more lines)`);
1597
+ }
1598
+ console.log(` ──────────────────────────────`);
1599
+ console.log(` 3. Add case '${promoteKeyword}': return ${fnName}(node); to generateCoreNode() in codegen-core.ts`);
1600
+ if (result.goldenKern) {
1601
+ console.log(` 4. Move golden test to packages/core/tests/`);
1602
+ }
1603
+ console.log(` 5. Run: kern evolve:rollback ${promoteKeyword} --force`);
1604
+ console.log('');
1605
+ process.exit(0);
1606
+ }
1607
+ // ── kern evolve:backfill <keyword> --target=<target> [--provider=...] ──
1608
+ if (args[0] === 'evolve:backfill') {
1609
+ const backfillKeyword = args[1];
1610
+ if (!backfillKeyword || backfillKeyword.startsWith('--')) {
1611
+ console.error('Usage: kern evolve:backfill <keyword> --target=<target> [--provider=openai|anthropic|ollama]');
1612
+ process.exit(1);
1613
+ }
1614
+ const backfillTarget = args.find(a => a.startsWith('--target='))?.split('=')[1];
1615
+ if (!backfillTarget) {
1616
+ console.error(' --target=<target> is required');
1617
+ process.exit(1);
1618
+ }
1619
+ const def = readNodeDefinition(backfillKeyword);
1620
+ if (!def) {
1621
+ console.error(` Node '${backfillKeyword}' is not graduated.`);
1622
+ process.exit(1);
1623
+ }
1624
+ // Read current codegen source
1625
+ const codegenTsPath = resolve('.kern', 'evolved', backfillKeyword, 'codegen.ts');
1626
+ if (!existsSync(codegenTsPath)) {
1627
+ console.error(` Missing codegen.ts for '${backfillKeyword}'`);
1628
+ process.exit(1);
1629
+ }
1630
+ const codegenSource = readFileSync(codegenTsPath, 'utf-8');
1631
+ const templateKernPath = resolve('.kern', 'evolved', backfillKeyword, 'template.kern');
1632
+ const kernExample = existsSync(templateKernPath) ? readFileSync(templateKernPath, 'utf-8') : '';
1633
+ const expectedOutputPath = resolve('.kern', 'evolved', backfillKeyword, 'expected-output.ts');
1634
+ const expectedOutput = existsSync(expectedOutputPath) ? readFileSync(expectedOutputPath, 'utf-8') : '';
1635
+ console.log(`\n KERN evolve:backfill — ${backfillKeyword} → ${backfillTarget}\n`);
1636
+ const providerArg = args.find(a => a.startsWith('--provider='))?.split('=')[1];
1637
+ let provider;
1638
+ try {
1639
+ provider = createLLMProvider({ provider: providerArg });
1640
+ }
1641
+ catch (err) {
1642
+ console.error(` ${err.message}`);
1643
+ process.exit(1);
1644
+ }
1645
+ console.log(` Provider: ${provider.name}`);
1646
+ const prompt = buildBackfillPrompt(backfillKeyword, {
1647
+ props: def.props,
1648
+ childTypes: def.childTypes,
1649
+ kernExample,
1650
+ codegenSource,
1651
+ expectedOutput,
1652
+ }, backfillTarget);
1653
+ try {
1654
+ const response = await provider.complete(prompt);
1655
+ // Parse JSON response
1656
+ let json = response.trim();
1657
+ const fenceMatch = json.match(/```(?:json)?\s*\n?([\s\S]*?)```/);
1658
+ if (fenceMatch)
1659
+ json = fenceMatch[1].trim();
1660
+ const objStart = json.indexOf('{');
1661
+ const objEnd = json.lastIndexOf('}');
1662
+ if (objStart !== -1 && objEnd > objStart)
1663
+ json = json.slice(objStart, objEnd + 1);
1664
+ const parsed = JSON.parse(json);
1665
+ if (typeof parsed !== 'object' || parsed === null) {
1666
+ console.error(' LLM response is not a JSON object');
1667
+ process.exit(1);
1668
+ }
1669
+ const targetCodegen = typeof parsed.codegenSource === 'string' ? parsed.codegenSource : undefined;
1670
+ if (!targetCodegen) {
1671
+ console.error(' LLM did not return codegenSource');
1672
+ process.exit(1);
1673
+ }
1674
+ // Write target override
1675
+ const targetsDir = resolve('.kern', 'evolved', backfillKeyword, 'targets');
1676
+ mkdirSync(targetsDir, { recursive: true });
1677
+ writeFileSync(resolve(targetsDir, `${backfillTarget}.js`), targetCodegen);
1678
+ console.log(` Written: .kern/evolved/${backfillKeyword}/targets/${backfillTarget}.js`);
1679
+ if (parsed.expectedOutput) {
1680
+ console.log(` Expected output preview:`);
1681
+ for (const line of parsed.expectedOutput.split('\n').slice(0, 10)) {
1682
+ console.log(` ${line}`);
1683
+ }
1684
+ }
1685
+ console.log(`\n Review the generated codegen before using in production.`);
1686
+ }
1687
+ catch (err) {
1688
+ console.error(` Error: ${err.message}`);
1689
+ process.exit(1);
1690
+ }
1691
+ console.log('');
1692
+ process.exit(0);
1693
+ }
1694
+ // ── kern evolve:prune [--dry-run] [--days=N] ─────────────────────────
1695
+ if (args[0] === 'evolve:prune') {
1696
+ const dryRun = args.includes('--dry-run');
1697
+ const daysArg = args.find(a => a.startsWith('--days='))?.split('=')[1];
1698
+ const thresholdDays = daysArg ? Number(daysArg) : 90;
1699
+ console.log(`\n KERN evolve:prune — removing unused nodes (>${thresholdDays}d)\n`);
1700
+ const results = pruneNodes(process.cwd(), thresholdDays, dryRun);
1701
+ if (results.length === 0) {
1702
+ console.log(' No nodes eligible for pruning.');
1703
+ process.exit(0);
1704
+ }
1705
+ for (const r of results) {
1706
+ if (dryRun) {
1707
+ console.log(` Would prune: ${r.keyword} (${r.daysUnused}d unused)`);
1708
+ }
1709
+ else if (r.pruned) {
1710
+ console.log(` Pruned: ${r.keyword} (${r.daysUnused}d unused) → .trash/`);
1711
+ }
1712
+ else {
1713
+ console.log(` Failed: ${r.keyword} — ${r.error}`);
1714
+ }
1715
+ }
1716
+ if (dryRun) {
1717
+ console.log(`\n Dry run — no changes made. Remove --dry-run to prune.`);
1718
+ }
1719
+ console.log('');
1720
+ process.exit(0);
1721
+ }
1722
+ // ── kern evolve:migrate ──────────────────────────────────────────────
1723
+ if (args[0] === 'evolve:migrate') {
1724
+ console.log(`\n KERN evolve:migrate — checking for keyword collisions\n`);
1725
+ const collisions = detectCollisions(NODE_TYPES);
1726
+ if (collisions.length === 0) {
1727
+ console.log(' No collisions. All evolved nodes are compatible with core.');
1728
+ process.exit(0);
1729
+ }
1730
+ console.log(` Found ${collisions.length} collision(s):\n`);
1731
+ for (const c of collisions) {
1732
+ console.log(` ${c.keyword} (graduated ${c.graduatedAt.split('T')[0]})`);
1733
+ console.log(` This keyword now exists in core NODE_TYPES.`);
1734
+ console.log(` Options:`);
1735
+ console.log(` kern evolve:migrate --rename=${c.keyword} --to=<new-name>`);
1736
+ console.log(` kern evolve:migrate --remove=${c.keyword} (core version supersedes)`);
1737
+ console.log('');
1738
+ }
1739
+ // Handle --rename and --remove flags
1740
+ const renameArg = args.find(a => a.startsWith('--rename='))?.split('=')[1];
1741
+ const toArg = args.find(a => a.startsWith('--to='))?.split('=')[1];
1742
+ const removeArg = args.find(a => a.startsWith('--remove='))?.split('=')[1];
1743
+ if (renameArg && toArg) {
1744
+ const result = renameEvolvedNode(renameArg, toArg);
1745
+ if (result.success) {
1746
+ console.log(` Renamed '${renameArg}' → '${toArg}'`);
1747
+ console.log(` Update your .kern files: replace '${renameArg}' with '${toArg}'.`);
1748
+ }
1749
+ else {
1750
+ console.error(` Rename failed: ${result.error}`);
1751
+ process.exit(1);
1752
+ }
1753
+ }
1754
+ if (removeArg) {
1755
+ const result = rollbackNode(removeArg, process.cwd(), true);
1756
+ if (result.success) {
1757
+ console.log(` Removed evolved '${removeArg}' — core version will be used.`);
1758
+ }
1759
+ else {
1760
+ console.error(` Remove failed: ${result.error}`);
1761
+ process.exit(1);
1762
+ }
1763
+ }
1764
+ console.log('');
1765
+ process.exit(0);
1766
+ }
1767
+ // ── kern evolve:rebuild ─────────────────────────────────────────────
1768
+ if (args[0] === 'evolve:rebuild') {
1769
+ console.log(`\n KERN evolve:rebuild — rebuilding manifest from disk\n`);
1770
+ const result = rebuildEvolvedManifest();
1771
+ if (result.errors.length > 0) {
1772
+ for (const err of result.errors) {
1773
+ console.log(` ⚠ ${err}`);
1774
+ }
1775
+ console.log('');
1776
+ }
1777
+ if (result.rebuilt === 0 && result.errors.length > 0) {
1778
+ console.error(' No nodes rebuilt.');
1779
+ process.exit(1);
1780
+ }
1781
+ console.log(` manifest.json rebuilt with ${result.rebuilt} node(s).`);
1782
+ console.log('');
1783
+ process.exit(0);
1784
+ }
1785
+ // ── kern confidence <file.kern|dir> ──────────────────────────────────
1786
+ if (args[0] === 'confidence') {
1787
+ const confInput = args[1];
1788
+ if (!confInput) {
1789
+ console.error('Usage: kern confidence <file.kern|dir>');
1790
+ console.error(' Builds and displays the confidence graph for .kern file(s).');
1791
+ process.exit(1);
1792
+ }
1793
+ const confPath = resolve(confInput);
1794
+ if (!existsSync(confPath)) {
1795
+ console.error(`Not found: ${confInput}`);
1796
+ process.exit(1);
1797
+ }
1798
+ const { buildConfidenceGraph, buildMultiFileConfidenceGraph, flattenIR, lintMultiFileConfidenceGraph } = await import('@kernlang/review');
1799
+ const confStat = statSync(confPath);
1800
+ const isDir = confStat.isDirectory();
1801
+ // Collect .kern files
1802
+ const kernFiles = isDir ? findKernFiles(confPath) : [confPath];
1803
+ if (kernFiles.length === 0) {
1804
+ console.log(' No .kern files found.');
1805
+ process.exit(0);
1806
+ }
1807
+ // Parse all files, skip those without confidence (fast early exit)
1808
+ const fileMap = new Map();
1809
+ for (const file of kernFiles.sort()) {
1810
+ const source = readFileSync(file, 'utf-8');
1811
+ if (!source.includes('confidence='))
1812
+ continue;
1813
+ const ast = parse(source);
1814
+ fileMap.set(file, flattenIR(ast));
1815
+ }
1816
+ if (fileMap.size === 0) {
1817
+ console.log(`\n No confidence declarations found in ${isDir ? confInput : basename(confInput)}`);
1818
+ process.exit(0);
1819
+ }
1820
+ // Build graph (single-file or multi-file)
1821
+ const graph = fileMap.size === 1
1822
+ ? buildConfidenceGraph([...fileMap.values()][0])
1823
+ : buildMultiFileConfidenceGraph(fileMap);
1824
+ const isMulti = fileMap.size > 1;
1825
+ console.log(`\n Confidence Graph (${graph.nodes.size} nodes, ${graph.topoOrder.length} resolved${isMulti ? `, ${fileMap.size} files` : ''}):\n`);
1826
+ if (isMulti) {
1827
+ // Group by source file
1828
+ const byFile = new Map();
1829
+ for (const cnode of graph.nodes.values()) {
1830
+ const file = cnode.sourceFile || 'unknown';
1831
+ if (!byFile.has(file))
1832
+ byFile.set(file, []);
1833
+ byFile.get(file).push(cnode);
1834
+ }
1835
+ for (const [file, nodes] of byFile) {
1836
+ const rel = relative(process.cwd(), file) || file;
1837
+ console.log(` ${rel} (${nodes.length} nodes):`);
1838
+ for (const cnode of nodes) {
1839
+ const resolvedStr = cnode.resolved !== null ? cnode.resolved.toFixed(2) : 'null';
1840
+ const specStr = cnode.spec.kind === 'literal'
1841
+ ? 'declared'
1842
+ : `from: ${cnode.spec.sources?.join(', ')}, ${cnode.spec.strategy}`;
1843
+ const crossFile = cnode.spec.sources?.some(s => {
1844
+ const src = graph.nodes.get(s);
1845
+ return src && src.sourceFile !== cnode.sourceFile;
1846
+ });
1847
+ const crossTag = crossFile ? ' [cross-file]' : '';
1848
+ const cycleTag = cnode.inCycle ? ' [CYCLE]' : '';
1849
+ console.log(` ${cnode.name.padEnd(20)} ${resolvedStr.padEnd(8)} (${specStr})${crossTag}${cycleTag}`);
1850
+ }
1851
+ console.log('');
1852
+ }
1853
+ }
1854
+ else {
1855
+ for (const [name, cnode] of graph.nodes) {
1856
+ const resolvedStr = cnode.resolved !== null ? cnode.resolved.toFixed(2) : 'null';
1857
+ const specStr = cnode.spec.kind === 'literal'
1858
+ ? 'declared'
1859
+ : `from: ${cnode.spec.sources?.join(', ')}, ${cnode.spec.strategy}`;
1860
+ const cycleTag = cnode.inCycle ? ' [CYCLE]' : '';
1861
+ console.log(` ${name.padEnd(20)} ${resolvedStr.padEnd(8)} (${specStr})${cycleTag}`);
1862
+ }
1863
+ }
1864
+ // Unresolved needs
1865
+ const unresolvedNeeds = [];
1866
+ for (const [name, cnode] of graph.nodes) {
1867
+ for (const need of cnode.needs) {
1868
+ if (!need.resolved) {
1869
+ unresolvedNeeds.push({ name, what: need.what, wouldRaiseTo: need.wouldRaiseTo });
1870
+ }
1871
+ }
1872
+ }
1873
+ if (unresolvedNeeds.length > 0) {
1874
+ console.log(` Unresolved needs (${unresolvedNeeds.length}):`);
1875
+ for (const n of unresolvedNeeds) {
1876
+ const raise = n.wouldRaiseTo !== undefined ? ` → would raise to ${n.wouldRaiseTo}` : '';
1877
+ console.log(` ${n.name}: "${n.what}"${raise}`);
1878
+ }
1879
+ }
1880
+ if (graph.cycles.length > 0) {
1881
+ console.log(`\n Cycles (${graph.cycles.length}):`);
1882
+ for (const cycle of graph.cycles) {
1883
+ console.log(` ${cycle.join(' → ')}`);
1884
+ }
1885
+ }
1886
+ // Duplicates (multi-file only)
1887
+ const dupes = 'duplicates' in graph ? graph.duplicates : [];
1888
+ if (dupes.length > 0) {
1889
+ console.log(`\n Duplicate names (${dupes.length}):`);
1890
+ for (const dup of dupes) {
1891
+ console.log(` ${dup.name}: ${dup.files.map((f) => relative(process.cwd(), f) || f).join(', ')}`);
1892
+ }
1893
+ }
1894
+ console.log('');
1895
+ process.exit(0);
1896
+ }
700
1897
  // ── Standard transpile mode ────────────────────────────────────────────
701
1898
  const inputFile = args.find(a => !a.startsWith('--'));
702
1899
  if (!inputFile) {
@@ -707,7 +1904,16 @@ if (!inputFile) {
707
1904
  console.log(' compile <dir|file> --outdir=<dir> Compile .kern → .ts (core nodes)');
708
1905
  console.log(' scan [--force] [--dry-run] Detect project → generate kern.config.ts');
709
1906
  console.log(' init-templates [--force] [--dry-run] Scan deps → scaffold template .kern files');
710
- console.log(' review <file.ts|dir> [options] Analyze TS infer .kern coverage + review');
1907
+ console.log(' review <file.ts|dir> [options] Static analysis, Cognitive Complexity & CI Gate');
1908
+ console.log(' evolve <dir|file> [options] Detect gaps → propose templates');
1909
+ console.log(' evolve:review [options] Review staged template proposals');
1910
+ console.log(' evolve:review-v4 [options] Review & graduate v4 node proposals');
1911
+ console.log(' evolve:promote <keyword> Show steps to move evolved → core');
1912
+ console.log(' evolve:backfill <kw> --target=<t> LLM generates target-specific codegen');
1913
+ console.log(' evolve:prune [--dry-run] [--days=N] Remove unused nodes (default 90d)');
1914
+ console.log(' evolve:migrate Detect & resolve keyword collisions');
1915
+ console.log(' evolve:rebuild Rebuild manifest.json from disk definitions');
1916
+ console.log(' confidence <file.kern> Display confidence graph for a .kern file');
711
1917
  console.log('');
712
1918
  console.log('Targets:');
713
1919
  console.log(' nextjs Next.js App Router (default)');
@@ -754,6 +1960,8 @@ else {
754
1960
  }
755
1961
  // Load templates before transpile
756
1962
  loadTemplates(config);
1963
+ // Load evolved nodes (v4) — graduated nodes from .kern/evolved/
1964
+ loadEvolvedNodes(process.cwd(), args.includes('--verify'));
757
1965
  // CLI flags override config — target
758
1966
  const cliTarget = args.find(a => a.startsWith('--target='))?.split('=')[1];
759
1967
  if (cliTarget) {
@@ -834,15 +2042,19 @@ const result = target === 'native'
834
2042
  ? transpileTailwind(ast, config)
835
2043
  : target === 'express'
836
2044
  ? transpileExpress(ast, config)
837
- : target === 'cli'
838
- ? transpileCliApp(ast, config)
839
- : target === 'terminal'
840
- ? transpileTerminal(ast, config)
841
- : target === 'vue'
842
- ? transpileVue(ast, config)
843
- : target === 'nuxt'
844
- ? transpileNuxt(ast, config)
845
- : transpileNextjs(ast, config);
2045
+ : target === 'fastapi'
2046
+ ? transpileFastAPI(ast, config)
2047
+ : target === 'cli'
2048
+ ? transpileCliApp(ast, config)
2049
+ : target === 'terminal'
2050
+ ? transpileTerminal(ast, config)
2051
+ : target === 'ink'
2052
+ ? transpileInk(ast, config)
2053
+ : target === 'vue'
2054
+ ? transpileVue(ast, config)
2055
+ : target === 'nuxt'
2056
+ ? transpileNuxt(ast, config)
2057
+ : transpileNextjs(ast, config);
846
2058
  const outDir = resolve(dirname(inputFile), config.output.outDir);
847
2059
  const isStructured = config.structure !== 'flat' && result.artifacts && result.artifacts.length > 0;
848
2060
  if (isStructured) {
@@ -859,8 +2071,10 @@ if (isStructured) {
859
2071
  }
860
2072
  else {
861
2073
  // Flat output: single file
862
- const outExt = (target === 'vue' || target === 'nuxt') ? '.vue'
863
- : (target === 'express' || target === 'cli' || target === 'terminal') ? '.ts' : '.tsx';
2074
+ const outExt = target === 'fastapi' ? '.py'
2075
+ : (target === 'vue' || target === 'nuxt') ? '.vue'
2076
+ : (target === 'express' || target === 'cli' || target === 'terminal') ? '.ts'
2077
+ : '.tsx';
864
2078
  const outFile = resolve(outDir, `${name}${outExt}`);
865
2079
  mkdirSync(dirname(outFile), { recursive: true });
866
2080
  writeFileSync(outFile, result.code);
@@ -873,7 +2087,7 @@ else {
873
2087
  }
874
2088
  console.log(`Transpiled: ${inputFile} → ${outFile}`);
875
2089
  }
876
- const targetNames = { native: 'React Native', web: 'React (inline)', tailwind: 'React + Tailwind', nextjs: 'Next.js App Router', express: 'Express TypeScript', cli: 'Commander.js CLI', terminal: 'ANSI Terminal', vue: 'Vue 3 SFC', nuxt: 'Nuxt 3' };
2090
+ const targetNames = { native: 'React Native', web: 'React (inline)', tailwind: 'React + Tailwind', nextjs: 'Next.js App Router', express: 'Express TypeScript', fastapi: 'FastAPI Python', cli: 'Commander.js CLI', terminal: 'ANSI Terminal', ink: 'Ink (React for Terminals)', vue: 'Vue 3 SFC', nuxt: 'Nuxt 3' };
877
2091
  console.log(`Target: ${targetNames[target] || target}`);
878
2092
  if (config.structure !== 'flat') {
879
2093
  const structureNames = { bulletproof: 'Bulletproof React', atomic: 'Atomic Design', kern: 'KERN Native' };
@@ -934,6 +2148,26 @@ function minifyKern(node) {
934
2148
  }
935
2149
  return head;
936
2150
  }
2151
+ function collectTsFilesFlat(dirPath, recursive) {
2152
+ const files = [];
2153
+ for (const entry of readdirSync(dirPath)) {
2154
+ const full = resolve(dirPath, entry);
2155
+ const s = statSync(full);
2156
+ if (s.isDirectory() && recursive && !entry.startsWith('.') && entry !== 'node_modules' && entry !== 'dist') {
2157
+ files.push(...collectTsFilesFlat(full, true));
2158
+ }
2159
+ else if ((entry.endsWith('.ts') || entry.endsWith('.tsx')) && !entry.endsWith('.d.ts') && !entry.endsWith('.test.ts')) {
2160
+ files.push(full);
2161
+ }
2162
+ else if (entry.endsWith('.kern')) {
2163
+ files.push(full);
2164
+ }
2165
+ else if (entry.endsWith('.py') && !entry.startsWith('test_') && !entry.endsWith('_test.py')) {
2166
+ files.push(full);
2167
+ }
2168
+ }
2169
+ return files;
2170
+ }
937
2171
  function prettyKern(node, indent = '') {
938
2172
  const type = node.type;
939
2173
  const props = node.props || {};