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