@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.
- package/dist/cli.js +1301 -67
- package/dist/cli.js.map +1 -1
- 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,
|
|
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
|
|
114
|
-
const
|
|
115
|
-
|
|
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 === '
|
|
189
|
-
?
|
|
190
|
-
: target === '
|
|
191
|
-
?
|
|
192
|
-
: target === '
|
|
193
|
-
?
|
|
194
|
-
: target === '
|
|
195
|
-
?
|
|
196
|
-
:
|
|
197
|
-
|
|
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 =
|
|
208
|
-
: (target === '
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
471
|
-
const
|
|
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> [--
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
694
|
+
reports.push(reviewFile(f, reviewConfig));
|
|
695
|
+
}
|
|
696
|
+
catch (e) {
|
|
697
|
+
console.error(` Review error in ${f}: ${e.message}`);
|
|
576
698
|
}
|
|
577
|
-
|
|
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
|
|
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
|
-
// --
|
|
594
|
-
if (
|
|
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
|
-
|
|
597
|
-
|
|
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('
|
|
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
|
-
|
|
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
|
-
|
|
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]
|
|
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 === '
|
|
838
|
-
?
|
|
839
|
-
: target === '
|
|
840
|
-
?
|
|
841
|
-
: target === '
|
|
842
|
-
?
|
|
843
|
-
: target === '
|
|
844
|
-
?
|
|
845
|
-
:
|
|
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 =
|
|
863
|
-
: (target === '
|
|
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 || {};
|