@kernlang/cli 3.1.6 → 3.1.8
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 +40 -2440
- package/dist/cli.js.map +1 -1
- package/dist/commands/compile.d.ts +1 -0
- package/dist/commands/compile.js +360 -0
- package/dist/commands/compile.js.map +1 -0
- package/dist/commands/confidence.d.ts +1 -0
- package/dist/commands/confidence.js +105 -0
- package/dist/commands/confidence.js.map +1 -0
- package/dist/commands/dev.d.ts +7 -0
- package/dist/commands/dev.js +17 -0
- package/dist/commands/dev.js.map +1 -0
- package/dist/commands/evolve/backfill.d.ts +1 -0
- package/dist/commands/evolve/backfill.js +80 -0
- package/dist/commands/evolve/backfill.js.map +1 -0
- package/dist/commands/evolve/discover.d.ts +1 -0
- package/dist/commands/evolve/discover.js +151 -0
- package/dist/commands/evolve/discover.js.map +1 -0
- package/dist/commands/evolve/index.d.ts +1 -0
- package/dist/commands/evolve/index.js +40 -0
- package/dist/commands/evolve/index.js.map +1 -0
- package/dist/commands/evolve/lifecycle.d.ts +8 -0
- package/dist/commands/evolve/lifecycle.js +190 -0
- package/dist/commands/evolve/lifecycle.js.map +1 -0
- package/dist/commands/evolve/main.d.ts +1 -0
- package/dist/commands/evolve/main.js +76 -0
- package/dist/commands/evolve/main.js.map +1 -0
- package/dist/commands/evolve/review-v4.d.ts +1 -0
- package/dist/commands/evolve/review-v4.js +181 -0
- package/dist/commands/evolve/review-v4.js.map +1 -0
- package/dist/commands/evolve/review.d.ts +1 -0
- package/dist/commands/evolve/review.js +63 -0
- package/dist/commands/evolve/review.js.map +1 -0
- package/dist/commands/import.d.ts +1 -0
- package/dist/commands/import.js +101 -0
- package/dist/commands/import.js.map +1 -0
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.js +154 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/review.d.ts +1 -0
- package/dist/commands/review.js +881 -0
- package/dist/commands/review.js.map +1 -0
- package/dist/commands/scan.d.ts +2 -0
- package/dist/commands/scan.js +89 -0
- package/dist/commands/scan.js.map +1 -0
- package/dist/commands/schema.d.ts +1 -0
- package/dist/commands/schema.js +6 -0
- package/dist/commands/schema.js.map +1 -0
- package/dist/commands/test.d.ts +1 -0
- package/dist/commands/test.js +184 -0
- package/dist/commands/test.js.map +1 -0
- package/dist/commands/transpile.d.ts +2 -0
- package/dist/commands/transpile.js +280 -0
- package/dist/commands/transpile.js.map +1 -0
- package/dist/shared.d.ts +51 -0
- package/dist/shared.js +459 -0
- package/dist/shared.js.map +1 -0
- package/dist/transpiler-cli.d.ts +1 -1
- package/dist/transpiler-cli.js +10 -9
- package/dist/transpiler-cli.js.map +1 -1
- package/package.json +13 -13
package/dist/cli.js
CHANGED
|
@@ -1,2450 +1,50 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import { transpileTerminal, transpileInk } from '@kernlang/terminal';
|
|
14
|
-
import { transpileVue, transpileNuxt } from '@kernlang/vue';
|
|
15
|
-
import { collectLanguageMetrics } from '@kernlang/metrics';
|
|
16
|
-
import { reviewFile, reviewGraph, resolveImportGraph, formatReport, formatSARIF, formatSummary, checkEnforcement, formatEnforcement, exportKernIR, buildLLMPrompt, dedup, runESLint, runTSCDiagnosticsFromPaths, linkToNodes, runLLMReview, isLLMAvailable, analyzeTaint, checkSpecFiles, specViolationsToFindings, clearReviewCache, getRuleRegistry } from '@kernlang/review';
|
|
17
|
-
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';
|
|
2
|
+
import { runCompile } from './commands/compile.js';
|
|
3
|
+
import { runConfidence } from './commands/confidence.js';
|
|
4
|
+
import { runDev } from './commands/dev.js';
|
|
5
|
+
import { routeEvolve } from './commands/evolve/index.js';
|
|
6
|
+
import { runImport } from './commands/import.js';
|
|
7
|
+
import { runInit } from './commands/init.js';
|
|
8
|
+
import { runReview } from './commands/review.js';
|
|
9
|
+
import { runInitTemplates, runScan } from './commands/scan.js';
|
|
10
|
+
import { runSchema } from './commands/schema.js';
|
|
11
|
+
import { runTest } from './commands/test.js';
|
|
12
|
+
import { printHelp, runTranspile } from './commands/transpile.js';
|
|
18
13
|
const args = process.argv.slice(2);
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
14
|
+
const cmd = args[0];
|
|
15
|
+
// ── Command registry ─────────────────────────────────────────────────────
|
|
16
|
+
const COMMANDS = {
|
|
17
|
+
dev: runDev,
|
|
18
|
+
compile: runCompile,
|
|
19
|
+
init: runInit,
|
|
20
|
+
test: runTest,
|
|
21
|
+
scan: runScan,
|
|
22
|
+
'init-templates': runInitTemplates,
|
|
23
|
+
import: runImport,
|
|
24
|
+
review: runReview,
|
|
25
|
+
confidence: runConfidence,
|
|
26
|
+
schema: runSchema,
|
|
27
|
+
};
|
|
28
|
+
async function main() {
|
|
29
|
+
// Route evolve commands (evolve + evolve:*)
|
|
30
|
+
if (cmd === 'evolve' || cmd?.startsWith('evolve:')) {
|
|
31
|
+
await routeEvolve(args);
|
|
22
32
|
return;
|
|
23
|
-
const prefix = file ? `${file}: ` : '';
|
|
24
|
-
for (const diagnostic of diagnostics) {
|
|
25
|
-
const tag = diagnostic.severity === 'error' ? 'ERROR' : diagnostic.severity === 'warning' ? 'WARN' : 'INFO';
|
|
26
|
-
console.error(` ${prefix}[${tag}] ${diagnostic.code}: ${diagnostic.message}`);
|
|
27
33
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
return result.root;
|
|
33
|
-
}
|
|
34
|
-
// ── kern dev <dir|file> [--target=...] [--outdir=...] ─────────────────
|
|
35
|
-
if (args[0] === 'dev') {
|
|
36
|
-
const devInput = args[1];
|
|
37
|
-
if (!devInput) {
|
|
38
|
-
console.error('Usage: kern dev <file.kern|dir> [--target=nextjs] [--outdir=<dir>]');
|
|
39
|
-
process.exit(1);
|
|
40
|
-
}
|
|
41
|
-
const inputPath = resolve(devInput);
|
|
42
|
-
const stat = existsSync(inputPath) ? statSync(inputPath) : null;
|
|
43
|
-
if (!stat) {
|
|
44
|
-
console.error(`Not found: ${devInput}`);
|
|
45
|
-
process.exit(1);
|
|
46
|
-
}
|
|
47
|
-
const watchDir = stat.isDirectory() ? inputPath : dirname(inputPath);
|
|
48
|
-
const watchPattern = stat.isDirectory() ? undefined : basename(inputPath);
|
|
49
|
-
// Load config
|
|
50
|
-
const devConfig = loadConfig();
|
|
51
|
-
// CLI overrides
|
|
52
|
-
const devCliTarget = args.find(a => a.startsWith('--target='))?.split('=')[1];
|
|
53
|
-
if (devCliTarget) {
|
|
54
|
-
if (!VALID_TARGETS.includes(devCliTarget)) {
|
|
55
|
-
console.error(`Unknown target: '${devCliTarget}'.`);
|
|
56
|
-
process.exit(1);
|
|
57
|
-
}
|
|
58
|
-
devConfig.target = devCliTarget;
|
|
59
|
-
}
|
|
60
|
-
const devOutDir = args.find(a => a.startsWith('--outdir='))?.split('=')[1];
|
|
61
|
-
// Auto-detect framework versions from nearest package.json (walk up from watchDir)
|
|
62
|
-
const pkgPath = findNearestPackageJson(watchDir);
|
|
63
|
-
if (pkgPath && Object.keys(devConfig.frameworkVersions).length === 0) {
|
|
64
|
-
try {
|
|
65
|
-
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
66
|
-
const detected = detectVersionsFromPackageJson(pkg);
|
|
67
|
-
if (detected.tailwind || detected.nextjs) {
|
|
68
|
-
devConfig.frameworkVersions = { ...devConfig.frameworkVersions, ...detected };
|
|
69
|
-
const parts = [];
|
|
70
|
-
if (detected.tailwind)
|
|
71
|
-
parts.push(`Tailwind ${detected.tailwind}`);
|
|
72
|
-
if (detected.nextjs)
|
|
73
|
-
parts.push(`Next.js ${detected.nextjs}`);
|
|
74
|
-
console.log(` Auto-detected: ${parts.join(', ')}`);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
catch {
|
|
78
|
-
// Intentional: package.json detection is optional — build continues without it
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
// Load templates before compilation
|
|
82
|
-
loadTemplates(devConfig);
|
|
83
|
-
// Load evolved nodes (v4) — graduated nodes from .kern/evolved/
|
|
84
|
-
const evolvedResult = loadEvolvedNodes(process.cwd(), args.includes('--verify'));
|
|
85
|
-
if (evolvedResult.loaded > 0) {
|
|
86
|
-
console.log(` Evolved nodes: ${evolvedResult.loaded} loaded`);
|
|
87
|
-
}
|
|
88
|
-
console.log(`\n KERN dev — watching for changes`);
|
|
89
|
-
console.log(` Target: ${devConfig.target}`);
|
|
90
|
-
console.log(` Watch: ${relative(process.cwd(), watchDir) || '.'}`);
|
|
91
|
-
console.log('');
|
|
92
|
-
// Initial build of all .kern files
|
|
93
|
-
const initialFiles = findKernFiles(watchDir, watchPattern);
|
|
94
|
-
for (const file of initialFiles) {
|
|
95
|
-
transpileAndWrite(file, devConfig, devOutDir, watchDir);
|
|
96
|
-
}
|
|
97
|
-
if (initialFiles.length > 0) {
|
|
98
|
-
console.log(` ${initialFiles.length} file(s) compiled.\n`);
|
|
99
|
-
}
|
|
100
|
-
// Watch for changes
|
|
101
|
-
import('chokidar').then(({ watch }) => {
|
|
102
|
-
const globPattern = watchPattern
|
|
103
|
-
? resolve(watchDir, watchPattern)
|
|
104
|
-
: resolve(watchDir, '**/*.kern');
|
|
105
|
-
const watcher = watch(globPattern, {
|
|
106
|
-
ignoreInitial: true,
|
|
107
|
-
awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 50 },
|
|
108
|
-
});
|
|
109
|
-
watcher.on('change', (filePath) => {
|
|
110
|
-
const rel = relative(process.cwd(), filePath);
|
|
111
|
-
const start = performance.now();
|
|
112
|
-
try {
|
|
113
|
-
transpileAndWrite(filePath, devConfig, devOutDir, watchDir);
|
|
114
|
-
const ms = Math.round(performance.now() - start);
|
|
115
|
-
console.log(` ${rel} → compiled (${ms}ms)`);
|
|
116
|
-
}
|
|
117
|
-
catch (err) {
|
|
118
|
-
console.error(` ${rel} → ERROR: ${err.message}`);
|
|
119
|
-
}
|
|
120
|
-
});
|
|
121
|
-
watcher.on('add', (filePath) => {
|
|
122
|
-
const rel = relative(process.cwd(), filePath);
|
|
123
|
-
const start = performance.now();
|
|
124
|
-
try {
|
|
125
|
-
transpileAndWrite(filePath, devConfig, devOutDir, watchDir);
|
|
126
|
-
const ms = Math.round(performance.now() - start);
|
|
127
|
-
console.log(` ${rel} → compiled (${ms}ms)`);
|
|
128
|
-
}
|
|
129
|
-
catch (err) {
|
|
130
|
-
console.error(` ${rel} → ERROR: ${err.message}`);
|
|
131
|
-
}
|
|
132
|
-
});
|
|
133
|
-
watcher.on('unlink', (filePath) => {
|
|
134
|
-
const rel = relative(process.cwd(), filePath);
|
|
135
|
-
const ext = filePath.endsWith('.kern') ? '.kern' : '.ir';
|
|
136
|
-
const fileBaseName = basename(filePath, ext);
|
|
137
|
-
const unlinkRelDir = relative(resolve(watchDir), dirname(filePath));
|
|
138
|
-
const unlinkBaseDir = devOutDir ? resolve(resolve(devOutDir), unlinkRelDir) : dirname(filePath);
|
|
139
|
-
const outDir = resolve(unlinkBaseDir, devConfig.output.outDir);
|
|
140
|
-
const outExt = devConfig.target === 'fastapi' ? '.py'
|
|
141
|
-
: (devConfig.target === 'vue' || devConfig.target === 'nuxt') ? '.vue'
|
|
142
|
-
: (devConfig.target === 'express' || devConfig.target === 'cli' || devConfig.target === 'terminal') ? '.ts' : '.tsx';
|
|
143
|
-
const outFile = resolve(outDir, `${fileBaseName}${outExt}`);
|
|
144
|
-
try {
|
|
145
|
-
if (existsSync(outFile)) {
|
|
146
|
-
unlinkSync(outFile);
|
|
147
|
-
console.log(` ${rel} → deleted generated file`);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
catch (err) {
|
|
151
|
-
console.error(` ${rel} → ERROR deleting: ${err.message}`);
|
|
152
|
-
}
|
|
153
|
-
});
|
|
154
|
-
console.log(' Watching for changes... (Ctrl+C to stop)\n');
|
|
155
|
-
}).catch((err) => {
|
|
156
|
-
console.error(`kern dev requires chokidar: npm install chokidar`);
|
|
157
|
-
process.exit(1);
|
|
158
|
-
});
|
|
159
|
-
// Keep process alive
|
|
160
|
-
process.on('SIGINT', () => {
|
|
161
|
-
console.log('\n KERN dev stopped.');
|
|
162
|
-
process.exit(0);
|
|
163
|
-
});
|
|
164
|
-
// Don't fall through to standard transpile
|
|
165
|
-
// Keep event loop running by not calling process.exit()
|
|
166
|
-
await new Promise(() => { }); // eslint-disable-line @typescript-eslint/no-empty-function
|
|
167
|
-
}
|
|
168
|
-
function findNearestPackageJson(startDir) {
|
|
169
|
-
let dir = startDir;
|
|
170
|
-
while (true) {
|
|
171
|
-
const candidate = resolve(dir, 'package.json');
|
|
172
|
-
if (existsSync(candidate))
|
|
173
|
-
return candidate;
|
|
174
|
-
const parent = dirname(dir);
|
|
175
|
-
if (parent === dir)
|
|
176
|
-
return null; // hit root
|
|
177
|
-
dir = parent;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
function findKernFiles(dir, singleFile) {
|
|
181
|
-
if (singleFile)
|
|
182
|
-
return [resolve(dir, singleFile)];
|
|
183
|
-
const files = [];
|
|
184
|
-
function walk(d) {
|
|
185
|
-
for (const entry of readdirSync(d)) {
|
|
186
|
-
const full = resolve(d, entry);
|
|
187
|
-
const s = statSync(full);
|
|
188
|
-
if (s.isDirectory() && !entry.startsWith('.') && entry !== 'node_modules' && entry !== 'dist') {
|
|
189
|
-
walk(full);
|
|
190
|
-
}
|
|
191
|
-
else if (entry.endsWith('.kern')) {
|
|
192
|
-
files.push(full);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
walk(dir);
|
|
197
|
-
return files;
|
|
198
|
-
}
|
|
199
|
-
function transpileAndWrite(file, cfg, outDirOverride, inputBase) {
|
|
200
|
-
const source = readFileSync(file, 'utf-8');
|
|
201
|
-
const ast = parseAndSurface(source, file);
|
|
202
|
-
const ext = file.endsWith('.kern') ? '.kern' : '.ir';
|
|
203
|
-
const name = basename(file, ext);
|
|
204
|
-
const target = cfg.target;
|
|
205
|
-
const relSource = relative(process.cwd(), file);
|
|
206
|
-
const header = GENERATED_HEADER + relSource + '\n\n';
|
|
207
|
-
// Emit coverage gaps unless --no-gaps
|
|
208
|
-
if (!args.includes('--no-gaps')) {
|
|
209
|
-
const gaps = collectCoverageGaps(ast, file);
|
|
210
|
-
if (gaps.length > 0) {
|
|
211
|
-
writeCoverageGaps(gaps, resolve(process.cwd(), '.kern-gaps'));
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
const result = target === 'native'
|
|
215
|
-
? transpile(ast, cfg)
|
|
216
|
-
: target === 'web'
|
|
217
|
-
? transpileWeb(ast, cfg)
|
|
218
|
-
: target === 'tailwind'
|
|
219
|
-
? transpileTailwind(ast, cfg)
|
|
220
|
-
: target === 'mcp'
|
|
221
|
-
? transpileMCP(ast, cfg)
|
|
222
|
-
: target === 'express'
|
|
223
|
-
? transpileExpress(ast, cfg)
|
|
224
|
-
: target === 'fastapi'
|
|
225
|
-
? transpileFastAPI(ast, cfg)
|
|
226
|
-
: target === 'cli'
|
|
227
|
-
? transpileCliApp(ast, cfg)
|
|
228
|
-
: target === 'terminal'
|
|
229
|
-
? transpileTerminal(ast, cfg)
|
|
230
|
-
: target === 'ink'
|
|
231
|
-
? transpileInk(ast, cfg)
|
|
232
|
-
: target === 'vue'
|
|
233
|
-
? transpileVue(ast, cfg)
|
|
234
|
-
: target === 'nuxt'
|
|
235
|
-
? transpileNuxt(ast, cfg)
|
|
236
|
-
: transpileNextjs(ast, cfg);
|
|
237
|
-
// Preserve relative directory structure when outDirOverride is set
|
|
238
|
-
// e.g. kern/llm/patterns.kern with --outdir=app/ → app/llm/page.tsx (not app/page.tsx)
|
|
239
|
-
const relDir = inputBase ? relative(resolve(inputBase), dirname(file)) : '';
|
|
240
|
-
const baseDir = outDirOverride ? resolve(resolve(outDirOverride), relDir) : dirname(file);
|
|
241
|
-
const outDir = resolve(baseDir, cfg.output.outDir);
|
|
242
|
-
mkdirSync(outDir, { recursive: true });
|
|
243
|
-
if (result.artifacts && result.artifacts.length > 0 && cfg.structure !== 'flat') {
|
|
244
|
-
for (const artifact of result.artifacts) {
|
|
245
|
-
const artifactPath = resolve(outDir, artifact.path);
|
|
246
|
-
mkdirSync(dirname(artifactPath), { recursive: true });
|
|
247
|
-
writeFileSync(artifactPath, header + artifact.content);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
else {
|
|
251
|
-
const outExt = target === 'fastapi' ? '.py'
|
|
252
|
-
: (target === 'vue' || target === 'nuxt') ? '.vue'
|
|
253
|
-
: (target === 'express' || target === 'cli' || target === 'terminal' || target === 'mcp') ? '.ts'
|
|
254
|
-
: '.tsx';
|
|
255
|
-
// For Next.js target, use the file convention name from the transpiler result (page.tsx, layout.tsx, etc.)
|
|
256
|
-
const resultWithFiles = result;
|
|
257
|
-
const outFileName = (target === 'nextjs' && resultWithFiles.files && resultWithFiles.files.length > 0)
|
|
258
|
-
? resultWithFiles.files[0].path
|
|
259
|
-
: `${name}${outExt}`;
|
|
260
|
-
const outFilePath = resolve(outDir, outFileName);
|
|
261
|
-
mkdirSync(dirname(outFilePath), { recursive: true });
|
|
262
|
-
writeFileSync(outFilePath, header + result.code);
|
|
263
|
-
if (result.artifacts) {
|
|
264
|
-
for (const artifact of result.artifacts) {
|
|
265
|
-
const artifactPath = resolve(outDir, artifact.path);
|
|
266
|
-
mkdirSync(dirname(artifactPath), { recursive: true });
|
|
267
|
-
writeFileSync(artifactPath, header + artifact.content);
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
function loadConfig() {
|
|
273
|
-
const configPath = resolve(process.cwd(), 'kern.config.ts');
|
|
274
|
-
if (existsSync(configPath)) {
|
|
275
|
-
try {
|
|
276
|
-
const jiti = createJiti(import.meta.url);
|
|
277
|
-
const mod = jiti(configPath);
|
|
278
|
-
const userConfig = mod.default ?? mod;
|
|
279
|
-
return resolveConfig(userConfig);
|
|
280
|
-
}
|
|
281
|
-
catch (err) {
|
|
282
|
-
console.error(`Warning: Failed to load kern.config.ts: ${err.message}`);
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
// No kern.config.ts — auto-detect target from package.json
|
|
286
|
-
const autoDetected = autoDetectTarget();
|
|
287
|
-
return resolveConfig(autoDetected ? { target: autoDetected } : {});
|
|
288
|
-
}
|
|
289
|
-
function autoDetectTarget() {
|
|
290
|
-
const pkgPath = resolve(process.cwd(), 'package.json');
|
|
291
|
-
if (!existsSync(pkgPath))
|
|
292
|
-
return null;
|
|
293
|
-
try {
|
|
294
|
-
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
295
|
-
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
296
|
-
// Priority: most specific first
|
|
297
|
-
if (allDeps['next'])
|
|
298
|
-
return 'nextjs';
|
|
299
|
-
if (allDeps['nuxt'])
|
|
300
|
-
return 'nuxt';
|
|
301
|
-
if (allDeps['vue'])
|
|
302
|
-
return 'vue';
|
|
303
|
-
if (allDeps['react-native'])
|
|
304
|
-
return 'native';
|
|
305
|
-
if (allDeps['fastapi'])
|
|
306
|
-
return 'fastapi';
|
|
307
|
-
if (allDeps['flask'] || allDeps['django']) {
|
|
308
|
-
console.warn(`Warning: ${allDeps['flask'] ? 'Flask' : 'Django'} detected but no dedicated target — falling back to 'express' (generates TypeScript, not Python). Use --target=fastapi for Python output.`);
|
|
309
|
-
return 'express';
|
|
310
|
-
}
|
|
311
|
-
if (allDeps['express'] || allDeps['fastify'] || allDeps['koa'] || allDeps['hono'])
|
|
312
|
-
return 'express';
|
|
313
|
-
if (allDeps['tailwindcss'] && allDeps['react'])
|
|
314
|
-
return 'tailwind';
|
|
315
|
-
if (allDeps['react'])
|
|
316
|
-
return 'web';
|
|
317
|
-
return null;
|
|
318
|
-
}
|
|
319
|
-
catch {
|
|
320
|
-
return null;
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
function loadTemplates(cfg) {
|
|
324
|
-
clearTemplates();
|
|
325
|
-
if (!cfg.templates || cfg.templates.length === 0)
|
|
34
|
+
// Route standard commands
|
|
35
|
+
const handler = cmd ? COMMANDS[cmd] : undefined;
|
|
36
|
+
if (handler) {
|
|
37
|
+
await handler(args);
|
|
326
38
|
return;
|
|
327
|
-
for (const templatePath of cfg.templates) {
|
|
328
|
-
const resolved = resolve(process.cwd(), templatePath);
|
|
329
|
-
if (!existsSync(resolved))
|
|
330
|
-
continue;
|
|
331
|
-
const stat = statSync(resolved);
|
|
332
|
-
const files = [];
|
|
333
|
-
if (stat.isDirectory()) {
|
|
334
|
-
for (const entry of readdirSync(resolved)) {
|
|
335
|
-
if (entry.endsWith('.kern'))
|
|
336
|
-
files.push(resolve(resolved, entry));
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
else if (resolved.endsWith('.kern')) {
|
|
340
|
-
files.push(resolved);
|
|
341
|
-
}
|
|
342
|
-
for (const file of files) {
|
|
343
|
-
try {
|
|
344
|
-
const source = readFileSync(file, 'utf-8');
|
|
345
|
-
const ast = parseAndSurface(source, file);
|
|
346
|
-
// Register top-level template nodes
|
|
347
|
-
const nodes = ast.type === 'template' ? [ast] : (ast.children || []).filter(n => n.type === 'template');
|
|
348
|
-
for (const node of nodes) {
|
|
349
|
-
registerTemplate(node, file);
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
catch (err) {
|
|
353
|
-
console.error(` Warning: Failed to load template ${basename(file)}: ${err.message}`);
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
// ── kern compile <dir|file> --outdir=<dir> ────────────────────────────
|
|
359
|
-
if (args[0] === 'compile') {
|
|
360
|
-
const compileInput = args[1];
|
|
361
|
-
const outDirArg = args.find(a => a.startsWith('--outdir='))?.split('=')[1];
|
|
362
|
-
if (!compileInput) {
|
|
363
|
-
console.error('Usage: kern compile <file.kern|dir> --outdir=<dir>');
|
|
364
|
-
process.exit(1);
|
|
365
|
-
}
|
|
366
|
-
const outDir = resolve(outDirArg || 'generated');
|
|
367
|
-
mkdirSync(outDir, { recursive: true });
|
|
368
|
-
const inputPath = resolve(compileInput);
|
|
369
|
-
const stat = existsSync(inputPath) ? statSync(inputPath) : null;
|
|
370
|
-
const kernFiles = [];
|
|
371
|
-
if (stat && stat.isDirectory()) {
|
|
372
|
-
for (const f of readdirSync(inputPath)) {
|
|
373
|
-
if (f.endsWith('.kern'))
|
|
374
|
-
kernFiles.push(resolve(inputPath, f));
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
else if (stat && stat.isFile()) {
|
|
378
|
-
kernFiles.push(inputPath);
|
|
379
|
-
}
|
|
380
|
-
else {
|
|
381
|
-
console.error(`Not found: ${compileInput}`);
|
|
382
|
-
process.exit(1);
|
|
383
|
-
}
|
|
384
|
-
if (kernFiles.length === 0) {
|
|
385
|
-
console.error(`No .kern files found in: ${compileInput}`);
|
|
386
|
-
process.exit(1);
|
|
387
|
-
}
|
|
388
|
-
// Load templates from config before compile
|
|
389
|
-
const compileConfig = loadConfig();
|
|
390
|
-
loadTemplates(compileConfig);
|
|
391
|
-
let compiled = 0;
|
|
392
|
-
for (const file of kernFiles) {
|
|
393
|
-
const source = readFileSync(file, 'utf-8');
|
|
394
|
-
const ast = parseAndSurface(source, file);
|
|
395
|
-
const lines = [];
|
|
396
|
-
let hasReactNodes = false;
|
|
397
|
-
// Generate TypeScript for all core + React + template nodes (root + children)
|
|
398
|
-
function processNode(node) {
|
|
399
|
-
if (isCoreNode(node.type)) {
|
|
400
|
-
lines.push(...generateCoreNode(node));
|
|
401
|
-
lines.push('');
|
|
402
|
-
// hook generates React imports, so flag it
|
|
403
|
-
if (node.type === 'hook')
|
|
404
|
-
hasReactNodes = true;
|
|
405
|
-
}
|
|
406
|
-
else if (isTemplateNode(node.type)) {
|
|
407
|
-
lines.push(...expandTemplateNode(node));
|
|
408
|
-
lines.push('');
|
|
409
|
-
}
|
|
410
|
-
else if (isReactNode(node.type)) {
|
|
411
|
-
lines.push(...generateReactNode(node));
|
|
412
|
-
lines.push('');
|
|
413
|
-
hasReactNodes = true;
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
processNode(ast);
|
|
417
|
-
if (ast.children) {
|
|
418
|
-
for (const child of ast.children) {
|
|
419
|
-
processNode(child);
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
if (lines.length > 0) {
|
|
423
|
-
// Use .tsx for files with React nodes (JSX output), .ts otherwise
|
|
424
|
-
const ext = hasReactNodes ? '.tsx' : '.ts';
|
|
425
|
-
const outName = basename(file, '.kern') + ext;
|
|
426
|
-
const outFile = resolve(outDir, outName);
|
|
427
|
-
writeFileSync(outFile, lines.join('\n') + '\n');
|
|
428
|
-
console.log(` ${basename(file)} → ${outName}`);
|
|
429
|
-
compiled++;
|
|
430
|
-
}
|
|
431
|
-
else {
|
|
432
|
-
console.log(` ${basename(file)} → (no core nodes, skipped)`);
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
console.log(`\nCompiled ${compiled}/${kernFiles.length} files → ${outDir}`);
|
|
436
|
-
process.exit(0);
|
|
437
|
-
}
|
|
438
|
-
// ── kern scan [--force] [--dry-run] ─────────────────────────────────────
|
|
439
|
-
if (args[0] === 'scan') {
|
|
440
|
-
const scanCwd = process.cwd();
|
|
441
|
-
const force = args.includes('--force');
|
|
442
|
-
const dryRun = args.includes('--dry-run');
|
|
443
|
-
const result = scanProject(scanCwd);
|
|
444
|
-
console.log(formatScanSummary(result));
|
|
445
|
-
if (dryRun) {
|
|
446
|
-
console.log(' --dry-run: no files written.\n');
|
|
447
|
-
console.log(generateConfigSource(result));
|
|
448
|
-
process.exit(0);
|
|
449
|
-
}
|
|
450
|
-
const configOutPath = resolve(scanCwd, 'kern.config.ts');
|
|
451
|
-
if (existsSync(configOutPath) && !force) {
|
|
452
|
-
console.log(' kern.config.ts already exists. Use --force to overwrite.\n');
|
|
453
|
-
process.exit(0);
|
|
454
|
-
}
|
|
455
|
-
writeFileSync(configOutPath, generateConfigSource(result));
|
|
456
|
-
console.log(' Written: kern.config.ts\n');
|
|
457
|
-
process.exit(0);
|
|
458
|
-
}
|
|
459
|
-
// ── kern init-templates [--force] [--dry-run] ───────────────────────────
|
|
460
|
-
if (args[0] === 'init-templates') {
|
|
461
|
-
const force = args.includes('--force');
|
|
462
|
-
const dryRun = args.includes('--dry-run');
|
|
463
|
-
const initCwd = process.cwd();
|
|
464
|
-
const templatesDir = resolve(initCwd, 'templates');
|
|
465
|
-
// Find package.json
|
|
466
|
-
const pkgPath = findNearestPackageJson(initCwd);
|
|
467
|
-
if (!pkgPath) {
|
|
468
|
-
console.error('No package.json found. Run this in a project directory.');
|
|
469
|
-
process.exit(1);
|
|
470
|
-
}
|
|
471
|
-
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
472
|
-
const detected = detectTemplates(pkg);
|
|
473
|
-
console.log('\n KERN init-templates — scanning dependencies\n');
|
|
474
|
-
if (detected.length === 0 && !force) {
|
|
475
|
-
console.log(' No recognized libraries detected.');
|
|
476
|
-
console.log(' Common templates (arrow-fn, window-event) will still be created.\n');
|
|
477
|
-
}
|
|
478
|
-
// Collect all template files to write
|
|
479
|
-
const filesToWrite = { ...COMMON_TEMPLATES };
|
|
480
|
-
for (const entry of detected) {
|
|
481
|
-
console.log(` Detected: ${entry.libraryName} (${entry.packageName})`);
|
|
482
|
-
Object.assign(filesToWrite, entry.templates);
|
|
483
|
-
}
|
|
484
|
-
if (dryRun) {
|
|
485
|
-
console.log(`\n --dry-run: would create ${Object.keys(filesToWrite).length} template files in templates/\n`);
|
|
486
|
-
for (const name of Object.keys(filesToWrite).sort()) {
|
|
487
|
-
console.log(` templates/${name}`);
|
|
488
|
-
}
|
|
489
|
-
process.exit(0);
|
|
490
|
-
}
|
|
491
|
-
mkdirSync(templatesDir, { recursive: true });
|
|
492
|
-
let written = 0;
|
|
493
|
-
let skipped = 0;
|
|
494
|
-
for (const [name, content] of Object.entries(filesToWrite)) {
|
|
495
|
-
const outPath = resolve(templatesDir, name);
|
|
496
|
-
if (existsSync(outPath) && !force) {
|
|
497
|
-
console.log(` skip: templates/${name} (exists, use --force)`);
|
|
498
|
-
skipped++;
|
|
499
|
-
continue;
|
|
500
|
-
}
|
|
501
|
-
writeFileSync(outPath, content);
|
|
502
|
-
console.log(` wrote: templates/${name}`);
|
|
503
|
-
written++;
|
|
504
|
-
}
|
|
505
|
-
// Update kern.config.ts to include templates path
|
|
506
|
-
const configPath = resolve(initCwd, 'kern.config.ts');
|
|
507
|
-
if (existsSync(configPath)) {
|
|
508
|
-
const configContent = readFileSync(configPath, 'utf-8');
|
|
509
|
-
if (!configContent.includes('templates')) {
|
|
510
|
-
console.log('\n Note: Add templates to your kern.config.ts:');
|
|
511
|
-
console.log(" templates: ['./templates/'],\n");
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
else {
|
|
515
|
-
// Create a minimal kern.config.ts
|
|
516
|
-
const configSource = [
|
|
517
|
-
'export default {',
|
|
518
|
-
" target: 'web',",
|
|
519
|
-
" templates: ['./templates/'],",
|
|
520
|
-
'};',
|
|
521
|
-
'',
|
|
522
|
-
].join('\n');
|
|
523
|
-
writeFileSync(configPath, configSource);
|
|
524
|
-
console.log(' wrote: kern.config.ts');
|
|
525
|
-
written++;
|
|
526
|
-
}
|
|
527
|
-
console.log(`\n Done: ${written} written, ${skipped} skipped.`);
|
|
528
|
-
if (detected.length > 0) {
|
|
529
|
-
console.log(` Templates ready for: ${detected.map(d => d.libraryName).join(', ')}`);
|
|
530
|
-
}
|
|
531
|
-
console.log('');
|
|
532
|
-
process.exit(0);
|
|
533
|
-
}
|
|
534
|
-
async function runReviewPipeline(reviewConfig, entryFilePaths, modes) {
|
|
535
|
-
const { graphMode, batchMode, llmMode, cloudMode, securityMode, mcpMode, specMode, fixMode, autofixMode, lintMode, exportKern, enforce, jsonOutput, sarifOutput, maxDepth, batchSize, tsconfigPath, specFile, minCoverageArg, maxComplexityArg, maxErrorsArg, maxWarningsArg, showConfidence } = modes;
|
|
536
|
-
let reports = [];
|
|
537
|
-
if (graphMode && entryFilePaths.length > 0) {
|
|
538
|
-
// --graph: resolve import graph, review all files with provenance
|
|
539
|
-
const graphOpts = { maxDepth, tsConfigFilePath: tsconfigPath ? resolve(tsconfigPath) : undefined };
|
|
540
|
-
const graph = resolveImportGraph(entryFilePaths, graphOpts);
|
|
541
|
-
console.log(` Graph: ${graph.totalFiles} files resolved (${graph.skipped} skipped, depth ${maxDepth})`);
|
|
542
|
-
reports = reviewGraph(entryFilePaths, reviewConfig, graphOpts);
|
|
543
|
-
}
|
|
544
|
-
else if (batchMode && entryFilePaths.length > batchSize) {
|
|
545
|
-
// --batch: process in chunks for large repos
|
|
546
|
-
const totalBatches = Math.ceil(entryFilePaths.length / batchSize);
|
|
547
|
-
for (let i = 0; i < entryFilePaths.length; i += batchSize) {
|
|
548
|
-
const batch = entryFilePaths.slice(i, i + batchSize);
|
|
549
|
-
const batchNum = Math.floor(i / batchSize) + 1;
|
|
550
|
-
for (const f of batch) {
|
|
551
|
-
try {
|
|
552
|
-
reports.push(reviewFile(f, reviewConfig));
|
|
553
|
-
}
|
|
554
|
-
catch (e) {
|
|
555
|
-
console.error(` Review error in ${f}: ${e.message}`);
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
const batchFindings = reports.slice(-batch.length).reduce((sum, r) => sum + r.findings.length, 0);
|
|
559
|
-
console.log(` Batch ${batchNum}/${totalBatches}: ${batch.length} files reviewed (${batchFindings} findings)`);
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
else {
|
|
563
|
-
// Standard mode: review each file individually
|
|
564
|
-
for (const f of entryFilePaths) {
|
|
565
|
-
try {
|
|
566
|
-
reports.push(reviewFile(f, reviewConfig));
|
|
567
|
-
}
|
|
568
|
-
catch (e) {
|
|
569
|
-
console.error(` Review error in ${f}: ${e.message}`);
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
if (reports.length === 0) {
|
|
574
|
-
console.log(' No reviewable files found (.ts/.tsx/.py/.kern).');
|
|
575
|
-
return { reports, exitCode: 0 };
|
|
576
|
-
}
|
|
577
|
-
// MCP security review — --mcp flag or auto-detect MCP server files
|
|
578
|
-
try {
|
|
579
|
-
const { reviewIfMCP, reviewMCPSource } = await import('@kernlang/review-mcp');
|
|
580
|
-
let mcpFileCount = 0;
|
|
581
|
-
for (const report of reports) {
|
|
582
|
-
const source = readFileSync(report.filePath, 'utf-8');
|
|
583
|
-
const mcpFindings = mcpMode
|
|
584
|
-
? reviewMCPSource(source, report.filePath)
|
|
585
|
-
: reviewIfMCP(source, report.filePath);
|
|
586
|
-
if (mcpFindings && mcpFindings.length > 0) {
|
|
587
|
-
report.findings.push(...mcpFindings);
|
|
588
|
-
mcpFileCount++;
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
if (mcpFileCount > 0 && !jsonOutput && !sarifOutput) {
|
|
592
|
-
console.log(` MCP security: ${mcpFileCount} server file(s) scanned`);
|
|
593
|
-
}
|
|
594
39
|
}
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
// --export-kern: output KERN IR for AI review (v1 compat)
|
|
602
|
-
if (exportKern) {
|
|
603
|
-
for (const report of reports) {
|
|
604
|
-
console.log(`\n// ── ${report.filePath} ──`);
|
|
605
|
-
console.log(exportKernIR(report.inferred, report.templateMatches));
|
|
606
|
-
}
|
|
607
|
-
return { reports, exitCode: 0 };
|
|
608
|
-
}
|
|
609
|
-
// --spec: verify .kern spec contracts against .ts implementation
|
|
610
|
-
if (specMode && specFile) {
|
|
611
|
-
const kernFilePath = resolve(specFile);
|
|
612
|
-
if (!existsSync(kernFilePath)) {
|
|
613
|
-
console.error(` .kern spec file not found: ${specFile}`);
|
|
614
|
-
return { reports, exitCode: 1 };
|
|
615
|
-
}
|
|
616
|
-
console.log(`\n KERN spec check: ${specFile} → ${reports.length} implementation files\n`);
|
|
617
|
-
let totalViolations = 0;
|
|
618
|
-
for (const report of reports) {
|
|
619
|
-
const result = checkSpecFiles(kernFilePath, report.filePath);
|
|
620
|
-
if (result.violations.length > 0) {
|
|
621
|
-
const findings = specViolationsToFindings(result);
|
|
622
|
-
totalViolations += findings.length;
|
|
623
|
-
report.findings.push(...findings);
|
|
624
|
-
report.findings = dedup(report.findings);
|
|
625
|
-
for (const v of result.violations) {
|
|
626
|
-
const icon = v.kind.includes('missing') || v.kind === 'spec-unimplemented' ? '✗' : '~';
|
|
627
|
-
const sev = v.kind === 'spec-auth-missing' || v.kind === 'spec-unimplemented' ? 'ERROR' : v.kind === 'spec-undeclared' ? 'INFO' : 'WARN';
|
|
628
|
-
console.log(` ${icon} [${sev}] ${v.kind}: ${v.detail}`);
|
|
629
|
-
if (v.suggestion)
|
|
630
|
-
console.log(` → ${v.suggestion}`);
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
if (result.matched.length > 0) {
|
|
634
|
-
const satisfied = result.matched.length - result.violations.filter(v => v.kind !== 'spec-undeclared' && v.kind !== 'spec-unimplemented').length;
|
|
635
|
-
console.log(`\n Matched: ${result.matched.length} routes | Satisfied: ${satisfied} | Violations: ${totalViolations}`);
|
|
636
|
-
if (result.unmatchedSpecs.length > 0)
|
|
637
|
-
console.log(` Unimplemented: ${result.unmatchedSpecs.map(s => s.routeKey).join(', ')}`);
|
|
638
|
-
if (result.unmatchedImpls.length > 0)
|
|
639
|
-
console.log(` Undeclared: ${result.unmatchedImpls.map(i => i.routeKey).join(', ')}`);
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
if (totalViolations === 0) {
|
|
643
|
-
console.log(' All spec contracts satisfied.');
|
|
644
|
-
}
|
|
645
|
-
console.log('');
|
|
646
|
-
// Fall through to normal output
|
|
647
|
-
}
|
|
648
|
-
// --security: show only security-related findings
|
|
649
|
-
if (securityMode) {
|
|
650
|
-
const SECURITY_RULES = new Set([
|
|
651
|
-
'xss-unsafe-html', 'hardcoded-secret', 'command-injection', 'no-eval',
|
|
652
|
-
'insecure-random', 'cors-wildcard', 'helmet-missing', 'open-redirect',
|
|
653
|
-
'jwt-weak-verification', 'cookie-hardening', 'csrf-detection', 'csp-strength',
|
|
654
|
-
'path-traversal', 'weak-password-hashing', 'regex-dos', 'missing-input-validation',
|
|
655
|
-
'prototype-pollution', 'information-exposure', 'prompt-injection',
|
|
656
|
-
'taint-command', 'taint-fs', 'taint-sql', 'taint-redirect', 'taint-eval',
|
|
657
|
-
'taint-insufficient-sanitizer', 'taint-crossfile-command', 'taint-crossfile-fs',
|
|
658
|
-
'taint-crossfile-sql', 'taint-crossfile-redirect', 'taint-crossfile-eval',
|
|
659
|
-
'spec-auth-missing', 'spec-validate-missing', 'spec-guard-missing',
|
|
660
|
-
'spec-middleware-missing', 'spec-unimplemented',
|
|
661
|
-
]);
|
|
662
|
-
console.log('\n KERN Security Report\n');
|
|
663
|
-
let totalSec = 0;
|
|
664
|
-
for (const report of reports) {
|
|
665
|
-
const secFindings = report.findings.filter(f => SECURITY_RULES.has(f.ruleId));
|
|
666
|
-
if (secFindings.length === 0)
|
|
667
|
-
continue;
|
|
668
|
-
totalSec += secFindings.length;
|
|
669
|
-
const rel = relative(process.cwd(), report.filePath);
|
|
670
|
-
console.log(` ${rel}:`);
|
|
671
|
-
for (const f of secFindings) {
|
|
672
|
-
const icon = f.severity === 'error' ? '✗' : f.severity === 'warning' ? '~' : '-';
|
|
673
|
-
console.log(` ${icon} L${f.primarySpan.startLine}: [${f.ruleId}] ${f.message}`);
|
|
674
|
-
if (f.suggestion)
|
|
675
|
-
console.log(` → ${f.suggestion}`);
|
|
676
|
-
}
|
|
677
|
-
console.log('');
|
|
678
|
-
}
|
|
679
|
-
if (totalSec === 0) {
|
|
680
|
-
console.log(' No security issues found.');
|
|
681
|
-
}
|
|
682
|
-
else {
|
|
683
|
-
const errors = reports.flatMap(r => r.findings).filter(f => SECURITY_RULES.has(f.ruleId) && f.severity === 'error').length;
|
|
684
|
-
const warnings = reports.flatMap(r => r.findings).filter(f => SECURITY_RULES.has(f.ruleId) && f.severity === 'warning').length;
|
|
685
|
-
console.log(` Total: ${totalSec} security findings (${errors} errors, ${warnings} warnings)`);
|
|
686
|
-
}
|
|
687
|
-
console.log(' Rules: OWASP Top 10, OWASP LLM Top 10, Taint Tracking, Spec Contracts');
|
|
688
|
-
console.log('');
|
|
689
|
-
return { reports, exitCode: 0 };
|
|
690
|
-
}
|
|
691
|
-
// --cloud: KERN Pro cloud review (coming soon)
|
|
692
|
-
if (cloudMode) {
|
|
693
|
-
console.log('');
|
|
694
|
-
console.log(' KERN Pro — Cloud-powered AI review');
|
|
695
|
-
console.log('');
|
|
696
|
-
console.log(' Coming soon. Cloud review will provide:');
|
|
697
|
-
console.log(' • LLM-powered security analysis without an AI IDE');
|
|
698
|
-
console.log(' • Team dashboard with trend tracking');
|
|
699
|
-
console.log(' • Custom rule engine for enterprise');
|
|
700
|
-
console.log(' • CI/CD integration with quality gates');
|
|
701
|
-
console.log('');
|
|
702
|
-
console.log(' For now, use --llm with your AI assistant (Claude Code, Cursor, etc.)');
|
|
703
|
-
console.log(' The assistant reads the KERN IR output and performs the AI review.');
|
|
704
|
-
console.log('');
|
|
705
|
-
console.log(' → kern review src/ --llm');
|
|
706
|
-
console.log('');
|
|
707
|
-
console.log(' Join the waitlist: https://kernlang.dev/pro');
|
|
708
|
-
console.log('');
|
|
709
|
-
return { reports, exitCode: 0 };
|
|
710
|
-
}
|
|
711
|
-
// --llm: LLM-assisted security review (batch file check)
|
|
712
|
-
if (llmMode) {
|
|
713
|
-
// Build graph context for LLM markers if --graph is active
|
|
714
|
-
const llmGraphContext = graphMode ? (() => {
|
|
715
|
-
const fileDistances = new Map();
|
|
716
|
-
for (const report of reports) {
|
|
717
|
-
const finding = report.findings[0];
|
|
718
|
-
const distance = finding?.distance ?? 0;
|
|
719
|
-
fileDistances.set(report.filePath, distance);
|
|
720
|
-
}
|
|
721
|
-
for (const ep of entryFilePaths) {
|
|
722
|
-
fileDistances.set(ep, 0);
|
|
723
|
-
}
|
|
724
|
-
return { fileDistances };
|
|
725
|
-
})() : undefined;
|
|
726
|
-
if (isLLMAvailable()) {
|
|
727
|
-
// Phase 3: actual LLM API call with taint context
|
|
728
|
-
console.log(' LLM review: calling API...');
|
|
729
|
-
const llmInputs = reports.map(report => ({
|
|
730
|
-
filePath: report.filePath,
|
|
731
|
-
inferred: report.inferred,
|
|
732
|
-
templateMatches: report.templateMatches,
|
|
733
|
-
taintResults: analyzeTaint(report.inferred, report.filePath),
|
|
734
|
-
graphContext: llmGraphContext,
|
|
735
|
-
}));
|
|
736
|
-
try {
|
|
737
|
-
const llmFindings = await runLLMReview(llmInputs);
|
|
738
|
-
console.log(` LLM review: ${llmFindings.length} findings from AI`);
|
|
739
|
-
// Merge LLM findings into reports
|
|
740
|
-
for (const f of llmFindings) {
|
|
741
|
-
const report = reports.find(r => r.filePath === f.primarySpan.file);
|
|
742
|
-
if (report) {
|
|
743
|
-
report.findings.push(f);
|
|
744
|
-
}
|
|
745
|
-
else if (reports.length > 0) {
|
|
746
|
-
reports[0].findings.push(f);
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
// Dedup after merge
|
|
750
|
-
for (const report of reports) {
|
|
751
|
-
report.findings = dedup(report.findings);
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
catch (err) {
|
|
755
|
-
console.error(` LLM review failed: ${err.message}`);
|
|
756
|
-
}
|
|
757
|
-
// Fall through to normal output (don't exit — show merged findings)
|
|
758
|
-
}
|
|
759
|
-
else {
|
|
760
|
-
// No cloud API key — output static findings + KERN IR for the AI assistant
|
|
761
|
-
// The LLM running this command (Claude Code, Cursor, etc.) IS the reviewer
|
|
762
|
-
console.log('\n ── KERN IR for LLM review ──\n');
|
|
763
|
-
for (const report of reports) {
|
|
764
|
-
console.log(`// ── ${report.filePath} ──`);
|
|
765
|
-
console.log(buildLLMPrompt(report.inferred, report.templateMatches, llmGraphContext));
|
|
766
|
-
// Include taint analysis context
|
|
767
|
-
const taintResults = analyzeTaint(report.inferred, report.filePath);
|
|
768
|
-
if (taintResults.length > 0) {
|
|
769
|
-
console.log('\n// Taint analysis:');
|
|
770
|
-
for (const t of taintResults) {
|
|
771
|
-
for (const p of t.paths) {
|
|
772
|
-
const status = p.sanitized ? `SANITIZED (${p.sanitizer})` : 'UNSANITIZED';
|
|
773
|
-
console.log(`// ${t.fnName}: ${p.source.origin} → ${p.sink.name}() [${status}]`);
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
console.log('');
|
|
778
|
-
}
|
|
779
|
-
// Show static findings summary
|
|
780
|
-
const totalFindings = reports.reduce((sum, r) => sum + r.findings.length, 0);
|
|
781
|
-
const errors = reports.reduce((sum, r) => sum + r.findings.filter(f => f.severity === 'error').length, 0);
|
|
782
|
-
const warnings = reports.reduce((sum, r) => sum + r.findings.filter(f => f.severity === 'warning').length, 0);
|
|
783
|
-
console.log(` Static analysis: ${totalFindings} findings (${errors} errors, ${warnings} warnings)`);
|
|
784
|
-
console.log(' Review the KERN IR above for security issues the static rules may have missed.');
|
|
785
|
-
console.log('');
|
|
786
|
-
}
|
|
787
|
-
// Fall through to normal output — show full report with static findings
|
|
788
|
-
}
|
|
789
|
-
// --fix: auto-migration — write .kern files from template suggestions, verify roundtrip
|
|
790
|
-
if (fixMode) {
|
|
791
|
-
let fixed = 0;
|
|
792
|
-
let verified = 0;
|
|
793
|
-
for (const report of reports) {
|
|
794
|
-
for (const t of report.templateMatches) {
|
|
795
|
-
if (!t.suggestedKern)
|
|
796
|
-
continue;
|
|
797
|
-
const kernFileName = report.filePath.replace(/\.tsx?$/, '.kern');
|
|
798
|
-
try {
|
|
799
|
-
writeFileSync(kernFileName, t.suggestedKern + '\n');
|
|
800
|
-
// Verify roundtrip: parse the written .kern file
|
|
801
|
-
try {
|
|
802
|
-
parseAndSurface(readFileSync(kernFileName, 'utf-8'), kernFileName);
|
|
803
|
-
console.log(` ${report.filePath} → ${kernFileName} (verified)`);
|
|
804
|
-
verified++;
|
|
805
|
-
}
|
|
806
|
-
catch (parseErr) {
|
|
807
|
-
console.error(` ${kernFileName} written but parse failed: ${parseErr.message}`);
|
|
808
|
-
}
|
|
809
|
-
fixed++;
|
|
810
|
-
}
|
|
811
|
-
catch (err) {
|
|
812
|
-
console.error(` Failed to write ${kernFileName}: ${err.message}`);
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
if (fixed === 0) {
|
|
817
|
-
console.log(' No template suggestions to fix — nothing to migrate.');
|
|
818
|
-
}
|
|
819
|
-
else {
|
|
820
|
-
console.log(`\n ${fixed} .kern file(s) written, ${verified} verified.`);
|
|
821
|
-
}
|
|
822
|
-
return { reports, exitCode: 0 };
|
|
823
|
-
}
|
|
824
|
-
// --autofix: apply structured source edits from finding autofixes
|
|
825
|
-
if (autofixMode) {
|
|
826
|
-
// Collect all findings with autofix, grouped by file
|
|
827
|
-
const fixesByFile = new Map();
|
|
828
|
-
for (const report of reports) {
|
|
829
|
-
for (const f of report.findings) {
|
|
830
|
-
if (!f.autofix)
|
|
831
|
-
continue;
|
|
832
|
-
const file = f.autofix.span.file || report.filePath;
|
|
833
|
-
if (!fixesByFile.has(file))
|
|
834
|
-
fixesByFile.set(file, []);
|
|
835
|
-
fixesByFile.get(file).push({ finding: f, fix: f.autofix });
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
if (fixesByFile.size === 0) {
|
|
839
|
-
console.log(' No autofixes available in findings.');
|
|
840
|
-
return { reports, exitCode: 0 };
|
|
841
|
-
}
|
|
842
|
-
let totalApplied = 0;
|
|
843
|
-
let totalSkipped = 0;
|
|
844
|
-
for (const [file, fixes] of fixesByFile) {
|
|
845
|
-
if (!existsSync(file)) {
|
|
846
|
-
console.error(` Skipping ${file} — file not found`);
|
|
847
|
-
totalSkipped += fixes.length;
|
|
848
|
-
continue;
|
|
849
|
-
}
|
|
850
|
-
// Sort bottom-up: highest line first, then highest col, to avoid offset shifts
|
|
851
|
-
fixes.sort((a, b) => {
|
|
852
|
-
const lineDiff = b.fix.span.startLine - a.fix.span.startLine;
|
|
853
|
-
if (lineDiff !== 0)
|
|
854
|
-
return lineDiff;
|
|
855
|
-
return b.fix.span.startCol - a.fix.span.startCol;
|
|
856
|
-
});
|
|
857
|
-
// Detect overlapping fixes — skip later ones that overlap with already-applied spans
|
|
858
|
-
const appliedSpans = [];
|
|
859
|
-
function overlaps(sl, el) {
|
|
860
|
-
return appliedSpans.some(s => sl <= s.el && el >= s.sl);
|
|
861
|
-
}
|
|
862
|
-
const lines = readFileSync(file, 'utf-8').split('\n');
|
|
863
|
-
let applied = 0;
|
|
864
|
-
for (const { finding, fix } of fixes) {
|
|
865
|
-
const { startLine, startCol, endLine, endCol } = fix.span;
|
|
866
|
-
// Lines are 1-indexed, array is 0-indexed
|
|
867
|
-
const sl = startLine - 1;
|
|
868
|
-
const el = endLine - 1;
|
|
869
|
-
if (sl < 0 || el >= lines.length) {
|
|
870
|
-
console.error(` Skipping ${finding.ruleId}@${startLine}:${startCol} — span out of range`);
|
|
871
|
-
totalSkipped++;
|
|
872
|
-
continue;
|
|
873
|
-
}
|
|
874
|
-
if (overlaps(sl, el)) {
|
|
875
|
-
console.error(` Skipping ${finding.ruleId}@${startLine}:${startCol} — overlaps with a previously applied fix`);
|
|
876
|
-
totalSkipped++;
|
|
877
|
-
continue;
|
|
878
|
-
}
|
|
879
|
-
if (fix.type === 'replace') {
|
|
880
|
-
const before = lines[sl].slice(0, startCol - 1);
|
|
881
|
-
const after = lines[el].slice(endCol - 1);
|
|
882
|
-
const replacementLines = fix.replacement.split('\n');
|
|
883
|
-
replacementLines[0] = before + replacementLines[0];
|
|
884
|
-
replacementLines[replacementLines.length - 1] += after;
|
|
885
|
-
lines.splice(sl, el - sl + 1, ...replacementLines);
|
|
886
|
-
}
|
|
887
|
-
else if (fix.type === 'insert-before') {
|
|
888
|
-
lines.splice(sl, 0, fix.replacement);
|
|
889
|
-
}
|
|
890
|
-
else if (fix.type === 'insert-after') {
|
|
891
|
-
lines.splice(el + 1, 0, fix.replacement);
|
|
892
|
-
}
|
|
893
|
-
else if (fix.type === 'remove') {
|
|
894
|
-
lines.splice(sl, el - sl + 1);
|
|
895
|
-
}
|
|
896
|
-
else if (fix.type === 'wrap') {
|
|
897
|
-
const original = lines.slice(sl, el + 1).join('\n');
|
|
898
|
-
const wrapped = fix.replacement.replace('$0', original);
|
|
899
|
-
lines.splice(sl, el - sl + 1, ...wrapped.split('\n'));
|
|
900
|
-
}
|
|
901
|
-
appliedSpans.push({ sl, el });
|
|
902
|
-
applied++;
|
|
903
|
-
}
|
|
904
|
-
writeFileSync(file, lines.join('\n'));
|
|
905
|
-
console.log(` ${file}: ${applied} fix${applied === 1 ? '' : 'es'} applied`);
|
|
906
|
-
totalApplied += applied;
|
|
907
|
-
}
|
|
908
|
-
console.log(`\n ${totalApplied} autofix${totalApplied === 1 ? '' : 'es'} applied, ${totalSkipped} skipped.`);
|
|
909
|
-
return { reports, exitCode: 0 };
|
|
910
|
-
}
|
|
911
|
-
// --lint: run ESLint + tsc diagnostics and merge into findings
|
|
912
|
-
if (lintMode) {
|
|
913
|
-
const filePaths = reports.map(r => r.filePath).filter(f => existsSync(f));
|
|
914
|
-
// ESLint pass
|
|
915
|
-
const eslintFindings = await runESLint(filePaths, process.cwd());
|
|
916
|
-
if (eslintFindings.length > 0) {
|
|
917
|
-
console.log(` ESLint: ${eslintFindings.length} findings`);
|
|
918
|
-
for (const report of reports) {
|
|
919
|
-
const fileFindings = eslintFindings.filter(f => f.primarySpan.file === report.filePath);
|
|
920
|
-
const linked = linkToNodes(fileFindings, report.inferred);
|
|
921
|
-
report.findings = dedup([...report.findings, ...linked]);
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
else {
|
|
925
|
-
console.log(' ESLint: no findings (or not installed)');
|
|
926
|
-
}
|
|
927
|
-
// tsc pass
|
|
928
|
-
const tscFindings = runTSCDiagnosticsFromPaths(filePaths);
|
|
929
|
-
if (tscFindings.length > 0) {
|
|
930
|
-
console.log(` tsc: ${tscFindings.length} findings`);
|
|
931
|
-
for (const report of reports) {
|
|
932
|
-
const fileFindings = tscFindings.filter(f => f.primarySpan.file === report.filePath);
|
|
933
|
-
const linked = linkToNodes(fileFindings, report.inferred);
|
|
934
|
-
report.findings = dedup([...report.findings, ...linked]);
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
else {
|
|
938
|
-
console.log(' tsc: no findings');
|
|
939
|
-
}
|
|
940
|
-
}
|
|
941
|
-
if (jsonOutput) {
|
|
942
|
-
// Include KERN IR + LLM prompt in JSON so the calling AI can review
|
|
943
|
-
const enriched = reports.map(report => {
|
|
944
|
-
const llmPrompt = buildLLMPrompt(report.inferred, report.templateMatches);
|
|
945
|
-
const kernIR = exportKernIR(report.inferred, report.templateMatches);
|
|
946
|
-
return { ...report, kernIR, llmPrompt };
|
|
947
|
-
});
|
|
948
|
-
console.log(JSON.stringify(enriched.length === 1 ? enriched[0] : enriched, null, 2));
|
|
949
|
-
}
|
|
950
|
-
else if (sarifOutput) {
|
|
951
|
-
console.log(formatSARIF(reports));
|
|
952
|
-
}
|
|
953
|
-
else {
|
|
954
|
-
for (const report of reports) {
|
|
955
|
-
console.log('');
|
|
956
|
-
console.log(formatReport(report, reviewConfig));
|
|
957
|
-
}
|
|
958
|
-
if (reports.length > 1) {
|
|
959
|
-
console.log('');
|
|
960
|
-
console.log(formatSummary(reports));
|
|
961
|
-
}
|
|
962
|
-
// Enforcement
|
|
963
|
-
const hasThresholds = minCoverageArg !== undefined || maxComplexityArg !== undefined || maxErrorsArg !== undefined || maxWarningsArg !== undefined;
|
|
964
|
-
if (enforce || hasThresholds) {
|
|
965
|
-
console.log('');
|
|
966
|
-
let allPassed = true;
|
|
967
|
-
for (const report of reports) {
|
|
968
|
-
const result = checkEnforcement(report, reviewConfig);
|
|
969
|
-
if (!result.passed) {
|
|
970
|
-
allPassed = false;
|
|
971
|
-
console.log(` File: ${report.filePath}`);
|
|
972
|
-
console.log(formatEnforcement(result));
|
|
973
|
-
console.log('');
|
|
974
|
-
}
|
|
975
|
-
}
|
|
976
|
-
if (allPassed) {
|
|
977
|
-
console.log(` Enforcement: PASS (all files checked against thresholds)`);
|
|
978
|
-
}
|
|
979
|
-
else {
|
|
980
|
-
return { reports, exitCode: 1 };
|
|
981
|
-
}
|
|
982
|
-
}
|
|
983
|
-
}
|
|
984
|
-
return { reports, exitCode: 0 };
|
|
985
|
-
}
|
|
986
|
-
// ── kern review <file|dir|--diff base> [--json] [--sarif] [--recursive] [--enforce] [--strict-parse] [--min-coverage=N] [--max-complexity=N] [--export-kern] [--llm] [--fix] [--autofix] [--lint] ──
|
|
987
|
-
if (args[0] === 'review') {
|
|
988
|
-
const jsonOutput = args.includes('--json');
|
|
989
|
-
const sarifOutput = args.includes('--sarif') || args.includes('--format=sarif');
|
|
990
|
-
const recursive = args.includes('--recursive') || args.includes('-r');
|
|
991
|
-
const enforce = args.includes('--enforce');
|
|
992
|
-
const exportKern = args.includes('--export-kern');
|
|
993
|
-
const llmMode = args.includes('--llm');
|
|
994
|
-
const cloudMode = args.includes('--cloud');
|
|
995
|
-
const securityMode = args.includes('--security');
|
|
996
|
-
const mcpMode = args.includes('--mcp');
|
|
997
|
-
const specMode = args.includes('--spec');
|
|
998
|
-
const specFile = args.find(a => a.endsWith('.kern') && a !== 'review');
|
|
999
|
-
const fixMode = args.includes('--fix');
|
|
1000
|
-
const autofixMode = args.includes('--autofix');
|
|
1001
|
-
const lintMode = args.includes('--lint');
|
|
1002
|
-
const graphMode = args.includes('--graph');
|
|
1003
|
-
const batchMode = args.includes('--batch');
|
|
1004
|
-
const maxDepthArg = args.find(a => a.startsWith('--max-depth='))?.split('=')[1];
|
|
1005
|
-
const maxDepth = maxDepthArg ? Number(maxDepthArg) : 3;
|
|
1006
|
-
const batchSizeArg = args.find(a => a.startsWith('--batch-size='))?.split('=')[1];
|
|
1007
|
-
const batchSize = batchSizeArg ? Number(batchSizeArg) : 20;
|
|
1008
|
-
const tsconfigPath = args.find(a => a.startsWith('--tsconfig='))?.split('=')[1];
|
|
1009
|
-
const minCoverageArg = args.find(a => a.startsWith('--min-coverage='))?.split('=')[1];
|
|
1010
|
-
const minCoverage = minCoverageArg ? Number(minCoverageArg) : undefined;
|
|
1011
|
-
const maxComplexityArg = args.find(a => a.startsWith('--max-complexity='))?.split('=')[1];
|
|
1012
|
-
const maxComplexity = maxComplexityArg ? Number(maxComplexityArg) : 15;
|
|
1013
|
-
const maxErrorsArg = args.find(a => a.startsWith('--max-errors='))?.split('=')[1];
|
|
1014
|
-
const maxErrors = maxErrorsArg ? Number(maxErrorsArg) : 0;
|
|
1015
|
-
const maxWarningsArg = args.find(a => a.startsWith('--max-warnings='))?.split('=')[1];
|
|
1016
|
-
const maxWarnings = maxWarningsArg ? Number(maxWarningsArg) : undefined;
|
|
1017
|
-
const showConfidence = args.includes('--confidence');
|
|
1018
|
-
const minConfidenceArg = args.find(a => a.startsWith('--min-confidence='))?.split('=')[1];
|
|
1019
|
-
const minConfidence = minConfidenceArg ? Number(minConfidenceArg) : undefined;
|
|
1020
|
-
const disableRuleArgs = args.filter(a => a.startsWith('--disable-rule=')).map(a => a.split('=')[1]);
|
|
1021
|
-
// --rules-dir: collect custom rule directories (supports --rules-dir=dir and --rules-dir dir)
|
|
1022
|
-
const rulesDirs = [];
|
|
1023
|
-
for (let i = 0; i < args.length; i++) {
|
|
1024
|
-
if (args[i] === '--rules-dir' && args[i + 1] && !args[i + 1].startsWith('--')) {
|
|
1025
|
-
rulesDirs.push(resolve(args[i + 1]));
|
|
1026
|
-
i++; // skip next
|
|
1027
|
-
}
|
|
1028
|
-
else if (args[i].startsWith('--rules-dir=')) {
|
|
1029
|
-
rulesDirs.push(resolve(args[i].split('=')[1]));
|
|
1030
|
-
}
|
|
1031
|
-
}
|
|
1032
|
-
const strictArg = args.find(a => a === '--strict' || a.startsWith('--strict='));
|
|
1033
|
-
const strict = strictArg === '--strict' ? 'inline' : strictArg === '--strict=all' ? 'all' : false;
|
|
1034
|
-
const strictParse = args.includes('--strict-parse');
|
|
1035
|
-
const listRules = args.includes('--list-rules');
|
|
1036
|
-
const diffBase = args.find(a => a.startsWith('--diff'))
|
|
1037
|
-
? (args.find(a => a.startsWith('--diff='))?.split('=')[1] || args[args.indexOf('--diff') + 1] || 'origin/main')
|
|
1038
|
-
: undefined;
|
|
1039
|
-
// --list-rules: print all active rules and exit
|
|
1040
|
-
if (listRules) {
|
|
1041
|
-
const reviewCfg = loadConfig();
|
|
1042
|
-
const targetArg = args.find(a => a.startsWith('--target='))?.split('=')[1];
|
|
1043
|
-
const target = targetArg || reviewCfg.target;
|
|
1044
|
-
const rules = getRuleRegistry(target);
|
|
1045
|
-
const layers = new Map();
|
|
1046
|
-
for (const r of rules) {
|
|
1047
|
-
if (!layers.has(r.layer))
|
|
1048
|
-
layers.set(r.layer, []);
|
|
1049
|
-
layers.get(r.layer).push(r);
|
|
1050
|
-
}
|
|
1051
|
-
console.log(`\n KERN Review Rules (target: ${target}) — ${rules.length} rules active\n`);
|
|
1052
|
-
for (const [layer, layerRules] of layers) {
|
|
1053
|
-
console.log(` [${layer}] (${layerRules.length} rules)`);
|
|
1054
|
-
for (const r of layerRules) {
|
|
1055
|
-
const sev = r.severity === 'error' ? 'ERR' : r.severity === 'warning' ? 'WRN' : 'INF';
|
|
1056
|
-
console.log(` ${sev} ${r.id.padEnd(30)} ${r.description}`);
|
|
1057
|
-
}
|
|
1058
|
-
console.log();
|
|
1059
|
-
}
|
|
1060
|
-
process.exit(0);
|
|
1061
|
-
}
|
|
1062
|
-
// --diff mode: get changed files from git
|
|
1063
|
-
const reviewInputs = args.filter(a => !a.startsWith('--') && a !== 'review');
|
|
1064
|
-
let reviewInput = reviewInputs[0];
|
|
1065
|
-
if (diffBase && !reviewInput) {
|
|
1066
|
-
try {
|
|
1067
|
-
const { execFileSync } = await import('child_process');
|
|
1068
|
-
const sanitizedBase = diffBase.replace(/[^a-zA-Z0-9_.\/\-~]/g, '');
|
|
1069
|
-
const diffFiles = execFileSync('git', ['diff', '--name-only', '--diff-filter=ACMR', sanitizedBase], { encoding: 'utf-8' })
|
|
1070
|
-
.trim()
|
|
1071
|
-
.split('\n')
|
|
1072
|
-
.filter(f => f.endsWith('.ts') || f.endsWith('.tsx'))
|
|
1073
|
-
.filter(f => !f.endsWith('.d.ts') && !f.endsWith('.test.ts'));
|
|
1074
|
-
if (diffFiles.length === 0) {
|
|
1075
|
-
console.log(' No changed .ts/.tsx files since ' + diffBase);
|
|
1076
|
-
process.exit(0);
|
|
1077
|
-
}
|
|
1078
|
-
console.log(` Reviewing ${diffFiles.length} changed files (diff from ${diffBase})\n`);
|
|
1079
|
-
// Process each file individually below
|
|
1080
|
-
reviewInput = '__diff__';
|
|
1081
|
-
globalThis.__diffFiles = diffFiles;
|
|
1082
|
-
}
|
|
1083
|
-
catch (err) {
|
|
1084
|
-
console.error(` git diff failed: ${err.message}`);
|
|
1085
|
-
process.exit(1);
|
|
1086
|
-
}
|
|
1087
|
-
}
|
|
1088
|
-
if (!reviewInput) {
|
|
1089
|
-
console.error('Usage: kern review <file.ts|dir> [--security] [--mcp] [--llm] [--spec file.kern] [--cloud]');
|
|
1090
|
-
console.error(' [--diff base] [--json] [--sarif] [--recursive] [--enforce] [--strict-parse] [--fix] [--autofix] [--rules-dir <dir>]');
|
|
40
|
+
// No command match — default to transpile mode (kern <file.kern> [options])
|
|
41
|
+
// or show help if no input file given
|
|
42
|
+
if (!cmd || cmd.startsWith('--')) {
|
|
43
|
+
printHelp();
|
|
1091
44
|
process.exit(1);
|
|
1092
45
|
}
|
|
1093
|
-
//
|
|
1094
|
-
|
|
1095
|
-
const reviewPath = resolve(reviewInput);
|
|
1096
|
-
const stat = existsSync(reviewPath) ? statSync(reviewPath) : null;
|
|
1097
|
-
if (!stat) {
|
|
1098
|
-
console.error(`Not found: ${reviewInput}`);
|
|
1099
|
-
process.exit(1);
|
|
1100
|
-
}
|
|
1101
|
-
}
|
|
1102
|
-
// Load kern.config.ts to get registered templates and target
|
|
1103
|
-
// Auto-detects target from package.json if no config exists
|
|
1104
|
-
const reviewCfg = loadConfig();
|
|
1105
|
-
if (!VALID_TARGETS.includes(reviewCfg.target)) {
|
|
1106
|
-
console.error(`Invalid target '${reviewCfg.target}' in config. Valid: ${VALID_TARGETS.join(', ')}`);
|
|
1107
|
-
process.exit(1);
|
|
1108
|
-
}
|
|
1109
|
-
if (!jsonOutput && !sarifOutput) {
|
|
1110
|
-
const configExists = existsSync(resolve(process.cwd(), 'kern.config.ts'));
|
|
1111
|
-
if (!configExists) {
|
|
1112
|
-
console.log(` Target: ${reviewCfg.target} (auto-detected from package.json)`);
|
|
1113
|
-
}
|
|
1114
|
-
}
|
|
1115
|
-
// Merge disabledRules from config + CLI flags
|
|
1116
|
-
const cfgDisabledRules = reviewCfg.review.disabledRules ?? [];
|
|
1117
|
-
const mergedDisabledRules = [...new Set([...cfgDisabledRules, ...disableRuleArgs])];
|
|
1118
|
-
const reviewConfig = {
|
|
1119
|
-
registeredTemplates: [],
|
|
1120
|
-
minCoverage: minCoverage ?? 0,
|
|
1121
|
-
enforceTemplates: enforce,
|
|
1122
|
-
maxComplexity: maxComplexity ?? reviewCfg.review.maxComplexity,
|
|
1123
|
-
maxErrors,
|
|
1124
|
-
maxWarnings,
|
|
1125
|
-
target: reviewCfg.target,
|
|
1126
|
-
showConfidence: showConfidence || reviewCfg.review.showConfidence,
|
|
1127
|
-
minConfidence: minConfidence ?? reviewCfg.review.minConfidence,
|
|
1128
|
-
disabledRules: mergedDisabledRules.length > 0 ? mergedDisabledRules : undefined,
|
|
1129
|
-
rulesDirs: rulesDirs.length > 0 ? rulesDirs : undefined,
|
|
1130
|
-
strict,
|
|
1131
|
-
strictParse,
|
|
1132
|
-
};
|
|
1133
|
-
// Load templates and collect their names
|
|
1134
|
-
if (reviewCfg.templates && reviewCfg.templates.length > 0) {
|
|
1135
|
-
clearTemplates();
|
|
1136
|
-
for (const templatePath of reviewCfg.templates) {
|
|
1137
|
-
const resolvedTpl = resolve(process.cwd(), templatePath);
|
|
1138
|
-
if (!existsSync(resolvedTpl))
|
|
1139
|
-
continue;
|
|
1140
|
-
const tplStat = statSync(resolvedTpl);
|
|
1141
|
-
const tplFiles = [];
|
|
1142
|
-
if (tplStat.isDirectory()) {
|
|
1143
|
-
for (const entry of readdirSync(resolvedTpl)) {
|
|
1144
|
-
if (entry.endsWith('.kern'))
|
|
1145
|
-
tplFiles.push(resolve(resolvedTpl, entry));
|
|
1146
|
-
}
|
|
1147
|
-
}
|
|
1148
|
-
else if (resolvedTpl.endsWith('.kern')) {
|
|
1149
|
-
tplFiles.push(resolvedTpl);
|
|
1150
|
-
}
|
|
1151
|
-
for (const file of tplFiles) {
|
|
1152
|
-
try {
|
|
1153
|
-
const source = readFileSync(file, 'utf-8');
|
|
1154
|
-
const ast = parseAndSurface(source, file);
|
|
1155
|
-
const nodes = ast.type === 'template' ? [ast] : (ast.children || []).filter(n => n.type === 'template');
|
|
1156
|
-
for (const node of nodes) {
|
|
1157
|
-
const tplName = node.props?.name;
|
|
1158
|
-
if (tplName)
|
|
1159
|
-
reviewConfig.registeredTemplates.push(tplName);
|
|
1160
|
-
registerTemplate(node, file);
|
|
1161
|
-
}
|
|
1162
|
-
}
|
|
1163
|
-
catch (e) {
|
|
1164
|
-
console.error(` Warning: Failed to parse template ${basename(file)}: ${e.message}`);
|
|
1165
|
-
}
|
|
1166
|
-
}
|
|
1167
|
-
}
|
|
1168
|
-
if (reviewConfig.registeredTemplates.length > 0) {
|
|
1169
|
-
console.log(` Templates loaded: ${reviewConfig.registeredTemplates.join(', ')}`);
|
|
1170
|
-
}
|
|
1171
|
-
}
|
|
1172
|
-
// Collect reports from diff, directory, or single file
|
|
1173
|
-
let reports = [];
|
|
1174
|
-
// Collect entry file paths for --graph mode
|
|
1175
|
-
let entryFilePaths = [];
|
|
1176
|
-
if (reviewInput === '__diff__') {
|
|
1177
|
-
const diffFiles = globalThis.__diffFiles;
|
|
1178
|
-
entryFilePaths = diffFiles.map(f => resolve(f)).filter(f => existsSync(f));
|
|
1179
|
-
}
|
|
1180
|
-
else {
|
|
1181
|
-
const paths = reviewInputs.length > 0 ? reviewInputs : [reviewInput];
|
|
1182
|
-
for (const p of paths) {
|
|
1183
|
-
const rPath = resolve(p);
|
|
1184
|
-
if (!existsSync(rPath))
|
|
1185
|
-
continue;
|
|
1186
|
-
const rStat = statSync(rPath);
|
|
1187
|
-
if (rStat.isDirectory()) {
|
|
1188
|
-
// Collect files from directory for graph seeding
|
|
1189
|
-
entryFilePaths.push(...collectTsFilesFlat(rPath, recursive));
|
|
1190
|
-
}
|
|
1191
|
-
else {
|
|
1192
|
-
entryFilePaths.push(rPath);
|
|
1193
|
-
}
|
|
1194
|
-
}
|
|
1195
|
-
}
|
|
1196
|
-
const modes = {
|
|
1197
|
-
graphMode, batchMode, llmMode, cloudMode, securityMode, mcpMode, specMode, fixMode, autofixMode, lintMode, exportKern, enforce,
|
|
1198
|
-
jsonOutput, sarifOutput, strictParse, maxDepth, batchSize, tsconfigPath, specFile, minCoverageArg, maxComplexityArg,
|
|
1199
|
-
maxErrorsArg, maxWarningsArg, showConfidence
|
|
1200
|
-
};
|
|
1201
|
-
const noCache = args.includes('--no-cache');
|
|
1202
|
-
if (noCache) {
|
|
1203
|
-
clearReviewCache();
|
|
1204
|
-
reviewConfig.noCache = true;
|
|
1205
|
-
}
|
|
1206
|
-
const watchMode = args.includes('--watch') || args.includes('-w');
|
|
1207
|
-
if (watchMode) {
|
|
1208
|
-
const chokidar = await import('chokidar');
|
|
1209
|
-
console.log(`\n KERN review — watching ${entryFilePaths.length} entry points`);
|
|
1210
|
-
let debounceTimer = null;
|
|
1211
|
-
const run = async (paths) => {
|
|
1212
|
-
console.clear();
|
|
1213
|
-
console.log(`\n KERN review — watching (${paths.length} file${paths.length === 1 ? '' : 's'})\n`);
|
|
1214
|
-
const watchModes = { ...modes, llmMode: false, enforce: false };
|
|
1215
|
-
await runReviewPipeline(reviewConfig, paths, watchModes);
|
|
1216
|
-
console.log('\n Watching for changes...');
|
|
1217
|
-
};
|
|
1218
|
-
const watcher = chokidar.watch(entryFilePaths, {
|
|
1219
|
-
persistent: true,
|
|
1220
|
-
awaitWriteFinish: { stabilityThreshold: 300 }
|
|
1221
|
-
});
|
|
1222
|
-
watcher.on('change', (path) => {
|
|
1223
|
-
if (debounceTimer)
|
|
1224
|
-
clearTimeout(debounceTimer);
|
|
1225
|
-
debounceTimer = setTimeout(() => run([path]), 300);
|
|
1226
|
-
});
|
|
1227
|
-
// Initial run
|
|
1228
|
-
await run(entryFilePaths);
|
|
1229
|
-
}
|
|
1230
|
-
else {
|
|
1231
|
-
const result = await runReviewPipeline(reviewConfig, entryFilePaths, modes);
|
|
1232
|
-
process.exit(result.exitCode);
|
|
1233
|
-
}
|
|
1234
|
-
}
|
|
1235
|
-
// ── kern evolve <dir|file> [options] ──────────────────────────────────
|
|
1236
|
-
if (args[0] === 'evolve' && args[1] !== undefined && !args[1].startsWith('evolve:')) {
|
|
1237
|
-
const evolveInput = args[1];
|
|
1238
|
-
if (!evolveInput || evolveInput.startsWith('--')) {
|
|
1239
|
-
console.error('Usage: kern evolve <dir|file> [--recursive] [--preview] [--min-confidence=N] [--min-support=N] [--json]');
|
|
1240
|
-
process.exit(1);
|
|
1241
|
-
}
|
|
1242
|
-
const evolvePath = resolve(evolveInput);
|
|
1243
|
-
const stat = existsSync(evolvePath) ? statSync(evolvePath) : null;
|
|
1244
|
-
if (!stat) {
|
|
1245
|
-
console.error(`Not found: ${evolveInput}`);
|
|
1246
|
-
process.exit(1);
|
|
1247
|
-
}
|
|
1248
|
-
const recursive = args.includes('--recursive') || args.includes('-r');
|
|
1249
|
-
const preview = args.includes('--preview');
|
|
1250
|
-
const jsonOutput = args.includes('--json');
|
|
1251
|
-
const minConfArg = args.find(a => a.startsWith('--min-confidence='))?.split('=')[1];
|
|
1252
|
-
const minSupportArg = args.find(a => a.startsWith('--min-support='))?.split('=')[1];
|
|
1253
|
-
const enableNodes = args.includes('--nodes') || args.includes('--from-gaps');
|
|
1254
|
-
const evolveOptions = {
|
|
1255
|
-
recursive,
|
|
1256
|
-
preview,
|
|
1257
|
-
enableNodeProposals: enableNodes,
|
|
1258
|
-
thresholds: {
|
|
1259
|
-
...(minConfArg ? { minConfidence: Number(minConfArg) } : {}),
|
|
1260
|
-
...(minSupportArg ? { minSupport: Number(minSupportArg) } : {}),
|
|
1261
|
-
},
|
|
1262
|
-
};
|
|
1263
|
-
// Load built-in detectors
|
|
1264
|
-
await loadBuiltinDetectors();
|
|
1265
|
-
console.log(`\n KERN evolve — scanning for template gaps\n`);
|
|
1266
|
-
console.log(` Input: ${relative(process.cwd(), evolvePath) || '.'}`);
|
|
1267
|
-
console.log(` Mode: ${preview ? 'preview (no staging)' : 'detect + stage'}`);
|
|
1268
|
-
console.log('');
|
|
1269
|
-
const result = evolve(evolvePath, evolveOptions);
|
|
1270
|
-
if (jsonOutput) {
|
|
1271
|
-
console.log(JSON.stringify(result, null, 2));
|
|
1272
|
-
}
|
|
1273
|
-
else {
|
|
1274
|
-
console.log(` Gaps detected: ${result.gaps.length}`);
|
|
1275
|
-
if (result.conceptSummary) {
|
|
1276
|
-
console.log(` ${result.conceptSummary.formatted}`);
|
|
1277
|
-
}
|
|
1278
|
-
console.log(` Patterns analyzed: ${result.analyzed.length}`);
|
|
1279
|
-
console.log(` Templates proposed: ${result.proposals.length}`);
|
|
1280
|
-
console.log(` Validated: ${result.validated.filter(v => v.validation.parseOk && v.validation.expansionOk).length}/${result.validated.length}`);
|
|
1281
|
-
if (!preview && result.staged.length > 0) {
|
|
1282
|
-
console.log(` Staged for review: ${result.staged.length}`);
|
|
1283
|
-
console.log(`\n Run 'kern evolve:review --list' to review proposals.`);
|
|
1284
|
-
}
|
|
1285
|
-
if (result.proposals.length > 0 && !jsonOutput) {
|
|
1286
|
-
console.log('\n Proposed templates:');
|
|
1287
|
-
for (const p of result.proposals) {
|
|
1288
|
-
const v = result.validated.find(v => v.proposal.id === p.id);
|
|
1289
|
-
const status = v ? (v.validation.parseOk && v.validation.expansionOk ? '✓' : '✗') : '?';
|
|
1290
|
-
console.log(` ${status} ${p.templateName} (${p.namespace}) — score: ${p.qualityScore.overallScore}, instances: ${p.instanceCount}`);
|
|
1291
|
-
}
|
|
1292
|
-
}
|
|
1293
|
-
// v3: Node proposals
|
|
1294
|
-
if (result.nodeProposals && result.nodeProposals.length > 0 && !jsonOutput) {
|
|
1295
|
-
console.log(`\n Node proposals (v3): ${result.nodeProposals.length}`);
|
|
1296
|
-
for (const np of result.nodeProposals) {
|
|
1297
|
-
const nv = result.nodeValidated?.find(v => v.proposal.id === np.id);
|
|
1298
|
-
const status = nv ? (nv.validation.parseOk && nv.validation.codegenOk ? '✓' : '✗') : '?';
|
|
1299
|
-
console.log(` ${status} ${np.nodeName} — express: ${np.expressibilityScore.overall}, freq: ${np.frequency}, score: ${np.qualityScore}`);
|
|
1300
|
-
}
|
|
1301
|
-
if (result.stagedNodes && result.stagedNodes.length > 0) {
|
|
1302
|
-
console.log(` Staged nodes: ${result.stagedNodes.length}`);
|
|
1303
|
-
}
|
|
1304
|
-
}
|
|
1305
|
-
}
|
|
1306
|
-
console.log('');
|
|
1307
|
-
process.exit(0);
|
|
1308
|
-
}
|
|
1309
|
-
// ── kern evolve:review [options] ─────────────────────────────────────
|
|
1310
|
-
if (args[0] === 'evolve:review') {
|
|
1311
|
-
const listMode = args.includes('--list') || args.length === 1;
|
|
1312
|
-
const approveId = (() => {
|
|
1313
|
-
const eqArg = args.find(a => a.startsWith('--approve='));
|
|
1314
|
-
if (eqArg)
|
|
1315
|
-
return eqArg.split('=')[1];
|
|
1316
|
-
const idx = args.indexOf('--approve');
|
|
1317
|
-
return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : undefined;
|
|
1318
|
-
})();
|
|
1319
|
-
const rejectId = (() => {
|
|
1320
|
-
const eqArg = args.find(a => a.startsWith('--reject='));
|
|
1321
|
-
if (eqArg)
|
|
1322
|
-
return eqArg.split('=')[1];
|
|
1323
|
-
const idx = args.indexOf('--reject');
|
|
1324
|
-
return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : undefined;
|
|
1325
|
-
})();
|
|
1326
|
-
const promoteMode = args.includes('--promote');
|
|
1327
|
-
const isLocal = args.includes('--local') || !args.includes('--catalog');
|
|
1328
|
-
if (approveId) {
|
|
1329
|
-
const updated = updateStagedStatus(approveId, 'approved');
|
|
1330
|
-
if (updated) {
|
|
1331
|
-
console.log(` Approved: ${approveId}`);
|
|
1332
|
-
}
|
|
1333
|
-
else {
|
|
1334
|
-
console.error(` Not found: ${approveId}`);
|
|
1335
|
-
process.exit(1);
|
|
1336
|
-
}
|
|
1337
|
-
process.exit(0);
|
|
1338
|
-
}
|
|
1339
|
-
if (rejectId) {
|
|
1340
|
-
const updated = updateStagedStatus(rejectId, 'rejected');
|
|
1341
|
-
if (updated) {
|
|
1342
|
-
console.log(` Rejected: ${rejectId}`);
|
|
1343
|
-
cleanRejected();
|
|
1344
|
-
}
|
|
1345
|
-
else {
|
|
1346
|
-
console.error(` Not found: ${rejectId}`);
|
|
1347
|
-
process.exit(1);
|
|
1348
|
-
}
|
|
1349
|
-
process.exit(0);
|
|
1350
|
-
}
|
|
1351
|
-
if (promoteMode) {
|
|
1352
|
-
if (!isLocal) {
|
|
1353
|
-
console.log(' Catalog promotion is for contributors who want to upstream templates.');
|
|
1354
|
-
console.log(' Use --local (default) to write templates to your project.');
|
|
1355
|
-
process.exit(0);
|
|
1356
|
-
}
|
|
1357
|
-
const promoted = promoteLocal();
|
|
1358
|
-
if (promoted.length === 0) {
|
|
1359
|
-
console.log(' No approved proposals to promote.');
|
|
1360
|
-
}
|
|
1361
|
-
else {
|
|
1362
|
-
console.log(` Promoted ${promoted.length} template(s) to templates/:`);
|
|
1363
|
-
for (const name of promoted) {
|
|
1364
|
-
console.log(` ${name}`);
|
|
1365
|
-
}
|
|
1366
|
-
}
|
|
1367
|
-
process.exit(0);
|
|
1368
|
-
}
|
|
1369
|
-
// Default: list mode
|
|
1370
|
-
const staged = listStaged();
|
|
1371
|
-
if (staged.length === 0) {
|
|
1372
|
-
console.log(' No staged proposals. Run \'kern evolve <dir>\' to detect gaps.');
|
|
1373
|
-
process.exit(0);
|
|
1374
|
-
}
|
|
1375
|
-
console.log(`\n KERN evolve:review — ${staged.length} proposal(s)\n`);
|
|
1376
|
-
for (const s of staged) {
|
|
1377
|
-
console.log(formatSplitView(s));
|
|
1378
|
-
console.log('');
|
|
1379
|
-
}
|
|
1380
|
-
process.exit(0);
|
|
1381
|
-
}
|
|
1382
|
-
// ── kern evolve:discover <dir> [--recursive] [--provider=openai|ollama] [--max-tokens=N] ──
|
|
1383
|
-
if (args[0] === 'evolve:discover') {
|
|
1384
|
-
const discoverInput = args[1];
|
|
1385
|
-
if (!discoverInput || discoverInput.startsWith('--')) {
|
|
1386
|
-
console.error('Usage: kern evolve:discover <dir> [--recursive] [--provider=openai|anthropic|ollama] [--max-tokens=N]');
|
|
1387
|
-
process.exit(1);
|
|
1388
|
-
}
|
|
1389
|
-
const discoverPath = resolve(discoverInput);
|
|
1390
|
-
if (!existsSync(discoverPath)) {
|
|
1391
|
-
console.error(`Not found: ${discoverInput}`);
|
|
1392
|
-
process.exit(1);
|
|
1393
|
-
}
|
|
1394
|
-
const recursive = args.includes('--recursive') || args.includes('-r');
|
|
1395
|
-
const providerArg = args.find(a => a.startsWith('--provider='))?.split('=')[1];
|
|
1396
|
-
const maxTokensArg = args.find(a => a.startsWith('--max-tokens='))?.split('=')[1];
|
|
1397
|
-
const maxTokens = maxTokensArg ? Number(maxTokensArg) : 100000;
|
|
1398
|
-
console.log(`\n KERN evolve:discover — LLM pattern discovery\n`);
|
|
1399
|
-
console.log(` Input: ${relative(process.cwd(), discoverPath) || '.'}`);
|
|
1400
|
-
// Collect files and batch
|
|
1401
|
-
const tsFiles = collectTsFiles(discoverPath, recursive);
|
|
1402
|
-
console.log(` Files found: ${tsFiles.length}`);
|
|
1403
|
-
if (tsFiles.length === 0) {
|
|
1404
|
-
console.log(' No TypeScript files to analyze.');
|
|
1405
|
-
process.exit(0);
|
|
1406
|
-
}
|
|
1407
|
-
const batches = selectRepresentativeFiles(tsFiles);
|
|
1408
|
-
console.log(` Batches: ${batches.length} (sampling representative files)`);
|
|
1409
|
-
// Load existing evolved keywords for dedup
|
|
1410
|
-
const manifest = readEvolvedManifest();
|
|
1411
|
-
const evolvedKeywords = manifest ? Object.keys(manifest.nodes) : [];
|
|
1412
|
-
// Create LLM provider
|
|
1413
|
-
let provider;
|
|
1414
|
-
try {
|
|
1415
|
-
provider = createLLMProvider({ provider: providerArg });
|
|
1416
|
-
}
|
|
1417
|
-
catch (err) {
|
|
1418
|
-
console.error(` ${err.message}`);
|
|
1419
|
-
process.exit(1);
|
|
1420
|
-
}
|
|
1421
|
-
console.log(` Provider: ${provider.name}`);
|
|
1422
|
-
const budget = new TokenBudget(maxTokens);
|
|
1423
|
-
const allProposals = [];
|
|
1424
|
-
const runId = `run-${Date.now()}`;
|
|
1425
|
-
for (let i = 0; i < batches.length; i++) {
|
|
1426
|
-
if (budget.exhausted) {
|
|
1427
|
-
console.log(` Token budget exhausted (${budget}). Stopping.`);
|
|
1428
|
-
break;
|
|
1429
|
-
}
|
|
1430
|
-
const batch = batches[i];
|
|
1431
|
-
const files = batch.map(fp => ({
|
|
1432
|
-
path: relative(process.cwd(), fp),
|
|
1433
|
-
content: readFileSync(fp, 'utf-8'),
|
|
1434
|
-
}));
|
|
1435
|
-
const prompt = buildDiscoveryPrompt(files, NODE_TYPES, evolvedKeywords);
|
|
1436
|
-
budget.add(estimateTokens(prompt));
|
|
1437
|
-
console.log(` Batch ${i + 1}/${batches.length}: ${files.map(f => f.path).join(', ')}`);
|
|
1438
|
-
try {
|
|
1439
|
-
const response = await provider.complete(prompt);
|
|
1440
|
-
budget.add(estimateTokens(response));
|
|
1441
|
-
const proposals = parseDiscoveryResponse(response, runId);
|
|
1442
|
-
allProposals.push(...proposals);
|
|
1443
|
-
if (proposals.length > 0) {
|
|
1444
|
-
console.log(` → ${proposals.length} pattern(s) found: ${proposals.map(p => p.keyword).join(', ')}`);
|
|
1445
|
-
}
|
|
1446
|
-
}
|
|
1447
|
-
catch (err) {
|
|
1448
|
-
console.error(` → Error: ${err.message}`);
|
|
1449
|
-
}
|
|
1450
|
-
}
|
|
1451
|
-
// Dedup across batches
|
|
1452
|
-
const seen = new Set();
|
|
1453
|
-
const uniqueProposals = allProposals.filter(p => {
|
|
1454
|
-
if (seen.has(p.keyword))
|
|
1455
|
-
return false;
|
|
1456
|
-
seen.add(p.keyword);
|
|
1457
|
-
return true;
|
|
1458
|
-
});
|
|
1459
|
-
console.log(`\n Discovery complete.`);
|
|
1460
|
-
console.log(` Tokens used: ${budget}`);
|
|
1461
|
-
console.log(` Proposals: ${uniqueProposals.length}\n`);
|
|
1462
|
-
// Validate and stage proposals (with LLM retry on failure, max 2 retries)
|
|
1463
|
-
const existingKw = [...NODE_TYPES, ...evolvedKeywords];
|
|
1464
|
-
let stagedCount = 0;
|
|
1465
|
-
const maxRetries = 2;
|
|
1466
|
-
for (let pi = 0; pi < uniqueProposals.length; pi++) {
|
|
1467
|
-
let proposal = uniqueProposals[pi];
|
|
1468
|
-
// Assign ID if missing
|
|
1469
|
-
if (!proposal.id) {
|
|
1470
|
-
proposal.id = `${proposal.keyword}-${Date.now()}`;
|
|
1471
|
-
}
|
|
1472
|
-
let validation = validateEvolveProposal(proposal, existingKw);
|
|
1473
|
-
let allOk = validation.schemaOk && validation.keywordOk && validation.parseOk && validation.codegenCompileOk && validation.codegenRunOk;
|
|
1474
|
-
// Retry on fixable failures (parse, codegen, typescript, golden diff)
|
|
1475
|
-
const isFixable = validation.schemaOk && validation.keywordOk && !allOk;
|
|
1476
|
-
if (!allOk && isFixable && provider) {
|
|
1477
|
-
for (let retry = 1; retry <= maxRetries; retry++) {
|
|
1478
|
-
console.log(` \u21BB ${proposal.keyword} — retry ${retry}/${maxRetries} (feeding errors to LLM)`);
|
|
1479
|
-
try {
|
|
1480
|
-
const retryPrompt = buildRetryPrompt(proposal, validation.errors);
|
|
1481
|
-
budget.add(estimateTokens(retryPrompt));
|
|
1482
|
-
const retryResponse = await provider.complete(retryPrompt);
|
|
1483
|
-
if (!retryResponse || typeof retryResponse !== 'string')
|
|
1484
|
-
throw new Error('LLM returned empty or invalid response');
|
|
1485
|
-
budget.add(estimateTokens(retryResponse));
|
|
1486
|
-
// Parse retry response as single object
|
|
1487
|
-
let json = retryResponse.trim();
|
|
1488
|
-
const fenceMatch = json.match(/```(?:json)?\s*\n?([\s\S]*?)```/);
|
|
1489
|
-
if (fenceMatch)
|
|
1490
|
-
json = fenceMatch[1].trim();
|
|
1491
|
-
const objStart = json.indexOf('{');
|
|
1492
|
-
const objEnd = json.lastIndexOf('}');
|
|
1493
|
-
if (objStart !== -1 && objEnd > objStart)
|
|
1494
|
-
json = json.slice(objStart, objEnd + 1);
|
|
1495
|
-
const fixed = JSON.parse(json);
|
|
1496
|
-
if (typeof fixed !== 'object' || fixed === null)
|
|
1497
|
-
throw new Error('LLM retry response is not a JSON object');
|
|
1498
|
-
// Merge fixes into proposal
|
|
1499
|
-
if (typeof fixed.kernExample === 'string')
|
|
1500
|
-
proposal.kernExample = fixed.kernExample;
|
|
1501
|
-
if (typeof fixed.expectedOutput === 'string')
|
|
1502
|
-
proposal.expectedOutput = fixed.expectedOutput;
|
|
1503
|
-
if (typeof fixed.codegenSource === 'string')
|
|
1504
|
-
proposal.codegenSource = fixed.codegenSource;
|
|
1505
|
-
validation = validateEvolveProposal(proposal, existingKw);
|
|
1506
|
-
allOk = validation.schemaOk && validation.keywordOk && validation.parseOk && validation.codegenCompileOk && validation.codegenRunOk;
|
|
1507
|
-
if (allOk)
|
|
1508
|
-
break;
|
|
1509
|
-
}
|
|
1510
|
-
catch (e) {
|
|
1511
|
-
console.error(` Retry ${retry} failed: ${e.message}`);
|
|
1512
|
-
}
|
|
1513
|
-
}
|
|
1514
|
-
}
|
|
1515
|
-
const status = allOk ? '\u2713' : '\u2717';
|
|
1516
|
-
console.log(` ${status} ${proposal.keyword} — ${proposal.reason.observation}`);
|
|
1517
|
-
if (validation.errors.length > 0) {
|
|
1518
|
-
for (const err of validation.errors.slice(0, 3)) {
|
|
1519
|
-
console.log(` ${err}`);
|
|
1520
|
-
}
|
|
1521
|
-
}
|
|
1522
|
-
// Stage proposals for review (including failed ones — user can inspect)
|
|
1523
|
-
validation.retryCount = allOk ? 0 : maxRetries;
|
|
1524
|
-
stageEvolveV4Proposal(proposal, validation);
|
|
1525
|
-
stagedCount++;
|
|
1526
|
-
}
|
|
1527
|
-
if (stagedCount > 0) {
|
|
1528
|
-
console.log(`\n Staged ${stagedCount} proposal(s).`);
|
|
1529
|
-
console.log(` Run 'kern evolve:review-v4' to review and graduate proposals.`);
|
|
1530
|
-
}
|
|
1531
|
-
console.log('');
|
|
1532
|
-
process.exit(0);
|
|
1533
|
-
}
|
|
1534
|
-
// ── kern evolve:review-v4 [--list] [--approve=<id>] [--reject=<id>] [--detail=<id>] ──
|
|
1535
|
-
if (args[0] === 'evolve:review-v4') {
|
|
1536
|
-
const approveV4Id = (() => {
|
|
1537
|
-
const eqArg = args.find(a => a.startsWith('--approve='));
|
|
1538
|
-
if (eqArg)
|
|
1539
|
-
return eqArg.split('=')[1];
|
|
1540
|
-
const idx = args.indexOf('--approve');
|
|
1541
|
-
return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : undefined;
|
|
1542
|
-
})();
|
|
1543
|
-
const rejectV4Id = (() => {
|
|
1544
|
-
const eqArg = args.find(a => a.startsWith('--reject='));
|
|
1545
|
-
if (eqArg)
|
|
1546
|
-
return eqArg.split('=')[1];
|
|
1547
|
-
const idx = args.indexOf('--reject');
|
|
1548
|
-
return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : undefined;
|
|
1549
|
-
})();
|
|
1550
|
-
const detailV4Id = (() => {
|
|
1551
|
-
const eqArg = args.find(a => a.startsWith('--detail='));
|
|
1552
|
-
if (eqArg)
|
|
1553
|
-
return eqArg.split('=')[1];
|
|
1554
|
-
const idx = args.indexOf('--detail');
|
|
1555
|
-
return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : undefined;
|
|
1556
|
-
})();
|
|
1557
|
-
if (approveV4Id) {
|
|
1558
|
-
// Approve → validate → compile → graduate
|
|
1559
|
-
const staged = getStagedEvolveV4(approveV4Id);
|
|
1560
|
-
if (!staged) {
|
|
1561
|
-
console.error(` Not found: ${approveV4Id}`);
|
|
1562
|
-
process.exit(1);
|
|
1563
|
-
}
|
|
1564
|
-
const { proposal, validation } = staged;
|
|
1565
|
-
const allOk = validation.schemaOk && validation.keywordOk && validation.parseOk && validation.codegenCompileOk && validation.codegenRunOk;
|
|
1566
|
-
if (!allOk) {
|
|
1567
|
-
console.error(` Cannot approve — validation failed for '${proposal.keyword}':`);
|
|
1568
|
-
for (const err of validation.errors) {
|
|
1569
|
-
console.error(` ${err}`);
|
|
1570
|
-
}
|
|
1571
|
-
process.exit(1);
|
|
1572
|
-
}
|
|
1573
|
-
// Compile codegen source to JS
|
|
1574
|
-
let compiledJs;
|
|
1575
|
-
try {
|
|
1576
|
-
compiledJs = compileCodegenToJS(proposal.codegenSource);
|
|
1577
|
-
}
|
|
1578
|
-
catch (err) {
|
|
1579
|
-
console.error(` Failed to compile codegen for '${proposal.keyword}': ${err.message}`);
|
|
1580
|
-
process.exit(1);
|
|
1581
|
-
}
|
|
1582
|
-
// Graduate the node
|
|
1583
|
-
const result = graduateNode(proposal, compiledJs);
|
|
1584
|
-
if (result.success) {
|
|
1585
|
-
updateStagedEvolveV4Status(approveV4Id, 'approved');
|
|
1586
|
-
cleanApprovedEvolveV4(approveV4Id);
|
|
1587
|
-
console.log(` Graduated '${proposal.keyword}' → ${result.path}`);
|
|
1588
|
-
console.log(` The node is now available in kern compile and kern dev.`);
|
|
1589
|
-
}
|
|
1590
|
-
else {
|
|
1591
|
-
console.error(` Graduation failed: ${result.error}`);
|
|
1592
|
-
process.exit(1);
|
|
1593
|
-
}
|
|
1594
|
-
process.exit(0);
|
|
1595
|
-
}
|
|
1596
|
-
if (rejectV4Id) {
|
|
1597
|
-
const updated = updateStagedEvolveV4Status(rejectV4Id, 'rejected');
|
|
1598
|
-
if (updated) {
|
|
1599
|
-
console.log(` Rejected: ${updated.proposal.keyword} (${rejectV4Id})`);
|
|
1600
|
-
cleanRejectedEvolveV4();
|
|
1601
|
-
}
|
|
1602
|
-
else {
|
|
1603
|
-
console.error(` Not found: ${rejectV4Id}`);
|
|
1604
|
-
process.exit(1);
|
|
1605
|
-
}
|
|
1606
|
-
process.exit(0);
|
|
1607
|
-
}
|
|
1608
|
-
if (detailV4Id) {
|
|
1609
|
-
const staged = getStagedEvolveV4(detailV4Id);
|
|
1610
|
-
if (!staged) {
|
|
1611
|
-
console.error(` Not found: ${detailV4Id}`);
|
|
1612
|
-
process.exit(1);
|
|
1613
|
-
}
|
|
1614
|
-
const { proposal, validation } = staged;
|
|
1615
|
-
console.log(`\n DETAIL: ${proposal.keyword} (${proposal.displayName})\n`);
|
|
1616
|
-
console.log(` Description: ${proposal.description}`);
|
|
1617
|
-
console.log(` Props: ${proposal.props.map(p => `${p.name}:${p.type}${p.required ? '*' : ''}`).join(', ')}`);
|
|
1618
|
-
console.log(` Child types: ${proposal.childTypes.join(', ') || '(none)'}`);
|
|
1619
|
-
console.log(` Codegen tier: ${proposal.codegenTier}`);
|
|
1620
|
-
console.log(` Run ID: ${proposal.evolveRunId}`);
|
|
1621
|
-
if (proposal.parserHints) {
|
|
1622
|
-
console.log(` Parser hints: ${JSON.stringify(proposal.parserHints)}`);
|
|
1623
|
-
}
|
|
1624
|
-
console.log(`\n --- Codegen Source ---`);
|
|
1625
|
-
console.log(proposal.codegenSource);
|
|
1626
|
-
console.log(` --- Instances (${proposal.reason.instances.length}) ---`);
|
|
1627
|
-
for (const inst of proposal.reason.instances.slice(0, 10)) {
|
|
1628
|
-
console.log(` ${inst}`);
|
|
1629
|
-
}
|
|
1630
|
-
if (validation.errors.length > 0) {
|
|
1631
|
-
console.log(`\n --- Validation Errors ---`);
|
|
1632
|
-
for (const err of validation.errors) {
|
|
1633
|
-
console.log(` ${err}`);
|
|
1634
|
-
}
|
|
1635
|
-
}
|
|
1636
|
-
console.log('');
|
|
1637
|
-
process.exit(0);
|
|
1638
|
-
}
|
|
1639
|
-
// Default: interactive review or list mode
|
|
1640
|
-
const stagedV4 = listStagedEvolveV4();
|
|
1641
|
-
const pendingV4 = stagedV4.filter(s => s.status === 'pending');
|
|
1642
|
-
if (pendingV4.length === 0) {
|
|
1643
|
-
console.log(' No pending v4 proposals. Run \'kern evolve:discover <dir>\' to find patterns.');
|
|
1644
|
-
process.exit(0);
|
|
1645
|
-
}
|
|
1646
|
-
const listOnly = args.includes('--list');
|
|
1647
|
-
if (listOnly) {
|
|
1648
|
-
console.log(`\n KERN evolve:review-v4 — ${pendingV4.length} proposal(s)\n`);
|
|
1649
|
-
for (const s of pendingV4) {
|
|
1650
|
-
console.log(formatEvolveV4SplitView(s));
|
|
1651
|
-
console.log('');
|
|
1652
|
-
}
|
|
1653
|
-
process.exit(0);
|
|
1654
|
-
}
|
|
1655
|
-
// Interactive review — walk through each proposal
|
|
1656
|
-
const { createInterface } = await import('readline');
|
|
1657
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
1658
|
-
const ask = (q) => new Promise(res => rl.question(q, res));
|
|
1659
|
-
console.log(`\n KERN evolve:review-v4 — ${pendingV4.length} proposal(s)\n`);
|
|
1660
|
-
for (const staged of pendingV4) {
|
|
1661
|
-
console.log(formatEvolveV4SplitView(staged));
|
|
1662
|
-
console.log('');
|
|
1663
|
-
let decided = false;
|
|
1664
|
-
while (!decided) {
|
|
1665
|
-
const answer = (await ask(' [a]pprove [r]eject [s]kip [d]etail [q]uit > ')).trim().toLowerCase();
|
|
1666
|
-
if (answer === 'a' || answer === 'approve') {
|
|
1667
|
-
const { proposal, validation } = staged;
|
|
1668
|
-
const allOk = validation.schemaOk && validation.keywordOk && validation.parseOk && validation.codegenCompileOk && validation.codegenRunOk;
|
|
1669
|
-
if (!allOk) {
|
|
1670
|
-
console.log(` Cannot approve — validation failed. Use [d]etail to see errors.\n`);
|
|
1671
|
-
continue;
|
|
1672
|
-
}
|
|
1673
|
-
try {
|
|
1674
|
-
const compiledJs = compileCodegenToJS(proposal.codegenSource);
|
|
1675
|
-
const result = graduateNode(proposal, compiledJs);
|
|
1676
|
-
if (result.success) {
|
|
1677
|
-
updateStagedEvolveV4Status(staged.id, 'approved');
|
|
1678
|
-
cleanApprovedEvolveV4(staged.id);
|
|
1679
|
-
console.log(` \u2713 Graduated '${proposal.keyword}'\n`);
|
|
1680
|
-
}
|
|
1681
|
-
else {
|
|
1682
|
-
console.log(` Graduation failed: ${result.error}\n`);
|
|
1683
|
-
}
|
|
1684
|
-
}
|
|
1685
|
-
catch (err) {
|
|
1686
|
-
console.log(` Error: ${err.message}\n`);
|
|
1687
|
-
}
|
|
1688
|
-
decided = true;
|
|
1689
|
-
}
|
|
1690
|
-
else if (answer === 'r' || answer === 'reject') {
|
|
1691
|
-
updateStagedEvolveV4Status(staged.id, 'rejected');
|
|
1692
|
-
cleanRejectedEvolveV4();
|
|
1693
|
-
console.log(` \u2717 Rejected '${staged.proposal.keyword}'\n`);
|
|
1694
|
-
decided = true;
|
|
1695
|
-
}
|
|
1696
|
-
else if (answer === 's' || answer === 'skip') {
|
|
1697
|
-
console.log(` Skipped.\n`);
|
|
1698
|
-
decided = true;
|
|
1699
|
-
}
|
|
1700
|
-
else if (answer === 'd' || answer === 'detail') {
|
|
1701
|
-
const { proposal, validation } = staged;
|
|
1702
|
-
console.log(`\n --- Codegen Source ---`);
|
|
1703
|
-
console.log(proposal.codegenSource);
|
|
1704
|
-
console.log(` --- Instances (${proposal.reason.instances.length}) ---`);
|
|
1705
|
-
for (const inst of proposal.reason.instances.slice(0, 5)) {
|
|
1706
|
-
console.log(` ${inst}`);
|
|
1707
|
-
}
|
|
1708
|
-
if (validation.errors.length > 0) {
|
|
1709
|
-
console.log(` --- Errors ---`);
|
|
1710
|
-
for (const err of validation.errors) {
|
|
1711
|
-
console.log(` ${err}`);
|
|
1712
|
-
}
|
|
1713
|
-
}
|
|
1714
|
-
console.log('');
|
|
1715
|
-
}
|
|
1716
|
-
else if (answer === 'q' || answer === 'quit') {
|
|
1717
|
-
rl.close();
|
|
1718
|
-
process.exit(0);
|
|
1719
|
-
}
|
|
1720
|
-
}
|
|
1721
|
-
}
|
|
1722
|
-
rl.close();
|
|
1723
|
-
console.log(' Review complete.\n');
|
|
1724
|
-
process.exit(0);
|
|
1725
|
-
}
|
|
1726
|
-
// ── kern evolve:test ─────────────────────────────────────────────────
|
|
1727
|
-
if (args[0] === 'evolve:test') {
|
|
1728
|
-
console.log('\n KERN evolve:test — golden test runner\n');
|
|
1729
|
-
const results = runGoldenTests();
|
|
1730
|
-
console.log(formatGoldenTestResults(results));
|
|
1731
|
-
const failed = results.filter(r => !r.pass).length;
|
|
1732
|
-
console.log('');
|
|
1733
|
-
process.exit(failed > 0 ? 1 : 0);
|
|
1734
|
-
}
|
|
1735
|
-
// ── kern evolve:rollback <keyword> [--force] ─────────────────────────
|
|
1736
|
-
if (args[0] === 'evolve:rollback') {
|
|
1737
|
-
const keyword = args[1];
|
|
1738
|
-
if (!keyword || keyword.startsWith('--')) {
|
|
1739
|
-
console.error('Usage: kern evolve:rollback <keyword> [--force]');
|
|
1740
|
-
process.exit(1);
|
|
1741
|
-
}
|
|
1742
|
-
const force = args.includes('--force');
|
|
1743
|
-
const result = rollbackNode(keyword, process.cwd(), force);
|
|
1744
|
-
if (result.success) {
|
|
1745
|
-
console.log(` Rolled back '${keyword}' (moved to .trash/).`);
|
|
1746
|
-
console.log(` Restore with: kern evolve:restore ${keyword}`);
|
|
1747
|
-
}
|
|
1748
|
-
else {
|
|
1749
|
-
console.error(` Failed: ${result.error}`);
|
|
1750
|
-
if (result.usageFiles) {
|
|
1751
|
-
console.error(' Used in:');
|
|
1752
|
-
for (const f of result.usageFiles.slice(0, 5)) {
|
|
1753
|
-
console.error(` ${relative(process.cwd(), f)}`);
|
|
1754
|
-
}
|
|
1755
|
-
}
|
|
1756
|
-
process.exit(1);
|
|
1757
|
-
}
|
|
1758
|
-
process.exit(0);
|
|
1759
|
-
}
|
|
1760
|
-
// ── kern evolve:restore <keyword> ────────────────────────────────────
|
|
1761
|
-
if (args[0] === 'evolve:restore') {
|
|
1762
|
-
const keyword = args[1];
|
|
1763
|
-
if (!keyword) {
|
|
1764
|
-
console.error('Usage: kern evolve:restore <keyword>');
|
|
1765
|
-
process.exit(1);
|
|
1766
|
-
}
|
|
1767
|
-
const result = restoreNode(keyword);
|
|
1768
|
-
if (result.success) {
|
|
1769
|
-
console.log(` Restored '${keyword}'.`);
|
|
1770
|
-
}
|
|
1771
|
-
else {
|
|
1772
|
-
console.error(` Failed: ${result.error}`);
|
|
1773
|
-
process.exit(1);
|
|
1774
|
-
}
|
|
1775
|
-
process.exit(0);
|
|
1776
|
-
}
|
|
1777
|
-
// ── kern evolve:list ─────────────────────────────────────────────────
|
|
1778
|
-
if (args[0] === 'evolve:list') {
|
|
1779
|
-
const manifest = readEvolvedManifest();
|
|
1780
|
-
if (!manifest || Object.keys(manifest.nodes).length === 0) {
|
|
1781
|
-
console.log('\n No evolved nodes graduated. Run \'kern evolve:discover\' to start.\n');
|
|
1782
|
-
process.exit(0);
|
|
1783
|
-
}
|
|
1784
|
-
console.log(`\n KERN evolved nodes — ${Object.keys(manifest.nodes).length} graduated\n`);
|
|
1785
|
-
for (const [keyword, entry] of Object.entries(manifest.nodes)) {
|
|
1786
|
-
console.log(` ${keyword} — ${entry.displayName} (graduated ${entry.graduatedAt.split('T')[0]} by ${entry.graduatedBy})`);
|
|
1787
|
-
}
|
|
1788
|
-
console.log('');
|
|
1789
|
-
process.exit(0);
|
|
1790
|
-
}
|
|
1791
|
-
// ── kern evolve:promote <keyword> ────────────────────────────────────
|
|
1792
|
-
if (args[0] === 'evolve:promote') {
|
|
1793
|
-
const promoteKeyword = args[1];
|
|
1794
|
-
if (!promoteKeyword || promoteKeyword.startsWith('--')) {
|
|
1795
|
-
console.error('Usage: kern evolve:promote <keyword>');
|
|
1796
|
-
console.error(' Reads codegen from .kern/evolved/<keyword>/ and outputs what to add to core.');
|
|
1797
|
-
process.exit(1);
|
|
1798
|
-
}
|
|
1799
|
-
const result = promoteNode(promoteKeyword);
|
|
1800
|
-
if (!result.success) {
|
|
1801
|
-
console.error(` Failed: ${result.error}`);
|
|
1802
|
-
process.exit(1);
|
|
1803
|
-
}
|
|
1804
|
-
const fnName = 'generate' + promoteKeyword.split('-').map(w => w[0].toUpperCase() + w.slice(1)).join('');
|
|
1805
|
-
console.log(`\n KERN evolve:promote — ${promoteKeyword}\n`);
|
|
1806
|
-
console.log(' To promote this node to core, apply these changes:\n');
|
|
1807
|
-
console.log(` 1. Add '${promoteKeyword}' to NODE_TYPES in packages/core/src/spec.ts`);
|
|
1808
|
-
console.log(` 2. Create packages/core/src/generators/${fnName}.ts with:`);
|
|
1809
|
-
console.log(` ──────────────────────────────`);
|
|
1810
|
-
for (const line of (result.codegenTs || '').split('\n').slice(0, 20)) {
|
|
1811
|
-
console.log(` ${line}`);
|
|
1812
|
-
}
|
|
1813
|
-
if ((result.codegenTs || '').split('\n').length > 20) {
|
|
1814
|
-
console.log(` ... (${(result.codegenTs || '').split('\n').length - 20} more lines)`);
|
|
1815
|
-
}
|
|
1816
|
-
console.log(` ──────────────────────────────`);
|
|
1817
|
-
console.log(` 3. Add case '${promoteKeyword}': return ${fnName}(node); to generateCoreNode() in codegen-core.ts`);
|
|
1818
|
-
if (result.goldenKern) {
|
|
1819
|
-
console.log(` 4. Move golden test to packages/core/tests/`);
|
|
1820
|
-
}
|
|
1821
|
-
console.log(` 5. Run: kern evolve:rollback ${promoteKeyword} --force`);
|
|
1822
|
-
console.log('');
|
|
1823
|
-
process.exit(0);
|
|
1824
|
-
}
|
|
1825
|
-
// ── kern evolve:backfill <keyword> --target=<target> [--provider=...] ──
|
|
1826
|
-
if (args[0] === 'evolve:backfill') {
|
|
1827
|
-
const backfillKeyword = args[1];
|
|
1828
|
-
if (!backfillKeyword || backfillKeyword.startsWith('--')) {
|
|
1829
|
-
console.error('Usage: kern evolve:backfill <keyword> --target=<target> [--provider=openai|anthropic|ollama]');
|
|
1830
|
-
process.exit(1);
|
|
1831
|
-
}
|
|
1832
|
-
const backfillTarget = args.find(a => a.startsWith('--target='))?.split('=')[1];
|
|
1833
|
-
if (!backfillTarget) {
|
|
1834
|
-
console.error(' --target=<target> is required');
|
|
1835
|
-
process.exit(1);
|
|
1836
|
-
}
|
|
1837
|
-
const def = readNodeDefinition(backfillKeyword);
|
|
1838
|
-
if (!def) {
|
|
1839
|
-
console.error(` Node '${backfillKeyword}' is not graduated.`);
|
|
1840
|
-
process.exit(1);
|
|
1841
|
-
}
|
|
1842
|
-
// Read current codegen source
|
|
1843
|
-
const codegenTsPath = resolve('.kern', 'evolved', backfillKeyword, 'codegen.ts');
|
|
1844
|
-
if (!existsSync(codegenTsPath)) {
|
|
1845
|
-
console.error(` Missing codegen.ts for '${backfillKeyword}'`);
|
|
1846
|
-
process.exit(1);
|
|
1847
|
-
}
|
|
1848
|
-
const codegenSource = readFileSync(codegenTsPath, 'utf-8');
|
|
1849
|
-
const templateKernPath = resolve('.kern', 'evolved', backfillKeyword, 'template.kern');
|
|
1850
|
-
const kernExample = existsSync(templateKernPath) ? readFileSync(templateKernPath, 'utf-8') : '';
|
|
1851
|
-
const expectedOutputPath = resolve('.kern', 'evolved', backfillKeyword, 'expected-output.ts');
|
|
1852
|
-
const expectedOutput = existsSync(expectedOutputPath) ? readFileSync(expectedOutputPath, 'utf-8') : '';
|
|
1853
|
-
console.log(`\n KERN evolve:backfill — ${backfillKeyword} → ${backfillTarget}\n`);
|
|
1854
|
-
const providerArg = args.find(a => a.startsWith('--provider='))?.split('=')[1];
|
|
1855
|
-
let provider;
|
|
1856
|
-
try {
|
|
1857
|
-
provider = createLLMProvider({ provider: providerArg });
|
|
1858
|
-
}
|
|
1859
|
-
catch (err) {
|
|
1860
|
-
console.error(` ${err.message}`);
|
|
1861
|
-
process.exit(1);
|
|
1862
|
-
}
|
|
1863
|
-
console.log(` Provider: ${provider.name}`);
|
|
1864
|
-
const prompt = buildBackfillPrompt(backfillKeyword, {
|
|
1865
|
-
props: def.props,
|
|
1866
|
-
childTypes: def.childTypes,
|
|
1867
|
-
kernExample,
|
|
1868
|
-
codegenSource,
|
|
1869
|
-
expectedOutput,
|
|
1870
|
-
}, backfillTarget);
|
|
1871
|
-
try {
|
|
1872
|
-
const response = await provider.complete(prompt);
|
|
1873
|
-
// Parse JSON response
|
|
1874
|
-
let json = response.trim();
|
|
1875
|
-
const fenceMatch = json.match(/```(?:json)?\s*\n?([\s\S]*?)```/);
|
|
1876
|
-
if (fenceMatch)
|
|
1877
|
-
json = fenceMatch[1].trim();
|
|
1878
|
-
const objStart = json.indexOf('{');
|
|
1879
|
-
const objEnd = json.lastIndexOf('}');
|
|
1880
|
-
if (objStart !== -1 && objEnd > objStart)
|
|
1881
|
-
json = json.slice(objStart, objEnd + 1);
|
|
1882
|
-
const parsed = JSON.parse(json);
|
|
1883
|
-
if (typeof parsed !== 'object' || parsed === null) {
|
|
1884
|
-
console.error(' LLM response is not a JSON object');
|
|
1885
|
-
process.exit(1);
|
|
1886
|
-
}
|
|
1887
|
-
const targetCodegen = typeof parsed.codegenSource === 'string' ? parsed.codegenSource : undefined;
|
|
1888
|
-
if (!targetCodegen) {
|
|
1889
|
-
console.error(' LLM did not return codegenSource');
|
|
1890
|
-
process.exit(1);
|
|
1891
|
-
}
|
|
1892
|
-
// Write target override
|
|
1893
|
-
const targetsDir = resolve('.kern', 'evolved', backfillKeyword, 'targets');
|
|
1894
|
-
mkdirSync(targetsDir, { recursive: true });
|
|
1895
|
-
writeFileSync(resolve(targetsDir, `${backfillTarget}.js`), targetCodegen);
|
|
1896
|
-
console.log(` Written: .kern/evolved/${backfillKeyword}/targets/${backfillTarget}.js`);
|
|
1897
|
-
if (parsed.expectedOutput) {
|
|
1898
|
-
console.log(` Expected output preview:`);
|
|
1899
|
-
for (const line of parsed.expectedOutput.split('\n').slice(0, 10)) {
|
|
1900
|
-
console.log(` ${line}`);
|
|
1901
|
-
}
|
|
1902
|
-
}
|
|
1903
|
-
console.log(`\n Review the generated codegen before using in production.`);
|
|
1904
|
-
}
|
|
1905
|
-
catch (err) {
|
|
1906
|
-
console.error(` Error: ${err.message}`);
|
|
1907
|
-
process.exit(1);
|
|
1908
|
-
}
|
|
1909
|
-
console.log('');
|
|
1910
|
-
process.exit(0);
|
|
1911
|
-
}
|
|
1912
|
-
// ── kern evolve:prune [--dry-run] [--days=N] ─────────────────────────
|
|
1913
|
-
if (args[0] === 'evolve:prune') {
|
|
1914
|
-
const dryRun = args.includes('--dry-run');
|
|
1915
|
-
const daysArg = args.find(a => a.startsWith('--days='))?.split('=')[1];
|
|
1916
|
-
const thresholdDays = daysArg ? Number(daysArg) : 90;
|
|
1917
|
-
console.log(`\n KERN evolve:prune — removing unused nodes (>${thresholdDays}d)\n`);
|
|
1918
|
-
const results = pruneNodes(process.cwd(), thresholdDays, dryRun);
|
|
1919
|
-
if (results.length === 0) {
|
|
1920
|
-
console.log(' No nodes eligible for pruning.');
|
|
1921
|
-
process.exit(0);
|
|
1922
|
-
}
|
|
1923
|
-
for (const r of results) {
|
|
1924
|
-
if (dryRun) {
|
|
1925
|
-
console.log(` Would prune: ${r.keyword} (${r.daysUnused}d unused)`);
|
|
1926
|
-
}
|
|
1927
|
-
else if (r.pruned) {
|
|
1928
|
-
console.log(` Pruned: ${r.keyword} (${r.daysUnused}d unused) → .trash/`);
|
|
1929
|
-
}
|
|
1930
|
-
else {
|
|
1931
|
-
console.log(` Failed: ${r.keyword} — ${r.error}`);
|
|
1932
|
-
}
|
|
1933
|
-
}
|
|
1934
|
-
if (dryRun) {
|
|
1935
|
-
console.log(`\n Dry run — no changes made. Remove --dry-run to prune.`);
|
|
1936
|
-
}
|
|
1937
|
-
console.log('');
|
|
1938
|
-
process.exit(0);
|
|
1939
|
-
}
|
|
1940
|
-
// ── kern evolve:migrate ──────────────────────────────────────────────
|
|
1941
|
-
if (args[0] === 'evolve:migrate') {
|
|
1942
|
-
console.log(`\n KERN evolve:migrate — checking for keyword collisions\n`);
|
|
1943
|
-
const collisions = detectCollisions(NODE_TYPES);
|
|
1944
|
-
if (collisions.length === 0) {
|
|
1945
|
-
console.log(' No collisions. All evolved nodes are compatible with core.');
|
|
1946
|
-
process.exit(0);
|
|
1947
|
-
}
|
|
1948
|
-
console.log(` Found ${collisions.length} collision(s):\n`);
|
|
1949
|
-
for (const c of collisions) {
|
|
1950
|
-
console.log(` ${c.keyword} (graduated ${c.graduatedAt.split('T')[0]})`);
|
|
1951
|
-
console.log(` This keyword now exists in core NODE_TYPES.`);
|
|
1952
|
-
console.log(` Options:`);
|
|
1953
|
-
console.log(` kern evolve:migrate --rename=${c.keyword} --to=<new-name>`);
|
|
1954
|
-
console.log(` kern evolve:migrate --remove=${c.keyword} (core version supersedes)`);
|
|
1955
|
-
console.log('');
|
|
1956
|
-
}
|
|
1957
|
-
// Handle --rename and --remove flags
|
|
1958
|
-
const renameArg = args.find(a => a.startsWith('--rename='))?.split('=')[1];
|
|
1959
|
-
const toArg = args.find(a => a.startsWith('--to='))?.split('=')[1];
|
|
1960
|
-
const removeArg = args.find(a => a.startsWith('--remove='))?.split('=')[1];
|
|
1961
|
-
if (renameArg && toArg) {
|
|
1962
|
-
const result = renameEvolvedNode(renameArg, toArg);
|
|
1963
|
-
if (result.success) {
|
|
1964
|
-
console.log(` Renamed '${renameArg}' → '${toArg}'`);
|
|
1965
|
-
console.log(` Update your .kern files: replace '${renameArg}' with '${toArg}'.`);
|
|
1966
|
-
}
|
|
1967
|
-
else {
|
|
1968
|
-
console.error(` Rename failed: ${result.error}`);
|
|
1969
|
-
process.exit(1);
|
|
1970
|
-
}
|
|
1971
|
-
}
|
|
1972
|
-
if (removeArg) {
|
|
1973
|
-
const result = rollbackNode(removeArg, process.cwd(), true);
|
|
1974
|
-
if (result.success) {
|
|
1975
|
-
console.log(` Removed evolved '${removeArg}' — core version will be used.`);
|
|
1976
|
-
}
|
|
1977
|
-
else {
|
|
1978
|
-
console.error(` Remove failed: ${result.error}`);
|
|
1979
|
-
process.exit(1);
|
|
1980
|
-
}
|
|
1981
|
-
}
|
|
1982
|
-
console.log('');
|
|
1983
|
-
process.exit(0);
|
|
1984
|
-
}
|
|
1985
|
-
// ── kern evolve:rebuild ─────────────────────────────────────────────
|
|
1986
|
-
if (args[0] === 'evolve:rebuild') {
|
|
1987
|
-
console.log(`\n KERN evolve:rebuild — rebuilding manifest from disk\n`);
|
|
1988
|
-
const result = rebuildEvolvedManifest();
|
|
1989
|
-
if (result.errors.length > 0) {
|
|
1990
|
-
for (const err of result.errors) {
|
|
1991
|
-
console.log(` ⚠ ${err}`);
|
|
1992
|
-
}
|
|
1993
|
-
console.log('');
|
|
1994
|
-
}
|
|
1995
|
-
if (result.rebuilt === 0 && result.errors.length > 0) {
|
|
1996
|
-
console.error(' No nodes rebuilt.');
|
|
1997
|
-
process.exit(1);
|
|
1998
|
-
}
|
|
1999
|
-
console.log(` manifest.json rebuilt with ${result.rebuilt} node(s).`);
|
|
2000
|
-
console.log('');
|
|
2001
|
-
process.exit(0);
|
|
2002
|
-
}
|
|
2003
|
-
// ── kern confidence <file.kern|dir> ──────────────────────────────────
|
|
2004
|
-
if (args[0] === 'confidence') {
|
|
2005
|
-
const confInput = args[1];
|
|
2006
|
-
if (!confInput) {
|
|
2007
|
-
console.error('Usage: kern confidence <file.kern|dir>');
|
|
2008
|
-
console.error(' Builds and displays the confidence graph for .kern file(s).');
|
|
2009
|
-
process.exit(1);
|
|
2010
|
-
}
|
|
2011
|
-
const confPath = resolve(confInput);
|
|
2012
|
-
if (!existsSync(confPath)) {
|
|
2013
|
-
console.error(`Not found: ${confInput}`);
|
|
2014
|
-
process.exit(1);
|
|
2015
|
-
}
|
|
2016
|
-
const { buildConfidenceGraph, buildMultiFileConfidenceGraph, flattenIR, lintMultiFileConfidenceGraph } = await import('@kernlang/review');
|
|
2017
|
-
const confStat = statSync(confPath);
|
|
2018
|
-
const isDir = confStat.isDirectory();
|
|
2019
|
-
// Collect .kern files
|
|
2020
|
-
const kernFiles = isDir ? findKernFiles(confPath) : [confPath];
|
|
2021
|
-
if (kernFiles.length === 0) {
|
|
2022
|
-
console.log(' No .kern files found.');
|
|
2023
|
-
process.exit(0);
|
|
2024
|
-
}
|
|
2025
|
-
// Parse all files, skip those without confidence (fast early exit)
|
|
2026
|
-
const fileMap = new Map();
|
|
2027
|
-
for (const file of kernFiles.sort()) {
|
|
2028
|
-
const source = readFileSync(file, 'utf-8');
|
|
2029
|
-
if (!source.includes('confidence='))
|
|
2030
|
-
continue;
|
|
2031
|
-
const ast = parseAndSurface(source, file);
|
|
2032
|
-
fileMap.set(file, flattenIR(ast));
|
|
2033
|
-
}
|
|
2034
|
-
if (fileMap.size === 0) {
|
|
2035
|
-
console.log(`\n No confidence declarations found in ${isDir ? confInput : basename(confInput)}`);
|
|
2036
|
-
process.exit(0);
|
|
2037
|
-
}
|
|
2038
|
-
// Build graph (single-file or multi-file)
|
|
2039
|
-
const graph = fileMap.size === 1
|
|
2040
|
-
? buildConfidenceGraph([...fileMap.values()][0])
|
|
2041
|
-
: buildMultiFileConfidenceGraph(fileMap);
|
|
2042
|
-
const isMulti = fileMap.size > 1;
|
|
2043
|
-
console.log(`\n Confidence Graph (${graph.nodes.size} nodes, ${graph.topoOrder.length} resolved${isMulti ? `, ${fileMap.size} files` : ''}):\n`);
|
|
2044
|
-
if (isMulti) {
|
|
2045
|
-
// Group by source file
|
|
2046
|
-
const byFile = new Map();
|
|
2047
|
-
for (const cnode of graph.nodes.values()) {
|
|
2048
|
-
const file = cnode.sourceFile || 'unknown';
|
|
2049
|
-
if (!byFile.has(file))
|
|
2050
|
-
byFile.set(file, []);
|
|
2051
|
-
byFile.get(file).push(cnode);
|
|
2052
|
-
}
|
|
2053
|
-
for (const [file, nodes] of byFile) {
|
|
2054
|
-
const rel = relative(process.cwd(), file) || file;
|
|
2055
|
-
console.log(` ${rel} (${nodes.length} nodes):`);
|
|
2056
|
-
for (const cnode of nodes) {
|
|
2057
|
-
const resolvedStr = cnode.resolved !== null ? cnode.resolved.toFixed(2) : 'null';
|
|
2058
|
-
const specStr = cnode.spec.kind === 'literal'
|
|
2059
|
-
? 'declared'
|
|
2060
|
-
: `from: ${cnode.spec.sources?.join(', ')}, ${cnode.spec.strategy}`;
|
|
2061
|
-
const crossFile = cnode.spec.sources?.some(s => {
|
|
2062
|
-
const src = graph.nodes.get(s);
|
|
2063
|
-
return src && src.sourceFile !== cnode.sourceFile;
|
|
2064
|
-
});
|
|
2065
|
-
const crossTag = crossFile ? ' [cross-file]' : '';
|
|
2066
|
-
const cycleTag = cnode.inCycle ? ' [CYCLE]' : '';
|
|
2067
|
-
console.log(` ${cnode.name.padEnd(20)} ${resolvedStr.padEnd(8)} (${specStr})${crossTag}${cycleTag}`);
|
|
2068
|
-
}
|
|
2069
|
-
console.log('');
|
|
2070
|
-
}
|
|
2071
|
-
}
|
|
2072
|
-
else {
|
|
2073
|
-
for (const [name, cnode] of graph.nodes) {
|
|
2074
|
-
const resolvedStr = cnode.resolved !== null ? cnode.resolved.toFixed(2) : 'null';
|
|
2075
|
-
const specStr = cnode.spec.kind === 'literal'
|
|
2076
|
-
? 'declared'
|
|
2077
|
-
: `from: ${cnode.spec.sources?.join(', ')}, ${cnode.spec.strategy}`;
|
|
2078
|
-
const cycleTag = cnode.inCycle ? ' [CYCLE]' : '';
|
|
2079
|
-
console.log(` ${name.padEnd(20)} ${resolvedStr.padEnd(8)} (${specStr})${cycleTag}`);
|
|
2080
|
-
}
|
|
2081
|
-
}
|
|
2082
|
-
// Unresolved needs
|
|
2083
|
-
const unresolvedNeeds = [];
|
|
2084
|
-
for (const [name, cnode] of graph.nodes) {
|
|
2085
|
-
for (const need of cnode.needs) {
|
|
2086
|
-
if (!need.resolved) {
|
|
2087
|
-
unresolvedNeeds.push({ name, what: need.what, wouldRaiseTo: need.wouldRaiseTo });
|
|
2088
|
-
}
|
|
2089
|
-
}
|
|
2090
|
-
}
|
|
2091
|
-
if (unresolvedNeeds.length > 0) {
|
|
2092
|
-
console.log(` Unresolved needs (${unresolvedNeeds.length}):`);
|
|
2093
|
-
for (const n of unresolvedNeeds) {
|
|
2094
|
-
const raise = n.wouldRaiseTo !== undefined ? ` → would raise to ${n.wouldRaiseTo}` : '';
|
|
2095
|
-
console.log(` ${n.name}: "${n.what}"${raise}`);
|
|
2096
|
-
}
|
|
2097
|
-
}
|
|
2098
|
-
if (graph.cycles.length > 0) {
|
|
2099
|
-
console.log(`\n Cycles (${graph.cycles.length}):`);
|
|
2100
|
-
for (const cycle of graph.cycles) {
|
|
2101
|
-
console.log(` ${cycle.join(' → ')}`);
|
|
2102
|
-
}
|
|
2103
|
-
}
|
|
2104
|
-
// Duplicates (multi-file only)
|
|
2105
|
-
const dupes = 'duplicates' in graph ? graph.duplicates : [];
|
|
2106
|
-
if (dupes.length > 0) {
|
|
2107
|
-
console.log(`\n Duplicate names (${dupes.length}):`);
|
|
2108
|
-
for (const dup of dupes) {
|
|
2109
|
-
console.log(` ${dup.name}: ${dup.files.map((f) => relative(process.cwd(), f) || f).join(', ')}`);
|
|
2110
|
-
}
|
|
2111
|
-
}
|
|
2112
|
-
console.log('');
|
|
2113
|
-
process.exit(0);
|
|
2114
|
-
}
|
|
2115
|
-
// ── Standard transpile mode ────────────────────────────────────────────
|
|
2116
|
-
const inputFile = args.find(a => !a.startsWith('--'));
|
|
2117
|
-
if (!inputFile) {
|
|
2118
|
-
console.log('Usage: kern <file.kern> [--target=nextjs|tailwind|web|native|express|cli] [options]');
|
|
2119
|
-
console.log('');
|
|
2120
|
-
console.log('Commands:');
|
|
2121
|
-
console.log(' dev <dir|file> [--target=...] [--outdir=...] Watch & hot-transpile .kern files');
|
|
2122
|
-
console.log(' compile <dir|file> --outdir=<dir> Compile .kern → .ts (core nodes)');
|
|
2123
|
-
console.log(' scan [--force] [--dry-run] Detect project → generate kern.config.ts');
|
|
2124
|
-
console.log(' init-templates [--force] [--dry-run] Scan deps → scaffold template .kern files');
|
|
2125
|
-
console.log(' review <file.ts|dir> [options] Static analysis, Cognitive Complexity & CI Gate');
|
|
2126
|
-
console.log(' evolve <dir|file> [options] Detect gaps → propose templates');
|
|
2127
|
-
console.log(' evolve:review [options] Review staged template proposals');
|
|
2128
|
-
console.log(' evolve:review-v4 [options] Review & graduate v4 node proposals');
|
|
2129
|
-
console.log(' evolve:promote <keyword> Show steps to move evolved → core');
|
|
2130
|
-
console.log(' evolve:backfill <kw> --target=<t> LLM generates target-specific codegen');
|
|
2131
|
-
console.log(' evolve:prune [--dry-run] [--days=N] Remove unused nodes (default 90d)');
|
|
2132
|
-
console.log(' evolve:migrate Detect & resolve keyword collisions');
|
|
2133
|
-
console.log(' evolve:rebuild Rebuild manifest.json from disk definitions');
|
|
2134
|
-
console.log(' confidence <file.kern> Display confidence graph for a .kern file');
|
|
2135
|
-
console.log('');
|
|
2136
|
-
console.log('Targets:');
|
|
2137
|
-
console.log(' nextjs Next.js App Router (default)');
|
|
2138
|
-
console.log(' tailwind React + Tailwind CSS');
|
|
2139
|
-
console.log(' web React with inline styles');
|
|
2140
|
-
console.log(' vue Vue 3 Single File Component');
|
|
2141
|
-
console.log(' nuxt Nuxt 3 (pages, layouts, server routes)');
|
|
2142
|
-
console.log(' native React Native component');
|
|
2143
|
-
console.log(' express Express TypeScript backend');
|
|
2144
|
-
console.log(' cli Commander.js CLI app');
|
|
2145
|
-
console.log(' terminal ANSI terminal rendering');
|
|
2146
|
-
console.log('');
|
|
2147
|
-
console.log('Options:');
|
|
2148
|
-
console.log(' --structure=flat|bulletproof|atomic|kern Output structure pattern (React targets)');
|
|
2149
|
-
console.log(' --decompile Output human-readable pseudocode');
|
|
2150
|
-
console.log(' --minify Output minified single-line Kern (LLM wire format)');
|
|
2151
|
-
console.log(' --pretty Expand minified Kern back to indented format');
|
|
2152
|
-
console.log(' --metrics Show language metrics (escape ratio, coverage, etc.)');
|
|
2153
|
-
console.log('');
|
|
2154
|
-
console.log('Structures (React targets only):');
|
|
2155
|
-
console.log(' flat Single .tsx file (default)');
|
|
2156
|
-
console.log(' bulletproof Feature-based folder structure');
|
|
2157
|
-
console.log(' atomic Atomic Design hierarchy (pages/templates/organisms/molecules/atoms)');
|
|
2158
|
-
console.log(' kern KERN-native (surfaces/blocks/signals/tokens/models)');
|
|
2159
|
-
process.exit(1);
|
|
2160
|
-
}
|
|
2161
|
-
// ── Load config via jiti (supports .ts config at runtime) ────────────────
|
|
2162
|
-
let config;
|
|
2163
|
-
const configPath = resolve(process.cwd(), 'kern.config.ts');
|
|
2164
|
-
if (existsSync(configPath)) {
|
|
2165
|
-
try {
|
|
2166
|
-
const jiti = createJiti(import.meta.url);
|
|
2167
|
-
const mod = jiti(configPath);
|
|
2168
|
-
const userConfig = mod.default ?? mod;
|
|
2169
|
-
config = resolveConfig(userConfig);
|
|
2170
|
-
}
|
|
2171
|
-
catch (err) {
|
|
2172
|
-
console.error(`Warning: Failed to load kern.config.ts: ${err.message}`);
|
|
2173
|
-
config = resolveConfig({});
|
|
2174
|
-
}
|
|
2175
|
-
}
|
|
2176
|
-
else {
|
|
2177
|
-
config = resolveConfig({});
|
|
2178
|
-
}
|
|
2179
|
-
// Load templates before transpile
|
|
2180
|
-
loadTemplates(config);
|
|
2181
|
-
// Load evolved nodes (v4) — graduated nodes from .kern/evolved/
|
|
2182
|
-
loadEvolvedNodes(process.cwd(), args.includes('--verify'));
|
|
2183
|
-
// CLI flags override config — target
|
|
2184
|
-
const cliTarget = args.find(a => a.startsWith('--target='))?.split('=')[1];
|
|
2185
|
-
if (cliTarget) {
|
|
2186
|
-
if (!VALID_TARGETS.includes(cliTarget)) {
|
|
2187
|
-
console.error(`Unknown target: '${cliTarget}'. Valid targets: ${VALID_TARGETS.join(', ')}`);
|
|
2188
|
-
process.exit(1);
|
|
2189
|
-
}
|
|
2190
|
-
config = { ...config, target: cliTarget };
|
|
2191
|
-
}
|
|
2192
|
-
const target = config.target;
|
|
2193
|
-
// CLI flags override config — structure
|
|
2194
|
-
const cliStructure = args.find(a => a.startsWith('--structure='))?.split('=')[1];
|
|
2195
|
-
if (cliStructure) {
|
|
2196
|
-
if (!VALID_STRUCTURES.includes(cliStructure)) {
|
|
2197
|
-
console.error(`Unknown structure: '${cliStructure}'. Valid structures: ${VALID_STRUCTURES.join(', ')}`);
|
|
2198
|
-
process.exit(1);
|
|
2199
|
-
}
|
|
2200
|
-
config = { ...config, structure: cliStructure };
|
|
2201
|
-
}
|
|
2202
|
-
const irSource = readFileSync(resolve(inputFile), 'utf-8');
|
|
2203
|
-
const ast = parseAndSurface(irSource, inputFile);
|
|
2204
|
-
const ext = inputFile.endsWith('.kern') ? '.kern' : '.ir';
|
|
2205
|
-
const name = basename(inputFile, ext);
|
|
2206
|
-
// ── Minify: indented Kern → single-line wire format ─────────────────────
|
|
2207
|
-
if (args.includes('--minify')) {
|
|
2208
|
-
const minified = minifyKern(ast);
|
|
2209
|
-
const outFile = resolve(dirname(inputFile), `${name}.min.kern`);
|
|
2210
|
-
writeFileSync(outFile, minified);
|
|
2211
|
-
const savings = Math.round((1 - minified.length / irSource.length) * 100);
|
|
2212
|
-
console.log(`Minified: ${inputFile} → ${outFile}`);
|
|
2213
|
-
console.log(`Chars: ${irSource.length} → ${minified.length} (${savings}% smaller)`);
|
|
2214
|
-
process.exit(0);
|
|
2215
|
-
}
|
|
2216
|
-
// ── Pretty: re-indent (useful after minify or messy edits) ──────────────
|
|
2217
|
-
if (args.includes('--pretty')) {
|
|
2218
|
-
const pretty = prettyKern(ast);
|
|
2219
|
-
const outFile = resolve(dirname(inputFile), `${name}.kern`);
|
|
2220
|
-
writeFileSync(outFile, pretty);
|
|
2221
|
-
console.log(`Formatted: ${inputFile} → ${outFile}`);
|
|
2222
|
-
process.exit(0);
|
|
2223
|
-
}
|
|
2224
|
-
// ── Decompile: Kern → human-readable pseudocode ─────────────────────────
|
|
2225
|
-
if (args.includes('--decompile')) {
|
|
2226
|
-
const result = decompile(ast);
|
|
2227
|
-
console.log(result.code);
|
|
2228
|
-
process.exit(0);
|
|
2229
|
-
}
|
|
2230
|
-
// ── Metrics: analyze language coverage ────────────────────────────────────
|
|
2231
|
-
if (args.includes('--metrics')) {
|
|
2232
|
-
const metrics = collectLanguageMetrics(ast);
|
|
2233
|
-
console.log(`Metrics: ${inputFile}`);
|
|
2234
|
-
console.log(` Nodes: ${metrics.nodeCount} (${metrics.nodeTypes.length} types)`);
|
|
2235
|
-
console.log(` Styles: ${metrics.styleMetrics.totalStyleDecls} declarations`);
|
|
2236
|
-
console.log(` Mapped: ${metrics.styleMetrics.mappedStyleDecls} (${Math.round((1 - metrics.styleMetrics.escapeRatio) * 100)}%)`);
|
|
2237
|
-
console.log(` Escaped: ${metrics.styleMetrics.escapedStyleDecls} (${Math.round(metrics.styleMetrics.escapeRatio * 100)}%)`);
|
|
2238
|
-
if (metrics.styleMetrics.escapedKeys.length > 0) {
|
|
2239
|
-
console.log(` Escape keys: ${metrics.styleMetrics.escapedKeys.join(', ')}`);
|
|
2240
|
-
}
|
|
2241
|
-
console.log(` Shorthand: ${Math.round(metrics.shorthandCoverage * 100)}% coverage`);
|
|
2242
|
-
console.log(` Theme refs: ${metrics.themeRefCount}`);
|
|
2243
|
-
console.log(` Pseudo: ${metrics.pseudoStyleCount}`);
|
|
2244
|
-
if (metrics.unknownNodeCount > 0) {
|
|
2245
|
-
console.log(` Unknown nodes: ${metrics.unknownNodeCount}`);
|
|
2246
|
-
}
|
|
2247
|
-
console.log('');
|
|
2248
|
-
console.log(' Node types:');
|
|
2249
|
-
for (const nt of metrics.nodeTypes.slice(0, 10)) {
|
|
2250
|
-
console.log(` ${nt.type}: ${nt.count} (${nt.styleDecls} styles)`);
|
|
2251
|
-
}
|
|
2252
|
-
process.exit(0);
|
|
2253
|
-
}
|
|
2254
|
-
// ── Transpile: Kern → target code ───────────────────────────────────────
|
|
2255
|
-
const result = target === 'native'
|
|
2256
|
-
? transpile(ast, config)
|
|
2257
|
-
: target === 'web'
|
|
2258
|
-
? transpileWeb(ast, config)
|
|
2259
|
-
: target === 'tailwind'
|
|
2260
|
-
? transpileTailwind(ast, config)
|
|
2261
|
-
: target === 'mcp'
|
|
2262
|
-
? transpileMCP(ast, config)
|
|
2263
|
-
: target === 'express'
|
|
2264
|
-
? transpileExpress(ast, config)
|
|
2265
|
-
: target === 'fastapi'
|
|
2266
|
-
? transpileFastAPI(ast, config)
|
|
2267
|
-
: target === 'cli'
|
|
2268
|
-
? transpileCliApp(ast, config)
|
|
2269
|
-
: target === 'terminal'
|
|
2270
|
-
? transpileTerminal(ast, config)
|
|
2271
|
-
: target === 'ink'
|
|
2272
|
-
? transpileInk(ast, config)
|
|
2273
|
-
: target === 'vue'
|
|
2274
|
-
? transpileVue(ast, config)
|
|
2275
|
-
: target === 'nuxt'
|
|
2276
|
-
? transpileNuxt(ast, config)
|
|
2277
|
-
: transpileNextjs(ast, config);
|
|
2278
|
-
const outDir = resolve(dirname(inputFile), config.output.outDir);
|
|
2279
|
-
const isStructured = config.structure !== 'flat' && result.artifacts && result.artifacts.length > 0;
|
|
2280
|
-
if (isStructured) {
|
|
2281
|
-
// Structured output: write all artifacts, entry code comes from artifacts
|
|
2282
|
-
for (const artifact of result.artifacts) {
|
|
2283
|
-
const artifactPath = resolve(outDir, artifact.path);
|
|
2284
|
-
mkdirSync(dirname(artifactPath), { recursive: true });
|
|
2285
|
-
writeFileSync(artifactPath, artifact.content);
|
|
2286
|
-
}
|
|
2287
|
-
// Find entry file path for display
|
|
2288
|
-
const entryArtifact = result.artifacts.find(a => a.type === 'entry' || a.type === 'page');
|
|
2289
|
-
const displayPath = entryArtifact ? resolve(outDir, entryArtifact.path) : resolve(outDir, `${name}.tsx`);
|
|
2290
|
-
console.log(`Transpiled: ${inputFile} → ${displayPath}`);
|
|
2291
|
-
}
|
|
2292
|
-
else {
|
|
2293
|
-
// Flat output: single file
|
|
2294
|
-
const outExt = target === 'fastapi' ? '.py'
|
|
2295
|
-
: (target === 'vue' || target === 'nuxt') ? '.vue'
|
|
2296
|
-
: (target === 'express' || target === 'cli' || target === 'terminal') ? '.ts'
|
|
2297
|
-
: '.tsx';
|
|
2298
|
-
const outFile = resolve(outDir, `${name}${outExt}`);
|
|
2299
|
-
mkdirSync(dirname(outFile), { recursive: true });
|
|
2300
|
-
writeFileSync(outFile, result.code);
|
|
2301
|
-
if (result.artifacts) {
|
|
2302
|
-
for (const artifact of result.artifacts) {
|
|
2303
|
-
const artifactPath = resolve(outDir, artifact.path);
|
|
2304
|
-
mkdirSync(dirname(artifactPath), { recursive: true });
|
|
2305
|
-
writeFileSync(artifactPath, artifact.content);
|
|
2306
|
-
}
|
|
2307
|
-
}
|
|
2308
|
-
console.log(`Transpiled: ${inputFile} → ${outFile}`);
|
|
2309
|
-
}
|
|
2310
|
-
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' };
|
|
2311
|
-
console.log(`Target: ${targetNames[target] || target}`);
|
|
2312
|
-
if (config.structure !== 'flat') {
|
|
2313
|
-
const structureNames = { bulletproof: 'Bulletproof React', atomic: 'Atomic Design', kern: 'KERN Native' };
|
|
2314
|
-
console.log(`Structure: ${structureNames[config.structure] || config.structure}`);
|
|
2315
|
-
}
|
|
2316
|
-
console.log(`IR tokens: ${result.irTokenCount}`);
|
|
2317
|
-
console.log(`TS tokens: ${result.tsTokenCount}`);
|
|
2318
|
-
console.log(`Reduction: ${result.tokenReduction}%`);
|
|
2319
|
-
console.log(`Source map: ${result.sourceMap.length} entries`);
|
|
2320
|
-
if (result.artifacts) {
|
|
2321
|
-
console.log(`Artifacts: ${result.artifacts.length}`);
|
|
2322
|
-
}
|
|
2323
|
-
if (result.diagnostics && result.diagnostics.length > 0) {
|
|
2324
|
-
const counts = {};
|
|
2325
|
-
for (const d of result.diagnostics)
|
|
2326
|
-
counts[d.outcome] = (counts[d.outcome] || 0) + 1;
|
|
2327
|
-
const parts = Object.entries(counts).map(([k, v]) => `${v} ${k}`);
|
|
2328
|
-
console.log(`Diagnostics: ${parts.join(', ')}`);
|
|
2329
|
-
const unsupported = result.diagnostics.filter(d => d.outcome === 'unsupported');
|
|
2330
|
-
if (unsupported.length > 0) {
|
|
2331
|
-
for (const d of unsupported) {
|
|
2332
|
-
const loc = d.loc ? `:${d.loc.line}` : '';
|
|
2333
|
-
const lost = d.childrenLost ? ` (+${d.childrenLost} children)` : '';
|
|
2334
|
-
console.log(` ⚠ ${d.nodeType}${loc} — unsupported in ${d.target}${lost}`);
|
|
2335
|
-
}
|
|
2336
|
-
}
|
|
2337
|
-
}
|
|
2338
|
-
// ── Minify/Pretty implementations ───────────────────────────────────────
|
|
2339
|
-
function minifyKern(node) {
|
|
2340
|
-
const type = node.type;
|
|
2341
|
-
const props = node.props || {};
|
|
2342
|
-
let head = type;
|
|
2343
|
-
// Serialize props (theme name is bare word, not key=value)
|
|
2344
|
-
for (const [k, v] of Object.entries(props)) {
|
|
2345
|
-
if (['styles', 'pseudoStyles', 'themeRefs'].includes(k))
|
|
2346
|
-
continue;
|
|
2347
|
-
if (type === 'theme' && k === 'name') {
|
|
2348
|
-
head += ` ${v}`;
|
|
2349
|
-
continue;
|
|
2350
|
-
}
|
|
2351
|
-
if (typeof v === 'object' && v !== null && '__expr' in v) {
|
|
2352
|
-
head += ` ${k}={{ ${v.code} }}`;
|
|
2353
|
-
continue;
|
|
2354
|
-
}
|
|
2355
|
-
const val = typeof v === 'string' && v.includes(' ') ? `"${v}"` : String(v);
|
|
2356
|
-
head += ` ${k}=${val}`;
|
|
2357
|
-
}
|
|
2358
|
-
// Serialize styles
|
|
2359
|
-
if (props.styles) {
|
|
2360
|
-
const pairs = Object.entries(props.styles)
|
|
2361
|
-
.map(([k, v]) => v.includes(' ') || v.includes(',') ? `"${k}":"${v}"` : `${k}:${v}`);
|
|
2362
|
-
head += ` {${pairs.join(',')}}`;
|
|
2363
|
-
}
|
|
2364
|
-
// Serialize pseudo styles
|
|
2365
|
-
if (props.pseudoStyles) {
|
|
2366
|
-
const pseudo = props.pseudoStyles;
|
|
2367
|
-
for (const [state, styles] of Object.entries(pseudo)) {
|
|
2368
|
-
for (const [k, v] of Object.entries(styles)) {
|
|
2369
|
-
head += ` {:${state}:${k}:${v}}`;
|
|
2370
|
-
}
|
|
2371
|
-
}
|
|
2372
|
-
}
|
|
2373
|
-
// Theme refs
|
|
2374
|
-
if (props.themeRefs) {
|
|
2375
|
-
for (const ref of props.themeRefs) {
|
|
2376
|
-
head += ` $${ref}`;
|
|
2377
|
-
}
|
|
2378
|
-
}
|
|
2379
|
-
// Children → S-expression style
|
|
2380
|
-
if (node.children && node.children.length > 0) {
|
|
2381
|
-
const kids = node.children.map(c => minifyKern(c)).join(',');
|
|
2382
|
-
return `${head}(${kids})`;
|
|
2383
|
-
}
|
|
2384
|
-
return head;
|
|
2385
|
-
}
|
|
2386
|
-
function collectTsFilesFlat(dirPath, recursive) {
|
|
2387
|
-
const files = [];
|
|
2388
|
-
for (const entry of readdirSync(dirPath)) {
|
|
2389
|
-
const full = resolve(dirPath, entry);
|
|
2390
|
-
const s = statSync(full);
|
|
2391
|
-
if (s.isDirectory() && recursive && !entry.startsWith('.') && entry !== 'node_modules' && entry !== 'dist') {
|
|
2392
|
-
files.push(...collectTsFilesFlat(full, true));
|
|
2393
|
-
}
|
|
2394
|
-
else if ((entry.endsWith('.ts') || entry.endsWith('.tsx')) && !entry.endsWith('.d.ts') && !entry.endsWith('.test.ts')) {
|
|
2395
|
-
files.push(full);
|
|
2396
|
-
}
|
|
2397
|
-
else if (entry.endsWith('.kern')) {
|
|
2398
|
-
files.push(full);
|
|
2399
|
-
}
|
|
2400
|
-
else if (entry.endsWith('.py') && !entry.startsWith('test_') && !entry.endsWith('_test.py')) {
|
|
2401
|
-
files.push(full);
|
|
2402
|
-
}
|
|
2403
|
-
}
|
|
2404
|
-
return files;
|
|
2405
|
-
}
|
|
2406
|
-
function prettyKern(node, indent = '') {
|
|
2407
|
-
const type = node.type;
|
|
2408
|
-
const props = node.props || {};
|
|
2409
|
-
let line = `${indent}${type}`;
|
|
2410
|
-
for (const [k, v] of Object.entries(props)) {
|
|
2411
|
-
if (['styles', 'pseudoStyles', 'themeRefs'].includes(k))
|
|
2412
|
-
continue;
|
|
2413
|
-
if (type === 'theme' && k === 'name') {
|
|
2414
|
-
line += ` ${v}`;
|
|
2415
|
-
continue;
|
|
2416
|
-
}
|
|
2417
|
-
if (typeof v === 'object' && v !== null && '__expr' in v) {
|
|
2418
|
-
line += ` ${k}={{ ${v.code} }}`;
|
|
2419
|
-
continue;
|
|
2420
|
-
}
|
|
2421
|
-
const val = typeof v === 'string' && v.includes(' ') ? `"${v}"` : String(v);
|
|
2422
|
-
line += ` ${k}=${val}`;
|
|
2423
|
-
}
|
|
2424
|
-
if (props.styles) {
|
|
2425
|
-
const pairs = Object.entries(props.styles)
|
|
2426
|
-
.map(([k, v]) => v.includes(' ') || v.includes(',') ? `"${k}":"${v}"` : `${k}:${v}`);
|
|
2427
|
-
line += ` {${pairs.join(',')}}`;
|
|
2428
|
-
}
|
|
2429
|
-
if (props.pseudoStyles) {
|
|
2430
|
-
const pseudo = props.pseudoStyles;
|
|
2431
|
-
for (const [state, styles] of Object.entries(pseudo)) {
|
|
2432
|
-
for (const [k, v] of Object.entries(styles)) {
|
|
2433
|
-
line += `,${`:${state}:${k}:${v}`}`;
|
|
2434
|
-
}
|
|
2435
|
-
}
|
|
2436
|
-
}
|
|
2437
|
-
if (props.themeRefs) {
|
|
2438
|
-
for (const ref of props.themeRefs) {
|
|
2439
|
-
line += ` $${ref}`;
|
|
2440
|
-
}
|
|
2441
|
-
}
|
|
2442
|
-
let result = line + '\n';
|
|
2443
|
-
if (node.children) {
|
|
2444
|
-
for (const child of node.children) {
|
|
2445
|
-
result += prettyKern(child, indent + ' ');
|
|
2446
|
-
}
|
|
2447
|
-
}
|
|
2448
|
-
return result;
|
|
46
|
+
// Treat as file input for transpile
|
|
47
|
+
runTranspile(args);
|
|
2449
48
|
}
|
|
49
|
+
await main();
|
|
2450
50
|
//# sourceMappingURL=cli.js.map
|