@kernlang/core 3.1.2 → 3.1.3
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/codegen-core.d.ts +9 -1
- package/dist/codegen-core.js +125 -74
- package/dist/codegen-core.js.map +1 -1
- package/dist/concepts.d.ts +12 -2
- package/dist/concepts.js.map +1 -1
- package/dist/config.js +2 -2
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/parser.d.ts +7 -0
- package/dist/parser.js +28 -5
- package/dist/parser.js.map +1 -1
- package/dist/spec.d.ts +2 -2
- package/dist/spec.js +3 -3
- package/dist/spec.js.map +1 -1
- package/dist/template-engine.js +9 -5
- package/dist/template-engine.js.map +1 -1
- package/dist/utils.js +7 -1
- package/dist/utils.js.map +1 -1
- package/package.json +10 -1
package/dist/codegen-core.d.ts
CHANGED
|
@@ -7,6 +7,14 @@
|
|
|
7
7
|
* Machine nodes are KERN's killer feature: 12 lines of KERN → 140+ lines of TS.
|
|
8
8
|
*/
|
|
9
9
|
import type { IRNode } from './types.js';
|
|
10
|
+
/** Validate and emit a safe identifier for generated code. Throws on invalid. */
|
|
11
|
+
export declare function emitIdentifier(value: string | undefined, fallback: string, node?: IRNode): string;
|
|
12
|
+
/** Escape a string for safe interpolation into a single-quoted JS string literal. */
|
|
13
|
+
export declare function emitStringLiteral(value: string): string;
|
|
14
|
+
/** Validate and emit a safe filesystem path for generated code. */
|
|
15
|
+
export declare function emitPath(value: string, node?: IRNode): string;
|
|
16
|
+
/** Escape a value for interpolation into a template literal in generated code. */
|
|
17
|
+
export declare function emitTemplateSafe(value: string): string;
|
|
10
18
|
/** Register an evolved generator (called at startup). */
|
|
11
19
|
export declare function registerEvolvedGenerator(keyword: string, fn: (node: IRNode) => string[]): void;
|
|
12
20
|
/** Register a target-specific evolved generator (called at startup). */
|
|
@@ -73,7 +81,7 @@ export declare function generateResolve(node: IRNode): string[];
|
|
|
73
81
|
export declare function generateExpect(node: IRNode): string[];
|
|
74
82
|
export declare function generateRecover(node: IRNode): string[];
|
|
75
83
|
export declare function generatePattern(node: IRNode): string[];
|
|
76
|
-
export declare function generateApply(node: IRNode): string[];
|
|
84
|
+
export declare function generateApply(node: IRNode, _depth?: number): string[];
|
|
77
85
|
export declare function generateConditional(node: IRNode): string[];
|
|
78
86
|
export declare function generateSelect(node: IRNode): string[];
|
|
79
87
|
export declare function generateModel(node: IRNode): string[];
|
package/dist/codegen-core.js
CHANGED
|
@@ -8,6 +8,50 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { isTemplateNode, expandTemplateNode } from './template-engine.js';
|
|
10
10
|
import { KernCodegenError } from './errors.js';
|
|
11
|
+
// ── Safe Emitters (prompt-injection immunity) ────────────────────────────
|
|
12
|
+
// Every prop value interpolated into generated code MUST go through these.
|
|
13
|
+
// Raw string splicing is the root cause of codegen injection (audit 2026-03-25).
|
|
14
|
+
// Matches valid JS/TS identifiers — KERN hyphens are converted to camelCase by the parser.
|
|
15
|
+
// Allows $ for React patterns (e.g., $state). Does NOT allow hyphens since
|
|
16
|
+
// generated TypeScript rejects them (e.g., `interface My-User` is invalid TS).
|
|
17
|
+
const SAFE_IDENT_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
18
|
+
const SAFE_PATH_RE = /^[A-Za-z0-9/_.\-~]+$/;
|
|
19
|
+
/** Validate and emit a safe identifier for generated code. Throws on invalid. */
|
|
20
|
+
export function emitIdentifier(value, fallback, node) {
|
|
21
|
+
const v = value || fallback;
|
|
22
|
+
if (!SAFE_IDENT_RE.test(v)) {
|
|
23
|
+
throw new KernCodegenError(`Invalid identifier: '${v.slice(0, 50)}' — must match KERN identifier grammar [A-Za-z_$][A-Za-z0-9_$-]*`, node);
|
|
24
|
+
}
|
|
25
|
+
return v;
|
|
26
|
+
}
|
|
27
|
+
/** Escape a string for safe interpolation into a single-quoted JS string literal. */
|
|
28
|
+
export function emitStringLiteral(value) {
|
|
29
|
+
const escaped = value
|
|
30
|
+
.replace(/\\/g, '\\\\')
|
|
31
|
+
.replace(/'/g, "\\'")
|
|
32
|
+
.replace(/`/g, '\\`')
|
|
33
|
+
.replace(/\$/g, '\\$')
|
|
34
|
+
.replace(/\n/g, '\\n')
|
|
35
|
+
.replace(/\r/g, '\\r');
|
|
36
|
+
return `'${escaped}'`;
|
|
37
|
+
}
|
|
38
|
+
/** Validate and emit a safe filesystem path for generated code. */
|
|
39
|
+
export function emitPath(value, node) {
|
|
40
|
+
if (!SAFE_PATH_RE.test(value)) {
|
|
41
|
+
throw new KernCodegenError(`Invalid path: '${value.slice(0, 80)}' — contains unsafe characters`, node);
|
|
42
|
+
}
|
|
43
|
+
if (value.includes('..')) {
|
|
44
|
+
throw new KernCodegenError(`Invalid path: '${value.slice(0, 80)}' — path traversal (..) not allowed`, node);
|
|
45
|
+
}
|
|
46
|
+
return emitStringLiteral(value);
|
|
47
|
+
}
|
|
48
|
+
/** Escape a value for interpolation into a template literal in generated code. */
|
|
49
|
+
export function emitTemplateSafe(value) {
|
|
50
|
+
return value
|
|
51
|
+
.replace(/\\/g, '\\\\')
|
|
52
|
+
.replace(/`/g, '\\`')
|
|
53
|
+
.replace(/\$\{/g, '\\${');
|
|
54
|
+
}
|
|
11
55
|
// ── Evolved Generators (v4) ─────────────────────────────────────────────
|
|
12
56
|
// Populated at startup by evolved-node-loader. Checked in generateCoreNode
|
|
13
57
|
// before the default case, allowing graduated nodes to produce output.
|
|
@@ -97,10 +141,11 @@ export function exportPrefix(node) {
|
|
|
97
141
|
// type name=PlanState values="draft|approved|running|paused|completed|failed|cancelled"
|
|
98
142
|
// → export type PlanState = 'draft' | 'approved' | 'running' | ...;
|
|
99
143
|
export function generateType(node) {
|
|
100
|
-
const { name, values, alias } = p(node);
|
|
144
|
+
const { name: rawName, values, alias } = p(node);
|
|
145
|
+
const name = emitIdentifier(rawName, 'UnknownType', node);
|
|
101
146
|
const exp = exportPrefix(node);
|
|
102
147
|
if (values) {
|
|
103
|
-
const members = values.split('|').map(v => `'${v.trim()}'`).join(' | ');
|
|
148
|
+
const members = values.split('|').map(v => `'${emitTemplateSafe(v.trim())}'`).join(' | ');
|
|
104
149
|
return [`${exp}type ${name} = ${members};`];
|
|
105
150
|
}
|
|
106
151
|
if (alias) {
|
|
@@ -116,15 +161,16 @@ export function generateType(node) {
|
|
|
116
161
|
// field name=engineId type=string optional=true
|
|
117
162
|
export function generateInterface(node) {
|
|
118
163
|
const props = p(node);
|
|
119
|
-
const name = props.name;
|
|
164
|
+
const name = emitIdentifier(props.name, 'UnknownInterface', node);
|
|
120
165
|
const ext = props.extends ? ` extends ${props.extends}` : '';
|
|
121
166
|
const exp = exportPrefix(node);
|
|
122
167
|
const lines = [];
|
|
123
168
|
lines.push(`${exp}interface ${name}${ext} {`);
|
|
124
169
|
for (const field of kids(node, 'field')) {
|
|
125
170
|
const fp = p(field);
|
|
171
|
+
const fieldName = emitIdentifier(fp.name, 'field', field);
|
|
126
172
|
const opt = fp.optional === 'true' || fp.optional === true ? '?' : '';
|
|
127
|
-
lines.push(` ${
|
|
173
|
+
lines.push(` ${fieldName}${opt}: ${fp.type};`);
|
|
128
174
|
}
|
|
129
175
|
lines.push('}');
|
|
130
176
|
return lines;
|
|
@@ -141,8 +187,8 @@ export function generateInterface(node) {
|
|
|
141
187
|
// | { type: 'code'; language: string; code: string };
|
|
142
188
|
export function generateUnion(node) {
|
|
143
189
|
const props = p(node);
|
|
144
|
-
const name = props.name;
|
|
145
|
-
const discriminant = props.discriminant
|
|
190
|
+
const name = emitIdentifier(props.name, 'UnknownUnion', node);
|
|
191
|
+
const discriminant = emitIdentifier(props.discriminant, 'type', node);
|
|
146
192
|
const exp = exportPrefix(node);
|
|
147
193
|
const variants = kids(node, 'variant');
|
|
148
194
|
if (variants.length === 0) {
|
|
@@ -151,9 +197,9 @@ export function generateUnion(node) {
|
|
|
151
197
|
const lines = [`${exp}type ${name} =`];
|
|
152
198
|
for (let i = 0; i < variants.length; i++) {
|
|
153
199
|
const vp = p(variants[i]);
|
|
154
|
-
const vname = vp.name;
|
|
200
|
+
const vname = emitIdentifier(vp.name, 'variant', variants[i]);
|
|
155
201
|
const fields = kids(variants[i], 'field');
|
|
156
|
-
const fieldParts = [`${discriminant}: '${vname}'`];
|
|
202
|
+
const fieldParts = [`${discriminant}: '${emitTemplateSafe(vname)}'`];
|
|
157
203
|
for (const field of fields) {
|
|
158
204
|
const fp = p(field);
|
|
159
205
|
const opt = fp.optional === 'true' || fp.optional === true ? '?' : '';
|
|
@@ -180,7 +226,7 @@ export function generateUnion(node) {
|
|
|
180
226
|
// → export const tracker = new TokenTracker();
|
|
181
227
|
export function generateService(node) {
|
|
182
228
|
const props = p(node);
|
|
183
|
-
const name = props.name;
|
|
229
|
+
const name = emitIdentifier(props.name, 'UnknownService', node);
|
|
184
230
|
const impl = props.implements;
|
|
185
231
|
const exp = exportPrefix(node);
|
|
186
232
|
const lines = [];
|
|
@@ -214,7 +260,7 @@ export function generateService(node) {
|
|
|
214
260
|
// Methods
|
|
215
261
|
for (const method of kids(node, 'method')) {
|
|
216
262
|
const mp = p(method);
|
|
217
|
-
const mname = mp.name;
|
|
263
|
+
const mname = emitIdentifier(mp.name, 'method', method);
|
|
218
264
|
const mparams = mp.params ? parseParamList(mp.params) : '';
|
|
219
265
|
const isAsync = mp.async === 'true' || mp.async === true;
|
|
220
266
|
const isStream = mp.stream === 'true' || mp.stream === true;
|
|
@@ -241,8 +287,8 @@ export function generateService(node) {
|
|
|
241
287
|
// Singleton instances
|
|
242
288
|
for (const singleton of kids(node, 'singleton')) {
|
|
243
289
|
const sp = p(singleton);
|
|
244
|
-
const sname = sp.name;
|
|
245
|
-
const stype = sp.type
|
|
290
|
+
const sname = emitIdentifier(sp.name, 'instance', singleton);
|
|
291
|
+
const stype = emitIdentifier(sp.type, name, singleton);
|
|
246
292
|
lines.push('');
|
|
247
293
|
lines.push(`${exp}const ${sname} = new ${stype}();`);
|
|
248
294
|
}
|
|
@@ -255,7 +301,7 @@ export function generateService(node) {
|
|
|
255
301
|
// >>>
|
|
256
302
|
export function generateFunction(node) {
|
|
257
303
|
const props = p(node);
|
|
258
|
-
const name = props.name;
|
|
304
|
+
const name = emitIdentifier(props.name, 'unknownFn', node);
|
|
259
305
|
const params = props.params || '';
|
|
260
306
|
const returns = props.returns;
|
|
261
307
|
const isAsync = props.async === 'true' || props.async === true;
|
|
@@ -290,7 +336,7 @@ export function generateFunction(node) {
|
|
|
290
336
|
lines.push(`${exp}${asyncKw}function ${name}(${paramList})${retClause} {`);
|
|
291
337
|
// Signal → AbortController setup
|
|
292
338
|
if (hasSignal) {
|
|
293
|
-
const signalName = p(signalNode).name
|
|
339
|
+
const signalName = emitIdentifier(p(signalNode).name, 'abort', signalNode);
|
|
294
340
|
lines.push(` const ${signalName} = new AbortController();`);
|
|
295
341
|
}
|
|
296
342
|
// Wrap body in try/finally if cleanup exists
|
|
@@ -327,8 +373,8 @@ export function generateFunction(node) {
|
|
|
327
373
|
// message "Invalid plan state: expected ${expected}, got ${actual}"
|
|
328
374
|
export function generateError(node) {
|
|
329
375
|
const props = p(node);
|
|
330
|
-
const name = props.name;
|
|
331
|
-
const ext = props.extends
|
|
376
|
+
const name = emitIdentifier(props.name, 'UnknownError', node);
|
|
377
|
+
const ext = emitIdentifier(props.extends, 'Error', node);
|
|
332
378
|
const message = props.message;
|
|
333
379
|
const exp = exportPrefix(node);
|
|
334
380
|
const fields = kids(node, 'field');
|
|
@@ -412,18 +458,18 @@ export function generateError(node) {
|
|
|
412
458
|
// - approvePlan(), startPlan(), cancelPlan(), failPlan() functions
|
|
413
459
|
export function generateMachine(node) {
|
|
414
460
|
const props = p(node);
|
|
415
|
-
const name = props.name;
|
|
461
|
+
const name = emitIdentifier(props.name, 'UnknownMachine', node);
|
|
416
462
|
const exp = exportPrefix(node);
|
|
417
463
|
const lines = [];
|
|
418
464
|
// Collect states
|
|
419
465
|
const states = kids(node, 'state');
|
|
420
466
|
const stateNames = states.map(s => {
|
|
421
467
|
const sp = p(s);
|
|
422
|
-
return (sp.name || sp.value);
|
|
468
|
+
return emitIdentifier((sp.name || sp.value), 'state', s);
|
|
423
469
|
});
|
|
424
470
|
// State type
|
|
425
471
|
const stateType = `${name}State`;
|
|
426
|
-
lines.push(`${exp}type ${stateType} = ${stateNames.map(s => `'${s}'`).join(' | ')};`);
|
|
472
|
+
lines.push(`${exp}type ${stateType} = ${stateNames.map(s => `'${emitTemplateSafe(s)}'`).join(' | ')};`);
|
|
427
473
|
lines.push('');
|
|
428
474
|
// Error class
|
|
429
475
|
const errorName = `${name}StateError`;
|
|
@@ -442,7 +488,7 @@ export function generateMachine(node) {
|
|
|
442
488
|
const transitions = kids(node, 'transition');
|
|
443
489
|
for (const t of transitions) {
|
|
444
490
|
const tp = p(t);
|
|
445
|
-
const tname = tp.name;
|
|
491
|
+
const tname = emitIdentifier(tp.name, 'transition', t);
|
|
446
492
|
const from = tp.from;
|
|
447
493
|
const to = tp.to;
|
|
448
494
|
const fromStates = from.split('|').map(s => s.trim());
|
|
@@ -480,7 +526,7 @@ export function generateMachine(node) {
|
|
|
480
526
|
// Called by transpiler-ink.ts when target=ink.
|
|
481
527
|
export function generateMachineReducer(node) {
|
|
482
528
|
const props = p(node);
|
|
483
|
-
const name = props.name;
|
|
529
|
+
const name = emitIdentifier(props.name, 'UnknownMachine', node);
|
|
484
530
|
const exp = exportPrefix(node);
|
|
485
531
|
const lines = [];
|
|
486
532
|
// First emit the standard machine output
|
|
@@ -496,7 +542,7 @@ export function generateMachineReducer(node) {
|
|
|
496
542
|
const transitions = kids(node, 'transition');
|
|
497
543
|
const stateType = `${name}State`;
|
|
498
544
|
// Action type union
|
|
499
|
-
const actionNames = transitions.map(t => p(t).name);
|
|
545
|
+
const actionNames = transitions.map(t => emitIdentifier(p(t).name, 'action', t));
|
|
500
546
|
lines.push(`${exp}type ${name}Action = ${actionNames.map(a => `'${a}'`).join(' | ')};`);
|
|
501
547
|
lines.push('');
|
|
502
548
|
// Reducer function
|
|
@@ -505,9 +551,9 @@ export function generateMachineReducer(node) {
|
|
|
505
551
|
lines.push(` switch (action) {`);
|
|
506
552
|
for (const t of transitions) {
|
|
507
553
|
const tp = p(t);
|
|
508
|
-
const tname = tp.name;
|
|
554
|
+
const tname = emitIdentifier(tp.name, 'action', t);
|
|
509
555
|
const fnName = `${tname}${name}`;
|
|
510
|
-
lines.push(` case '${tname}': return ${fnName}(entity).state;`);
|
|
556
|
+
lines.push(` case '${emitTemplateSafe(tname)}': return ${fnName}(entity).state;`);
|
|
511
557
|
}
|
|
512
558
|
lines.push(` default: return state;`);
|
|
513
559
|
lines.push(` }`);
|
|
@@ -527,7 +573,7 @@ export function generateMachineReducer(node) {
|
|
|
527
573
|
// field name=approvalLevel type=ApprovalLevel default="plan"
|
|
528
574
|
export function generateConfig(node) {
|
|
529
575
|
const props = p(node);
|
|
530
|
-
const name = props.name;
|
|
576
|
+
const name = emitIdentifier(props.name, 'Config', node);
|
|
531
577
|
const exp = exportPrefix(node);
|
|
532
578
|
const fields = kids(node, 'field');
|
|
533
579
|
const lines = [];
|
|
@@ -535,8 +581,9 @@ export function generateConfig(node) {
|
|
|
535
581
|
lines.push(`${exp}interface ${name} {`);
|
|
536
582
|
for (const field of fields) {
|
|
537
583
|
const fp = p(field);
|
|
584
|
+
const fieldName = emitIdentifier(fp.name, 'field', field);
|
|
538
585
|
const opt = fp.default !== undefined ? '?' : '';
|
|
539
|
-
lines.push(` ${
|
|
586
|
+
lines.push(` ${fieldName}${opt}: ${fp.type};`);
|
|
540
587
|
}
|
|
541
588
|
lines.push('}');
|
|
542
589
|
lines.push('');
|
|
@@ -544,6 +591,7 @@ export function generateConfig(node) {
|
|
|
544
591
|
lines.push(`${exp}const DEFAULT_${name.replace(/([a-z])([A-Z])/g, '$1_$2').toUpperCase()}: Required<${name}> = {`);
|
|
545
592
|
for (const field of fields) {
|
|
546
593
|
const fp = p(field);
|
|
594
|
+
const fieldName = emitIdentifier(fp.name, 'field', field);
|
|
547
595
|
const ftype = fp.type;
|
|
548
596
|
let def = fp.default;
|
|
549
597
|
if (def === undefined) {
|
|
@@ -557,9 +605,9 @@ export function generateConfig(node) {
|
|
|
557
605
|
def = "''";
|
|
558
606
|
}
|
|
559
607
|
else if (ftype === 'string' || (!['number', 'boolean'].includes(ftype) && !ftype.endsWith('[]') && !def.startsWith("'") && !def.startsWith('"'))) {
|
|
560
|
-
def =
|
|
608
|
+
def = emitStringLiteral(def);
|
|
561
609
|
}
|
|
562
|
-
lines.push(` ${
|
|
610
|
+
lines.push(` ${fieldName}: ${def},`);
|
|
563
611
|
}
|
|
564
612
|
lines.push('};');
|
|
565
613
|
return lines;
|
|
@@ -569,16 +617,17 @@ export function generateConfig(node) {
|
|
|
569
617
|
// model Plan
|
|
570
618
|
export function generateStore(node) {
|
|
571
619
|
const props = p(node);
|
|
572
|
-
const name = props.name;
|
|
573
|
-
const
|
|
574
|
-
const key = props.key
|
|
575
|
-
const model = props.model
|
|
620
|
+
const name = emitIdentifier(props.name, 'Store', node);
|
|
621
|
+
const rawPath = props.path || '~/.data';
|
|
622
|
+
const key = emitIdentifier(props.key, 'id', node);
|
|
623
|
+
const model = emitIdentifier(props.model, 'unknown', node);
|
|
576
624
|
const exp = exportPrefix(node);
|
|
577
625
|
const lines = [];
|
|
578
626
|
const dirConst = `${name.toUpperCase()}_DIR`;
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
627
|
+
// Validate path before interpolation — blocks injection + traversal via storePath
|
|
628
|
+
const resolvedPath = rawPath.startsWith('~/')
|
|
629
|
+
? `join(homedir(), ${emitPath(rawPath.slice(2), node)})`
|
|
630
|
+
: emitPath(rawPath, node);
|
|
582
631
|
lines.push(`import { readFileSync, writeFileSync, mkdirSync, readdirSync, unlinkSync } from 'node:fs';`);
|
|
583
632
|
lines.push(`import { join, resolve } from 'node:path';`);
|
|
584
633
|
lines.push(`import { homedir } from 'node:os';`);
|
|
@@ -591,6 +640,7 @@ export function generateStore(node) {
|
|
|
591
640
|
lines.push('');
|
|
592
641
|
lines.push(`function safe${name}Path(id: string): string {`);
|
|
593
642
|
lines.push(` const sanitized = id.replace(/[^a-zA-Z0-9_-]/g, '');`);
|
|
643
|
+
lines.push(` if (!sanitized) throw new Error(\`Invalid ID: \${id}\`);`);
|
|
594
644
|
lines.push(` const full = resolve(${dirConst}, \`\${sanitized}.json\`);`);
|
|
595
645
|
lines.push(` if (!full.startsWith(resolve(${dirConst}))) throw new Error(\`Invalid ID: \${id}\`);`);
|
|
596
646
|
lines.push(` return full;`);
|
|
@@ -603,17 +653,18 @@ export function generateStore(node) {
|
|
|
603
653
|
lines.push('');
|
|
604
654
|
lines.push(`${exp}function load${name}(id: string): ${model} | null {`);
|
|
605
655
|
lines.push(` try { return JSON.parse(readFileSync(safe${name}Path(id), 'utf-8')) as ${model}; }`);
|
|
606
|
-
lines.push(` catch { return null; }`);
|
|
656
|
+
lines.push(` catch (e) { if ((e as NodeJS.ErrnoException).code === 'ENOENT') return null; throw e; }`);
|
|
607
657
|
lines.push('}');
|
|
608
658
|
lines.push('');
|
|
609
659
|
lines.push(`${exp}function list${name}s(limit = 20): ${model}[] {`);
|
|
610
660
|
lines.push(` ensure${name}Dir();`);
|
|
611
|
-
lines.push(`
|
|
612
|
-
lines.push(`
|
|
613
|
-
lines.push(`
|
|
614
|
-
lines.push(`
|
|
615
|
-
lines.push(`
|
|
616
|
-
lines.push(` }
|
|
661
|
+
lines.push(` const files = readdirSync(${dirConst}).filter(f => f.endsWith('.json'));`);
|
|
662
|
+
lines.push(` const items: ${model}[] = [];`);
|
|
663
|
+
lines.push(` for (const f of files) {`);
|
|
664
|
+
lines.push(` try { items.push(JSON.parse(readFileSync(join(${dirConst}, f), 'utf-8')) as ${model}); }`);
|
|
665
|
+
lines.push(` catch { /* skip corrupt files */ }`);
|
|
666
|
+
lines.push(` }`);
|
|
667
|
+
lines.push(` return items.sort((a: any, b: any) => (b.updatedAt || '').localeCompare(a.updatedAt || '')).slice(0, limit);`);
|
|
617
668
|
lines.push('}');
|
|
618
669
|
lines.push('');
|
|
619
670
|
lines.push(`${exp}function delete${name}(id: string): boolean {`);
|
|
@@ -631,7 +682,7 @@ export function generateStore(node) {
|
|
|
631
682
|
// >>>
|
|
632
683
|
export function generateTest(node) {
|
|
633
684
|
const props = p(node);
|
|
634
|
-
const name = props.name;
|
|
685
|
+
const name = emitTemplateSafe(props.name || 'UnknownTest');
|
|
635
686
|
const lines = [];
|
|
636
687
|
lines.push(`import { describe, it, expect } from 'vitest';`);
|
|
637
688
|
lines.push('');
|
|
@@ -644,10 +695,10 @@ export function generateTest(node) {
|
|
|
644
695
|
}
|
|
645
696
|
lines.push(`describe('${name}', () => {`);
|
|
646
697
|
for (const desc of kids(node, 'describe')) {
|
|
647
|
-
const dname = p(desc).name;
|
|
698
|
+
const dname = emitTemplateSafe(p(desc).name || 'describe');
|
|
648
699
|
lines.push(` describe('${dname}', () => {`);
|
|
649
700
|
for (const test of kids(desc, 'it')) {
|
|
650
|
-
const tname = p(test).name;
|
|
701
|
+
const tname = emitTemplateSafe(p(test).name || 'test');
|
|
651
702
|
const code = handlerCode(test);
|
|
652
703
|
lines.push(` it('${tname}', () => {`);
|
|
653
704
|
if (code) {
|
|
@@ -660,7 +711,7 @@ export function generateTest(node) {
|
|
|
660
711
|
}
|
|
661
712
|
// Top-level it blocks
|
|
662
713
|
for (const test of kids(node, 'it')) {
|
|
663
|
-
const tname = p(test).name;
|
|
714
|
+
const tname = emitTemplateSafe(p(test).name || 'test');
|
|
664
715
|
const code = handlerCode(test);
|
|
665
716
|
lines.push(` it('${tname}', () => {`);
|
|
666
717
|
if (code) {
|
|
@@ -679,12 +730,12 @@ export function generateTest(node) {
|
|
|
679
730
|
// type name="winner:determined" data="{ winner: string, bestScore: number }"
|
|
680
731
|
export function generateEvent(node) {
|
|
681
732
|
const props = p(node);
|
|
682
|
-
const name = props.name;
|
|
733
|
+
const name = emitIdentifier(props.name, 'UnknownEvent', node);
|
|
683
734
|
const exp = exportPrefix(node);
|
|
684
735
|
const types = kids(node, 'type');
|
|
685
736
|
const lines = [];
|
|
686
737
|
// Event type union
|
|
687
|
-
lines.push(`${exp}type ${name}Type = ${types.map(t => `'${(p(t).name || p(t).value)}'`).join(' | ')};`);
|
|
738
|
+
lines.push(`${exp}type ${name}Type = ${types.map(t => `'${emitTemplateSafe((p(t).name || p(t).value))}'`).join(' | ')};`);
|
|
688
739
|
lines.push('');
|
|
689
740
|
// Event interface
|
|
690
741
|
lines.push(`${exp}interface ${name} {`);
|
|
@@ -697,7 +748,7 @@ export function generateEvent(node) {
|
|
|
697
748
|
lines.push(`${exp}interface ${name}Map {`);
|
|
698
749
|
for (const t of types) {
|
|
699
750
|
const tp = p(t);
|
|
700
|
-
const tname = (tp.name || tp.value);
|
|
751
|
+
const tname = emitTemplateSafe((tp.name || tp.value));
|
|
701
752
|
const data = tp.data || 'Record<string, unknown>';
|
|
702
753
|
lines.push(` '${tname}': ${data};`);
|
|
703
754
|
}
|
|
@@ -853,7 +904,7 @@ export function generateWebSocket(node) {
|
|
|
853
904
|
// export from="./plan.js" names="createPlan,advanceStep"
|
|
854
905
|
export function generateModule(node) {
|
|
855
906
|
const props = p(node);
|
|
856
|
-
const name = props.name;
|
|
907
|
+
const name = emitTemplateSafe(props.name || 'unknown');
|
|
857
908
|
const lines = [];
|
|
858
909
|
lines.push(`// ── Module: ${name} ──`);
|
|
859
910
|
lines.push('');
|
|
@@ -949,7 +1000,7 @@ export function generateImport(node) {
|
|
|
949
1000
|
// → export const DEFAULT_WEIGHTS: ScoreWeights = { pass: 50 };
|
|
950
1001
|
export function generateConst(node) {
|
|
951
1002
|
const props = p(node);
|
|
952
|
-
const name = props.name;
|
|
1003
|
+
const name = emitIdentifier(props.name, 'unknownConst', node);
|
|
953
1004
|
const constType = props.type;
|
|
954
1005
|
const value = props.value;
|
|
955
1006
|
const exp = exportPrefix(node);
|
|
@@ -1090,7 +1141,7 @@ export function generateDerive(node) {
|
|
|
1090
1141
|
const conf = p(node).confidence;
|
|
1091
1142
|
const todo = emitLowConfidenceTodo(node, conf);
|
|
1092
1143
|
const props = p(node);
|
|
1093
|
-
const name = props.name;
|
|
1144
|
+
const name = emitIdentifier(props.name, 'derived', node);
|
|
1094
1145
|
const expr = props.expr;
|
|
1095
1146
|
const constType = props.type;
|
|
1096
1147
|
const exp = exportPrefix(node);
|
|
@@ -1105,7 +1156,7 @@ export function generateTransform(node) {
|
|
|
1105
1156
|
const conf = p(node).confidence;
|
|
1106
1157
|
const todo = emitLowConfidenceTodo(node, conf);
|
|
1107
1158
|
const props = p(node);
|
|
1108
|
-
const name = props.name;
|
|
1159
|
+
const name = emitIdentifier(props.name, 'transform', node);
|
|
1109
1160
|
const target = props.target;
|
|
1110
1161
|
const via = props.via;
|
|
1111
1162
|
const constType = props.type;
|
|
@@ -1138,7 +1189,7 @@ export function generateAction(node) {
|
|
|
1138
1189
|
const conf = p(node).confidence;
|
|
1139
1190
|
const todo = emitLowConfidenceTodo(node, conf);
|
|
1140
1191
|
const props = p(node);
|
|
1141
|
-
const name = props.name;
|
|
1192
|
+
const name = emitIdentifier(props.name, 'action', node);
|
|
1142
1193
|
const idempotent = props.idempotent === 'true' || props.idempotent === true;
|
|
1143
1194
|
const reversible = props.reversible === 'true' || props.reversible === true;
|
|
1144
1195
|
const params = props.params || '';
|
|
@@ -1257,7 +1308,7 @@ export function generateCollect(node) {
|
|
|
1257
1308
|
const conf = p(node).confidence;
|
|
1258
1309
|
const todo = emitLowConfidenceTodo(node, conf);
|
|
1259
1310
|
const props = p(node);
|
|
1260
|
-
const name = props.name;
|
|
1311
|
+
const name = emitIdentifier(props.name, 'collected', node);
|
|
1261
1312
|
const from = props.from;
|
|
1262
1313
|
const where = props.where;
|
|
1263
1314
|
const limit = props.limit;
|
|
@@ -1314,7 +1365,7 @@ export function generateResolve(node) {
|
|
|
1314
1365
|
const conf = p(node).confidence;
|
|
1315
1366
|
const todo = emitLowConfidenceTodo(node, conf);
|
|
1316
1367
|
const props = p(node);
|
|
1317
|
-
const name = props.name;
|
|
1368
|
+
const name = emitIdentifier(props.name, 'resolver', node);
|
|
1318
1369
|
const candidates = kids(node, 'candidate');
|
|
1319
1370
|
const discriminator = firstChild(node, 'discriminator');
|
|
1320
1371
|
if (!discriminator)
|
|
@@ -1328,7 +1379,7 @@ export function generateResolve(node) {
|
|
|
1328
1379
|
lines.push(`const _${name}_candidates = [`);
|
|
1329
1380
|
for (const c of candidates) {
|
|
1330
1381
|
const cp = p(c);
|
|
1331
|
-
const cname = cp.name;
|
|
1382
|
+
const cname = emitIdentifier(cp.name, 'candidate', c);
|
|
1332
1383
|
const code = handlerCode(c);
|
|
1333
1384
|
lines.push(` { name: '${cname}', fn: (signal: unknown) => { ${code.trim()} } },`);
|
|
1334
1385
|
}
|
|
@@ -1392,7 +1443,7 @@ export function generateRecover(node) {
|
|
|
1392
1443
|
const conf = p(node).confidence;
|
|
1393
1444
|
const todo = emitLowConfidenceTodo(node, conf);
|
|
1394
1445
|
const props = p(node);
|
|
1395
|
-
const name = props.name;
|
|
1446
|
+
const name = emitIdentifier(props.name, 'recovery', node);
|
|
1396
1447
|
const strategies = kids(node, 'strategy');
|
|
1397
1448
|
const hasFallback = strategies.some(s => p(s).name === 'fallback');
|
|
1398
1449
|
if (!hasFallback)
|
|
@@ -1402,7 +1453,7 @@ export function generateRecover(node) {
|
|
|
1402
1453
|
lines.push(`async function ${name}WithRecovery<T>(fn: () => Promise<T>): Promise<T> {`);
|
|
1403
1454
|
for (const strategy of strategies) {
|
|
1404
1455
|
const sp = p(strategy);
|
|
1405
|
-
const sname = sp.name;
|
|
1456
|
+
const sname = emitIdentifier(sp.name, 'strategy', strategy);
|
|
1406
1457
|
const code = handlerCode(strategy);
|
|
1407
1458
|
if (sname === 'retry') {
|
|
1408
1459
|
const max = Number(sp.max) || 3;
|
|
@@ -1446,16 +1497,16 @@ export function generatePattern(node) {
|
|
|
1446
1497
|
// pattern nodes are registered as templates — no direct output
|
|
1447
1498
|
return [];
|
|
1448
1499
|
}
|
|
1449
|
-
export function generateApply(node) {
|
|
1500
|
+
export function generateApply(node, _depth = 0) {
|
|
1450
1501
|
// apply nodes expand the referenced pattern
|
|
1451
1502
|
const props = p(node);
|
|
1452
1503
|
const patternName = props.pattern;
|
|
1453
1504
|
if (!patternName)
|
|
1454
1505
|
return [];
|
|
1455
|
-
// Delegate to template expansion
|
|
1506
|
+
// Delegate to template expansion — propagate depth to prevent infinite recursion
|
|
1456
1507
|
const syntheticNode = { ...node, type: patternName };
|
|
1457
1508
|
if (isTemplateNode(patternName)) {
|
|
1458
|
-
return expandTemplateNode(syntheticNode);
|
|
1509
|
+
return expandTemplateNode(syntheticNode, _depth + 1);
|
|
1459
1510
|
}
|
|
1460
1511
|
return [`// apply: pattern '${patternName}' not found`];
|
|
1461
1512
|
}
|
|
@@ -1509,12 +1560,12 @@ export function generateSelect(node) {
|
|
|
1509
1560
|
const lines = onChange ? [`{/* kern:use-client */}`] : [];
|
|
1510
1561
|
lines.push(`<select ${attrs.join(' ')}>`);
|
|
1511
1562
|
if (placeholder) {
|
|
1512
|
-
lines.push(` <option value="" disabled>${placeholder}</option>`);
|
|
1563
|
+
lines.push(` <option value="" disabled>${emitTemplateSafe(placeholder)}</option>`);
|
|
1513
1564
|
}
|
|
1514
1565
|
for (const opt of kids(node, 'option')) {
|
|
1515
1566
|
const op = p(opt);
|
|
1516
|
-
const optValue = op.value || '';
|
|
1517
|
-
const optLabel = op.label ||
|
|
1567
|
+
const optValue = emitTemplateSafe(op.value || '');
|
|
1568
|
+
const optLabel = emitTemplateSafe(op.label || op.value || '');
|
|
1518
1569
|
lines.push(` <option value="${optValue}">${optLabel}</option>`);
|
|
1519
1570
|
}
|
|
1520
1571
|
lines.push(`</select>`);
|
|
@@ -1527,7 +1578,7 @@ export function generateSelect(node) {
|
|
|
1527
1578
|
// relation name=posts target=Post kind=one-to-many cascade=delete
|
|
1528
1579
|
export function generateModel(node) {
|
|
1529
1580
|
const props = p(node);
|
|
1530
|
-
const name = props.name;
|
|
1581
|
+
const name = emitIdentifier(props.name, 'UnknownModel', node);
|
|
1531
1582
|
const table = props.table;
|
|
1532
1583
|
const exp = exportPrefix(node);
|
|
1533
1584
|
const lines = [];
|
|
@@ -1535,14 +1586,14 @@ export function generateModel(node) {
|
|
|
1535
1586
|
lines.push(`${exp}interface ${name} {`);
|
|
1536
1587
|
for (const col of kids(node, 'column')) {
|
|
1537
1588
|
const cp = p(col);
|
|
1538
|
-
const colName = cp.name;
|
|
1589
|
+
const colName = emitIdentifier(cp.name, 'column', col);
|
|
1539
1590
|
const colType = mapColumnType(cp.type);
|
|
1540
1591
|
const opt = cp.optional === 'true' || cp.optional === true ? '?' : '';
|
|
1541
1592
|
lines.push(` ${colName}${opt}: ${colType};`);
|
|
1542
1593
|
}
|
|
1543
1594
|
for (const rel of kids(node, 'relation')) {
|
|
1544
1595
|
const rp = p(rel);
|
|
1545
|
-
const relName = rp.name;
|
|
1596
|
+
const relName = emitIdentifier(rp.name, 'relation', rel);
|
|
1546
1597
|
const target = rp.target;
|
|
1547
1598
|
const kind = rp.kind || 'one-to-many';
|
|
1548
1599
|
const relType = kind.includes('many') ? `${target}[]` : target;
|
|
@@ -1572,7 +1623,7 @@ function mapColumnType(kernType) {
|
|
|
1572
1623
|
// handler <<<return this.findOne({ email });>>>
|
|
1573
1624
|
export function generateRepository(node) {
|
|
1574
1625
|
const props = p(node);
|
|
1575
|
-
const name = props.name;
|
|
1626
|
+
const name = emitIdentifier(props.name, 'UnknownRepo', node);
|
|
1576
1627
|
const model = props.model;
|
|
1577
1628
|
const exp = exportPrefix(node);
|
|
1578
1629
|
const lines = [];
|
|
@@ -1583,7 +1634,7 @@ export function generateRepository(node) {
|
|
|
1583
1634
|
}
|
|
1584
1635
|
for (const method of kids(node, 'method')) {
|
|
1585
1636
|
const mp = p(method);
|
|
1586
|
-
const mname = mp.name;
|
|
1637
|
+
const mname = emitIdentifier(mp.name, 'method', method);
|
|
1587
1638
|
const mparams = mp.params ? parseParamList(mp.params) : '';
|
|
1588
1639
|
const isAsync = mp.async === 'true' || mp.async === true;
|
|
1589
1640
|
const asyncKw = isAsync ? 'async ' : '';
|
|
@@ -1608,7 +1659,7 @@ export function generateRepository(node) {
|
|
|
1608
1659
|
// returns AuthService with=repo
|
|
1609
1660
|
export function generateDependency(node) {
|
|
1610
1661
|
const props = p(node);
|
|
1611
|
-
const name = props.name;
|
|
1662
|
+
const name = emitIdentifier(props.name, 'unknownDep', node);
|
|
1612
1663
|
const scope = props.scope || 'transient';
|
|
1613
1664
|
const exp = exportPrefix(node);
|
|
1614
1665
|
const lines = [];
|
|
@@ -1625,7 +1676,7 @@ export function generateDependency(node) {
|
|
|
1625
1676
|
}
|
|
1626
1677
|
for (const inj of injects) {
|
|
1627
1678
|
const ip = p(inj);
|
|
1628
|
-
const injName = ip.name;
|
|
1679
|
+
const injName = emitIdentifier(ip.name, 'dep', inj);
|
|
1629
1680
|
const injType = ip.type;
|
|
1630
1681
|
const injFrom = ip.from;
|
|
1631
1682
|
const injWith = ip.with;
|
|
@@ -1660,7 +1711,7 @@ export function generateDependency(node) {
|
|
|
1660
1711
|
// invalidate on=userUpdate tags="user:{id}"
|
|
1661
1712
|
export function generateCache(node) {
|
|
1662
1713
|
const props = p(node);
|
|
1663
|
-
const name = props.name;
|
|
1714
|
+
const name = emitIdentifier(props.name, 'unknownCache', node);
|
|
1664
1715
|
const backend = props.backend || 'memory';
|
|
1665
1716
|
const prefix = props.prefix || '';
|
|
1666
1717
|
const ttl = props.ttl;
|
|
@@ -1675,7 +1726,7 @@ export function generateCache(node) {
|
|
|
1675
1726
|
// Entry methods
|
|
1676
1727
|
for (const entry of kids(node, 'entry')) {
|
|
1677
1728
|
const ep = p(entry);
|
|
1678
|
-
const entryName = ep.name;
|
|
1729
|
+
const entryName = emitIdentifier(ep.name, 'entry', entry);
|
|
1679
1730
|
const key = ep.key || entryName;
|
|
1680
1731
|
const strategyNode = firstChild(entry, 'strategy');
|
|
1681
1732
|
const strategy = strategyNode ? (p(strategyNode).name || 'cache-aside') : 'cache-aside';
|