@kernlang/core 3.1.2 → 3.1.4

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.
@@ -8,103 +8,62 @@
8
8
  */
9
9
  import { isTemplateNode, expandTemplateNode } from './template-engine.js';
10
10
  import { KernCodegenError } from './errors.js';
11
+ import { defaultRuntime } from './runtime.js';
12
+ // Re-export emitters and helpers from extracted modules for backward compatibility.
13
+ // All existing `import { emitIdentifier } from './codegen-core.js'` paths continue to work.
14
+ export { emitIdentifier, emitStringLiteral, emitPath, emitTemplateSafe, emitTypeAnnotation, emitImportSpecifier } from './codegen/emitters.js';
15
+ export { getProps, getChildren, getFirstChild, getStyles, getPseudoStyles, getThemeRefs, dedent, cssPropertyName, handlerCode, exportPrefix, capitalize, parseParamList, emitReasonAnnotations, emitLowConfidenceTodo } from './codegen/helpers.js';
16
+ // Import for local use within this file
17
+ import { emitIdentifier, emitStringLiteral, emitPath, emitTemplateSafe, emitTypeAnnotation, emitImportSpecifier } from './codegen/emitters.js';
18
+ import { getProps, getChildren, getFirstChild, dedent, handlerCode, exportPrefix, capitalize, parseParamList, emitReasonAnnotations, emitLowConfidenceTodo } from './codegen/helpers.js';
19
+ // ── Safe Emitters & Helpers ───────────────────────────────────────────────
20
+ // Implementations extracted to codegen/emitters.ts and codegen/helpers.ts.
21
+ // Re-exported above for backward compatibility.
22
+ // (emitter implementations in codegen/emitters.ts)
23
+ // (emitTypeAnnotation, emitImportSpecifier implementations in codegen/emitters.ts)
11
24
  // ── Evolved Generators (v4) ─────────────────────────────────────────────
12
25
  // Populated at startup by evolved-node-loader. Checked in generateCoreNode
13
26
  // before the default case, allowing graduated nodes to produce output.
14
- const _evolvedGenerators = new Map();
15
- const _evolvedTargetGenerators = new Map();
27
+ // Evolved generators now live in defaultRuntime. These functions delegate for backward compatibility.
16
28
  /** Register an evolved generator (called at startup). */
17
29
  export function registerEvolvedGenerator(keyword, fn) {
18
- _evolvedGenerators.set(keyword, fn);
30
+ defaultRuntime.registerEvolvedGenerator(keyword, fn);
19
31
  }
20
32
  /** Register a target-specific evolved generator (called at startup). */
21
33
  export function registerEvolvedTargetGenerator(keyword, target, fn) {
22
- if (!_evolvedTargetGenerators.has(keyword)) {
23
- _evolvedTargetGenerators.set(keyword, new Map());
24
- }
25
- _evolvedTargetGenerators.get(keyword).set(target, fn);
34
+ defaultRuntime.registerEvolvedTargetGenerator(keyword, target, fn);
26
35
  }
27
36
  /** Unregister an evolved generator (for rollback/testing). */
28
37
  export function unregisterEvolvedGenerator(keyword) {
29
- _evolvedGenerators.delete(keyword);
30
- _evolvedTargetGenerators.delete(keyword);
38
+ defaultRuntime.unregisterEvolvedGenerator(keyword);
31
39
  }
32
40
  /** Clear all evolved generators (for test isolation). */
33
41
  export function clearEvolvedGenerators() {
34
- _evolvedGenerators.clear();
35
- _evolvedTargetGenerators.clear();
42
+ defaultRuntime.clearEvolvedGenerators();
36
43
  }
37
44
  /** Check if an evolved generator exists for a type. */
38
45
  export function hasEvolvedGenerator(type) {
39
- return _evolvedGenerators.has(type);
46
+ return defaultRuntime.hasEvolvedGenerator(type);
40
47
  }
41
48
  // ── Shared IR node helpers ───────────────────────────────────────────────
42
- // These are used by every transpiler. Exported for reuse.
43
- /** Extract props from an IR node. */
44
- export function getProps(node) {
45
- return node.props || {};
46
- }
47
- /** Get children, optionally filtered by type. */
48
- export function getChildren(node, type) {
49
- const c = node.children || [];
50
- return type ? c.filter(n => n.type === type) : c;
51
- }
52
- /** Get first child of a given type. */
53
- export function getFirstChild(node, type) {
54
- return getChildren(node, type)[0];
55
- }
56
- /** Extract styles from node props. */
57
- export function getStyles(node) {
58
- return getProps(node).styles || {};
59
- }
60
- /** Extract pseudo-styles from node props. */
61
- export function getPseudoStyles(node) {
62
- return getProps(node).pseudoStyles || {};
63
- }
64
- /** Extract theme refs from node props. */
65
- export function getThemeRefs(node) {
66
- return getProps(node).themeRefs || [];
67
- }
68
- /** Strip common leading whitespace from multiline handler code. */
69
- export function dedent(code) {
70
- const lines = code.split('\n');
71
- const nonEmpty = lines.filter(l => l.trim().length > 0);
72
- if (nonEmpty.length === 0)
73
- return code;
74
- const min = Math.min(...nonEmpty.map(l => l.match(/^(\s*)/)?.[1].length ?? 0));
75
- return lines.map(l => l.slice(min)).join('\n');
76
- }
77
- /** Convert camelCase to kebab-case for CSS property names. */
78
- export function cssPropertyName(camel) {
79
- return camel.replace(/([A-Z])/g, '-$1').toLowerCase();
80
- }
81
- /** Extract handler code from a node (finds handler child, dedents). */
82
- export function handlerCode(node) {
83
- const handler = getFirstChild(node, 'handler');
84
- if (!handler)
85
- return '';
86
- const raw = getProps(handler).code || '';
87
- return dedent(raw);
88
- }
89
- // Internal aliases for backward compat within this file
49
+ // Implementations extracted to codegen/helpers.ts. Re-exported above.
50
+ // Internal aliases for local use within this file
90
51
  const p = getProps;
91
52
  const kids = getChildren;
92
53
  const firstChild = getFirstChild;
93
- export function exportPrefix(node) {
94
- return p(node).export === 'false' ? '' : 'export ';
95
- }
96
54
  // ── Type Alias ───────────────────────────────────────────────────────────
97
55
  // type name=PlanState values="draft|approved|running|paused|completed|failed|cancelled"
98
56
  // → export type PlanState = 'draft' | 'approved' | 'running' | ...;
99
57
  export function generateType(node) {
100
- const { name, values, alias } = p(node);
58
+ const { name: rawName, values, alias } = p(node);
59
+ const name = emitIdentifier(rawName, 'UnknownType', node);
101
60
  const exp = exportPrefix(node);
102
61
  if (values) {
103
- const members = values.split('|').map(v => `'${v.trim()}'`).join(' | ');
62
+ const members = values.split('|').map(v => `'${emitTemplateSafe(v.trim())}'`).join(' | ');
104
63
  return [`${exp}type ${name} = ${members};`];
105
64
  }
106
65
  if (alias) {
107
- return [`${exp}type ${name} = ${alias};`];
66
+ return [`${exp}type ${name} = ${emitTypeAnnotation(alias, 'unknown', node)};`];
108
67
  }
109
68
  return [`${exp}type ${name} = unknown;`];
110
69
  }
@@ -116,15 +75,16 @@ export function generateType(node) {
116
75
  // field name=engineId type=string optional=true
117
76
  export function generateInterface(node) {
118
77
  const props = p(node);
119
- const name = props.name;
120
- const ext = props.extends ? ` extends ${props.extends}` : '';
78
+ const name = emitIdentifier(props.name, 'UnknownInterface', node);
79
+ const ext = props.extends ? ` extends ${emitTypeAnnotation(props.extends, 'unknown', node)}` : '';
121
80
  const exp = exportPrefix(node);
122
81
  const lines = [];
123
82
  lines.push(`${exp}interface ${name}${ext} {`);
124
83
  for (const field of kids(node, 'field')) {
125
84
  const fp = p(field);
85
+ const fieldName = emitIdentifier(fp.name, 'field', field);
126
86
  const opt = fp.optional === 'true' || fp.optional === true ? '?' : '';
127
- lines.push(` ${fp.name}${opt}: ${fp.type};`);
87
+ lines.push(` ${fieldName}${opt}: ${emitTypeAnnotation(fp.type, 'unknown', field)};`);
128
88
  }
129
89
  lines.push('}');
130
90
  return lines;
@@ -141,8 +101,8 @@ export function generateInterface(node) {
141
101
  // | { type: 'code'; language: string; code: string };
142
102
  export function generateUnion(node) {
143
103
  const props = p(node);
144
- const name = props.name;
145
- const discriminant = props.discriminant || 'type';
104
+ const name = emitIdentifier(props.name, 'UnknownUnion', node);
105
+ const discriminant = emitIdentifier(props.discriminant, 'type', node);
146
106
  const exp = exportPrefix(node);
147
107
  const variants = kids(node, 'variant');
148
108
  if (variants.length === 0) {
@@ -151,13 +111,13 @@ export function generateUnion(node) {
151
111
  const lines = [`${exp}type ${name} =`];
152
112
  for (let i = 0; i < variants.length; i++) {
153
113
  const vp = p(variants[i]);
154
- const vname = vp.name;
114
+ const vname = emitIdentifier(vp.name, 'variant', variants[i]);
155
115
  const fields = kids(variants[i], 'field');
156
- const fieldParts = [`${discriminant}: '${vname}'`];
116
+ const fieldParts = [`${discriminant}: '${emitTemplateSafe(vname)}'`];
157
117
  for (const field of fields) {
158
118
  const fp = p(field);
159
119
  const opt = fp.optional === 'true' || fp.optional === true ? '?' : '';
160
- fieldParts.push(`${fp.name}${opt}: ${fp.type}`);
120
+ fieldParts.push(`${emitIdentifier(fp.name, 'field', field)}${opt}: ${emitTypeAnnotation(fp.type, 'unknown', field)}`);
161
121
  }
162
122
  const semi = i === variants.length - 1 ? ';' : '';
163
123
  lines.push(` | { ${fieldParts.join('; ')} }${semi}`);
@@ -180,21 +140,23 @@ export function generateUnion(node) {
180
140
  // → export const tracker = new TokenTracker();
181
141
  export function generateService(node) {
182
142
  const props = p(node);
183
- const name = props.name;
143
+ const name = emitIdentifier(props.name, 'UnknownService', node);
184
144
  const impl = props.implements;
185
145
  const exp = exportPrefix(node);
186
146
  const lines = [];
187
- const implClause = impl ? ` implements ${impl}` : '';
147
+ const implClause = impl ? ` implements ${emitTypeAnnotation(impl, 'unknown', node)}` : '';
188
148
  lines.push(`${exp}class ${name}${implClause} {`);
189
149
  // Fields
190
150
  for (const field of kids(node, 'field')) {
191
151
  const fp = p(field);
152
+ const fieldName = emitIdentifier(fp.name, 'field', field);
192
153
  const vis = fp.private === 'true' || fp.private === true ? 'private ' : '';
193
154
  const readonly = fp.readonly === 'true' || fp.readonly === true ? 'readonly ' : '';
194
- const typeAnnotation = fp.type ? `: ${fp.type}` : '';
155
+ const typeAnnotation = fp.type ? `: ${emitTypeAnnotation(fp.type, 'unknown', field)}` : '';
195
156
  const defaultVal = fp.default;
157
+ // default values are by-design raw code (escape hatch) — documented, not sanitized
196
158
  const init = defaultVal !== undefined ? ` = ${defaultVal}` : '';
197
- lines.push(` ${vis}${readonly}${fp.name}${typeAnnotation}${init};`);
159
+ lines.push(` ${vis}${readonly}${fieldName}${typeAnnotation}${init};`);
198
160
  }
199
161
  // Constructor (if any constructor child exists)
200
162
  const ctorNode = firstChild(node, 'constructor');
@@ -214,7 +176,7 @@ export function generateService(node) {
214
176
  // Methods
215
177
  for (const method of kids(node, 'method')) {
216
178
  const mp = p(method);
217
- const mname = mp.name;
179
+ const mname = emitIdentifier(mp.name, 'method', method);
218
180
  const mparams = mp.params ? parseParamList(mp.params) : '';
219
181
  const isAsync = mp.async === 'true' || mp.async === true;
220
182
  const isStream = mp.stream === 'true' || mp.stream === true;
@@ -226,8 +188,8 @@ export function generateService(node) {
226
188
  const mcode = handlerCode(method);
227
189
  // stream=true → AsyncGenerator return type
228
190
  const mreturns = isStream
229
- ? `: AsyncGenerator<${mp.returns || 'unknown'}>`
230
- : mp.returns ? `: ${mp.returns}` : '';
191
+ ? `: AsyncGenerator<${emitTypeAnnotation(mp.returns, 'unknown', method)}>`
192
+ : mp.returns ? `: ${emitTypeAnnotation(mp.returns, 'unknown', method)}` : '';
231
193
  lines.push('');
232
194
  lines.push(` ${vis}${staticKw}${asyncKw}${star}${mname}(${mparams})${mreturns} {`);
233
195
  if (mcode) {
@@ -241,8 +203,8 @@ export function generateService(node) {
241
203
  // Singleton instances
242
204
  for (const singleton of kids(node, 'singleton')) {
243
205
  const sp = p(singleton);
244
- const sname = sp.name;
245
- const stype = sp.type || name;
206
+ const sname = emitIdentifier(sp.name, 'instance', singleton);
207
+ const stype = emitIdentifier(sp.type, name, singleton);
246
208
  lines.push('');
247
209
  lines.push(`${exp}const ${sname} = new ${stype}();`);
248
210
  }
@@ -255,7 +217,7 @@ export function generateService(node) {
255
217
  // >>>
256
218
  export function generateFunction(node) {
257
219
  const props = p(node);
258
- const name = props.name;
220
+ const name = emitIdentifier(props.name, 'unknownFn', node);
259
221
  const params = props.params || '';
260
222
  const returns = props.returns;
261
223
  const isAsync = props.async === 'true' || props.async === true;
@@ -267,7 +229,7 @@ export function generateFunction(node) {
267
229
  const paramList = params ? parseParamList(params) : '';
268
230
  // stream=true → async generator function
269
231
  if (isStream) {
270
- const yieldType = returns || 'unknown';
232
+ const yieldType = emitTypeAnnotation(returns, 'unknown', node);
271
233
  const retClause = `: AsyncGenerator<${yieldType}>`;
272
234
  const code = handlerCode(node);
273
235
  lines.push(`${exp}async function* ${name}(${paramList})${retClause} {`);
@@ -279,7 +241,7 @@ export function generateFunction(node) {
279
241
  lines.push('}');
280
242
  return lines;
281
243
  }
282
- const retClause = returns ? `: ${returns}` : '';
244
+ const retClause = returns ? `: ${emitTypeAnnotation(returns, 'unknown', node)}` : '';
283
245
  const asyncKw = isAsync ? 'async ' : '';
284
246
  const code = handlerCode(node);
285
247
  // Gap 3: signal + cleanup support for async functions
@@ -290,7 +252,7 @@ export function generateFunction(node) {
290
252
  lines.push(`${exp}${asyncKw}function ${name}(${paramList})${retClause} {`);
291
253
  // Signal → AbortController setup
292
254
  if (hasSignal) {
293
- const signalName = p(signalNode).name || 'abort';
255
+ const signalName = emitIdentifier(p(signalNode).name, 'abort', signalNode);
294
256
  lines.push(` const ${signalName} = new AbortController();`);
295
257
  }
296
258
  // Wrap body in try/finally if cleanup exists
@@ -327,8 +289,8 @@ export function generateFunction(node) {
327
289
  // message "Invalid plan state: expected ${expected}, got ${actual}"
328
290
  export function generateError(node) {
329
291
  const props = p(node);
330
- const name = props.name;
331
- const ext = props.extends || 'Error';
292
+ const name = emitIdentifier(props.name, 'UnknownError', node);
293
+ const ext = emitIdentifier(props.extends, 'Error', node);
332
294
  const message = props.message;
333
295
  const exp = exportPrefix(node);
334
296
  const fields = kids(node, 'field');
@@ -344,11 +306,13 @@ export function generateError(node) {
344
306
  const opt = fp.optional === 'true' || fp.optional === true ? '?' : '';
345
307
  const isMessage = fp.name === 'message';
346
308
  // 'message' param is not readonly — it's passed to super
309
+ const fName = emitIdentifier(fp.name, 'field', field);
310
+ const fType = emitTypeAnnotation(fp.type, 'unknown', field);
347
311
  if (isMessage) {
348
- lines.push(` ${fp.name}${opt}: ${fp.type},`);
312
+ lines.push(` ${fName}${opt}: ${fType},`);
349
313
  }
350
314
  else {
351
- lines.push(` public readonly ${fp.name}${opt}: ${fp.type},`);
315
+ lines.push(` public readonly ${fName}${opt}: ${fType},`);
352
316
  }
353
317
  }
354
318
  lines.push(` ) {`);
@@ -412,18 +376,18 @@ export function generateError(node) {
412
376
  // - approvePlan(), startPlan(), cancelPlan(), failPlan() functions
413
377
  export function generateMachine(node) {
414
378
  const props = p(node);
415
- const name = props.name;
379
+ const name = emitIdentifier(props.name, 'UnknownMachine', node);
416
380
  const exp = exportPrefix(node);
417
381
  const lines = [];
418
382
  // Collect states
419
383
  const states = kids(node, 'state');
420
384
  const stateNames = states.map(s => {
421
385
  const sp = p(s);
422
- return (sp.name || sp.value);
386
+ return emitIdentifier((sp.name || sp.value), 'state', s);
423
387
  });
424
388
  // State type
425
389
  const stateType = `${name}State`;
426
- lines.push(`${exp}type ${stateType} = ${stateNames.map(s => `'${s}'`).join(' | ')};`);
390
+ lines.push(`${exp}type ${stateType} = ${stateNames.map(s => `'${emitTemplateSafe(s)}'`).join(' | ')};`);
427
391
  lines.push('');
428
392
  // Error class
429
393
  const errorName = `${name}StateError`;
@@ -442,7 +406,7 @@ export function generateMachine(node) {
442
406
  const transitions = kids(node, 'transition');
443
407
  for (const t of transitions) {
444
408
  const tp = p(t);
445
- const tname = tp.name;
409
+ const tname = emitIdentifier(tp.name, 'transition', t);
446
410
  const from = tp.from;
447
411
  const to = tp.to;
448
412
  const fromStates = from.split('|').map(s => s.trim());
@@ -480,7 +444,7 @@ export function generateMachine(node) {
480
444
  // Called by transpiler-ink.ts when target=ink.
481
445
  export function generateMachineReducer(node) {
482
446
  const props = p(node);
483
- const name = props.name;
447
+ const name = emitIdentifier(props.name, 'UnknownMachine', node);
484
448
  const exp = exportPrefix(node);
485
449
  const lines = [];
486
450
  // First emit the standard machine output
@@ -496,7 +460,7 @@ export function generateMachineReducer(node) {
496
460
  const transitions = kids(node, 'transition');
497
461
  const stateType = `${name}State`;
498
462
  // Action type union
499
- const actionNames = transitions.map(t => p(t).name);
463
+ const actionNames = transitions.map(t => emitIdentifier(p(t).name, 'action', t));
500
464
  lines.push(`${exp}type ${name}Action = ${actionNames.map(a => `'${a}'`).join(' | ')};`);
501
465
  lines.push('');
502
466
  // Reducer function
@@ -505,9 +469,9 @@ export function generateMachineReducer(node) {
505
469
  lines.push(` switch (action) {`);
506
470
  for (const t of transitions) {
507
471
  const tp = p(t);
508
- const tname = tp.name;
472
+ const tname = emitIdentifier(tp.name, 'action', t);
509
473
  const fnName = `${tname}${name}`;
510
- lines.push(` case '${tname}': return ${fnName}(entity).state;`);
474
+ lines.push(` case '${emitTemplateSafe(tname)}': return ${fnName}(entity).state;`);
511
475
  }
512
476
  lines.push(` default: return state;`);
513
477
  lines.push(` }`);
@@ -527,7 +491,7 @@ export function generateMachineReducer(node) {
527
491
  // field name=approvalLevel type=ApprovalLevel default="plan"
528
492
  export function generateConfig(node) {
529
493
  const props = p(node);
530
- const name = props.name;
494
+ const name = emitIdentifier(props.name, 'Config', node);
531
495
  const exp = exportPrefix(node);
532
496
  const fields = kids(node, 'field');
533
497
  const lines = [];
@@ -535,8 +499,9 @@ export function generateConfig(node) {
535
499
  lines.push(`${exp}interface ${name} {`);
536
500
  for (const field of fields) {
537
501
  const fp = p(field);
502
+ const fieldName = emitIdentifier(fp.name, 'field', field);
538
503
  const opt = fp.default !== undefined ? '?' : '';
539
- lines.push(` ${fp.name}${opt}: ${fp.type};`);
504
+ lines.push(` ${fieldName}${opt}: ${emitTypeAnnotation(fp.type, 'unknown', field)};`);
540
505
  }
541
506
  lines.push('}');
542
507
  lines.push('');
@@ -544,7 +509,8 @@ export function generateConfig(node) {
544
509
  lines.push(`${exp}const DEFAULT_${name.replace(/([a-z])([A-Z])/g, '$1_$2').toUpperCase()}: Required<${name}> = {`);
545
510
  for (const field of fields) {
546
511
  const fp = p(field);
547
- const ftype = fp.type;
512
+ const fieldName = emitIdentifier(fp.name, 'field', field);
513
+ const ftype = emitTypeAnnotation(fp.type, 'unknown', field);
548
514
  let def = fp.default;
549
515
  if (def === undefined) {
550
516
  if (ftype === 'number')
@@ -557,9 +523,9 @@ export function generateConfig(node) {
557
523
  def = "''";
558
524
  }
559
525
  else if (ftype === 'string' || (!['number', 'boolean'].includes(ftype) && !ftype.endsWith('[]') && !def.startsWith("'") && !def.startsWith('"'))) {
560
- def = `'${def}'`;
526
+ def = emitStringLiteral(def);
561
527
  }
562
- lines.push(` ${fp.name}: ${def},`);
528
+ lines.push(` ${fieldName}: ${def},`);
563
529
  }
564
530
  lines.push('};');
565
531
  return lines;
@@ -569,16 +535,17 @@ export function generateConfig(node) {
569
535
  // model Plan
570
536
  export function generateStore(node) {
571
537
  const props = p(node);
572
- const name = props.name;
573
- const storePath = props.path || '~/.data';
574
- const key = props.key || 'id';
575
- const model = props.model || 'unknown';
538
+ const name = emitIdentifier(props.name, 'Store', node);
539
+ const rawPath = props.path || '~/.data';
540
+ const key = emitIdentifier(props.key, 'id', node);
541
+ const model = emitIdentifier(props.model, 'unknown', node);
576
542
  const exp = exportPrefix(node);
577
543
  const lines = [];
578
544
  const dirConst = `${name.toUpperCase()}_DIR`;
579
- const resolvedPath = storePath.startsWith('~/')
580
- ? `join(homedir(), '${storePath.slice(2)}')`
581
- : `'${storePath}'`;
545
+ // Validate path before interpolation — blocks injection + traversal via storePath
546
+ const resolvedPath = rawPath.startsWith('~/')
547
+ ? `join(homedir(), ${emitPath(rawPath.slice(2), node)})`
548
+ : emitPath(rawPath, node);
582
549
  lines.push(`import { readFileSync, writeFileSync, mkdirSync, readdirSync, unlinkSync } from 'node:fs';`);
583
550
  lines.push(`import { join, resolve } from 'node:path';`);
584
551
  lines.push(`import { homedir } from 'node:os';`);
@@ -591,6 +558,7 @@ export function generateStore(node) {
591
558
  lines.push('');
592
559
  lines.push(`function safe${name}Path(id: string): string {`);
593
560
  lines.push(` const sanitized = id.replace(/[^a-zA-Z0-9_-]/g, '');`);
561
+ lines.push(` if (!sanitized) throw new Error(\`Invalid ID: \${id}\`);`);
594
562
  lines.push(` const full = resolve(${dirConst}, \`\${sanitized}.json\`);`);
595
563
  lines.push(` if (!full.startsWith(resolve(${dirConst}))) throw new Error(\`Invalid ID: \${id}\`);`);
596
564
  lines.push(` return full;`);
@@ -603,17 +571,18 @@ export function generateStore(node) {
603
571
  lines.push('');
604
572
  lines.push(`${exp}function load${name}(id: string): ${model} | null {`);
605
573
  lines.push(` try { return JSON.parse(readFileSync(safe${name}Path(id), 'utf-8')) as ${model}; }`);
606
- lines.push(` catch { return null; }`);
574
+ lines.push(` catch (e) { if ((e as NodeJS.ErrnoException).code === 'ENOENT') return null; throw e; }`);
607
575
  lines.push('}');
608
576
  lines.push('');
609
577
  lines.push(`${exp}function list${name}s(limit = 20): ${model}[] {`);
610
578
  lines.push(` ensure${name}Dir();`);
611
- lines.push(` try {`);
612
- lines.push(` return readdirSync(${dirConst}).filter(f => f.endsWith('.json'))`);
613
- lines.push(` .map(f => JSON.parse(readFileSync(join(${dirConst}, f), 'utf-8')) as ${model})`);
614
- lines.push(` .sort((a: any, b: any) => (b.updatedAt || '').localeCompare(a.updatedAt || ''))`);
615
- lines.push(` .slice(0, limit);`);
616
- lines.push(` } catch { return []; }`);
579
+ lines.push(` const files = readdirSync(${dirConst}).filter(f => f.endsWith('.json'));`);
580
+ lines.push(` const items: ${model}[] = [];`);
581
+ lines.push(` for (const f of files) {`);
582
+ lines.push(` try { items.push(JSON.parse(readFileSync(join(${dirConst}, f), 'utf-8')) as ${model}); }`);
583
+ lines.push(` catch { /* skip corrupt files */ }`);
584
+ lines.push(` }`);
585
+ lines.push(` return items.sort((a: any, b: any) => (b.updatedAt || '').localeCompare(a.updatedAt || '')).slice(0, limit);`);
617
586
  lines.push('}');
618
587
  lines.push('');
619
588
  lines.push(`${exp}function delete${name}(id: string): boolean {`);
@@ -631,7 +600,7 @@ export function generateStore(node) {
631
600
  // >>>
632
601
  export function generateTest(node) {
633
602
  const props = p(node);
634
- const name = props.name;
603
+ const name = emitTemplateSafe(props.name || 'UnknownTest');
635
604
  const lines = [];
636
605
  lines.push(`import { describe, it, expect } from 'vitest';`);
637
606
  lines.push('');
@@ -644,10 +613,10 @@ export function generateTest(node) {
644
613
  }
645
614
  lines.push(`describe('${name}', () => {`);
646
615
  for (const desc of kids(node, 'describe')) {
647
- const dname = p(desc).name;
616
+ const dname = emitTemplateSafe(p(desc).name || 'describe');
648
617
  lines.push(` describe('${dname}', () => {`);
649
618
  for (const test of kids(desc, 'it')) {
650
- const tname = p(test).name;
619
+ const tname = emitTemplateSafe(p(test).name || 'test');
651
620
  const code = handlerCode(test);
652
621
  lines.push(` it('${tname}', () => {`);
653
622
  if (code) {
@@ -660,7 +629,7 @@ export function generateTest(node) {
660
629
  }
661
630
  // Top-level it blocks
662
631
  for (const test of kids(node, 'it')) {
663
- const tname = p(test).name;
632
+ const tname = emitTemplateSafe(p(test).name || 'test');
664
633
  const code = handlerCode(test);
665
634
  lines.push(` it('${tname}', () => {`);
666
635
  if (code) {
@@ -679,12 +648,12 @@ export function generateTest(node) {
679
648
  // type name="winner:determined" data="{ winner: string, bestScore: number }"
680
649
  export function generateEvent(node) {
681
650
  const props = p(node);
682
- const name = props.name;
651
+ const name = emitIdentifier(props.name, 'UnknownEvent', node);
683
652
  const exp = exportPrefix(node);
684
653
  const types = kids(node, 'type');
685
654
  const lines = [];
686
655
  // Event type union
687
- lines.push(`${exp}type ${name}Type = ${types.map(t => `'${(p(t).name || p(t).value)}'`).join(' | ')};`);
656
+ lines.push(`${exp}type ${name}Type = ${types.map(t => `'${emitTemplateSafe((p(t).name || p(t).value))}'`).join(' | ')};`);
688
657
  lines.push('');
689
658
  // Event interface
690
659
  lines.push(`${exp}interface ${name} {`);
@@ -697,8 +666,8 @@ export function generateEvent(node) {
697
666
  lines.push(`${exp}interface ${name}Map {`);
698
667
  for (const t of types) {
699
668
  const tp = p(t);
700
- const tname = (tp.name || tp.value);
701
- const data = tp.data || 'Record<string, unknown>';
669
+ const tname = emitTemplateSafe((tp.name || tp.value));
670
+ const data = emitTypeAnnotation(tp.data, 'Record<string, unknown>', t);
702
671
  lines.push(` '${tname}': ${data};`);
703
672
  }
704
673
  lines.push('}');
@@ -853,44 +822,47 @@ export function generateWebSocket(node) {
853
822
  // export from="./plan.js" names="createPlan,advanceStep"
854
823
  export function generateModule(node) {
855
824
  const props = p(node);
856
- const name = props.name;
825
+ const name = emitTemplateSafe(props.name || 'unknown');
857
826
  const lines = [];
858
827
  lines.push(`// ── Module: ${name} ──`);
859
828
  lines.push('');
860
829
  for (const exp of kids(node, 'export')) {
861
830
  const ep = p(exp);
862
- const from = ep.from;
863
- const names = ep.names;
864
- const typeNames = ep.types;
831
+ const rawFrom = ep.from;
832
+ const safeFrom = rawFrom ? emitImportSpecifier(rawFrom, exp) : '';
833
+ const rawNames = ep.names;
834
+ const safeNames = rawNames ? rawNames.split(',').map(s => emitIdentifier(s.trim(), 'export', exp)).join(', ') : '';
835
+ const rawTypeNames = ep.types;
836
+ const safeTypeNames = rawTypeNames ? rawTypeNames.split(',').map(s => emitIdentifier(s.trim(), 'export', exp)).join(', ') : '';
865
837
  const star = ep.star === 'true' || ep.star === true;
866
- const defaultExport = ep.default;
838
+ const safeDefault = ep.default ? emitIdentifier(ep.default, 'default', exp) : '';
867
839
  // export * from './foo.js'
868
- if (from && !names && !typeNames && star) {
869
- lines.push(`export * from '${from}';`);
840
+ if (safeFrom && !safeNames && !safeTypeNames && star) {
841
+ lines.push(`export * from '${safeFrom}';`);
870
842
  }
871
843
  // export { a, b } from './foo.js'
872
- if (from && names) {
873
- lines.push(`export { ${names.split(',').map(s => s.trim()).join(', ')} } from '${from}';`);
844
+ if (safeFrom && safeNames) {
845
+ lines.push(`export { ${safeNames} } from '${safeFrom}';`);
874
846
  }
875
847
  // export type { A, B } from './types.js'
876
- if (from && typeNames) {
877
- lines.push(`export type { ${typeNames.split(',').map(s => s.trim()).join(', ')} } from '${from}';`);
848
+ if (safeFrom && safeTypeNames) {
849
+ lines.push(`export type { ${safeTypeNames} } from '${safeFrom}';`);
878
850
  }
879
851
  // export default foo
880
- if (defaultExport && !from) {
881
- lines.push(`export default ${defaultExport};`);
852
+ if (safeDefault && !safeFrom) {
853
+ lines.push(`export default ${safeDefault};`);
882
854
  }
883
855
  // export default from './foo.js' (re-export default)
884
- if (defaultExport && from) {
885
- lines.push(`export { default as ${defaultExport} } from '${from}';`);
856
+ if (safeDefault && safeFrom) {
857
+ lines.push(`export { default as ${safeDefault} } from '${safeFrom}';`);
886
858
  }
887
859
  // export { a, b } (no from — local re-export)
888
- if (!from && names && !defaultExport) {
889
- lines.push(`export { ${names.split(',').map(s => s.trim()).join(', ')} };`);
860
+ if (!safeFrom && safeNames && !safeDefault) {
861
+ lines.push(`export { ${safeNames} };`);
890
862
  }
891
863
  // export type { A, B } (no from — local type re-export)
892
- if (!from && typeNames && !defaultExport) {
893
- lines.push(`export type { ${typeNames.split(',').map(s => s.trim()).join(', ')} };`);
864
+ if (!safeFrom && safeTypeNames && !safeDefault) {
865
+ lines.push(`export type { ${safeTypeNames} };`);
894
866
  }
895
867
  }
896
868
  // Inline child definitions
@@ -922,21 +894,23 @@ export function generateImport(node) {
922
894
  const isTypeOnly = props.types === 'true' || props.types === true;
923
895
  if (!from)
924
896
  return [];
897
+ const safePath = emitImportSpecifier(from, node);
925
898
  const typeKw = isTypeOnly ? 'type ' : '';
899
+ const safeDefault = defaultImport ? emitIdentifier(defaultImport, 'default', node) : '';
926
900
  const namedList = names
927
- ? names.split(',').map(s => s.trim()).join(', ')
901
+ ? names.split(',').map(s => emitIdentifier(s.trim(), 'import', node)).join(', ')
928
902
  : '';
929
- if (defaultImport && namedList) {
930
- return [`import ${typeKw}${defaultImport}, { ${namedList} } from '${from}';`];
903
+ if (safeDefault && namedList) {
904
+ return [`import ${typeKw}${safeDefault}, { ${namedList} } from '${safePath}';`];
931
905
  }
932
- if (defaultImport) {
933
- return [`import ${typeKw}${defaultImport} from '${from}';`];
906
+ if (safeDefault) {
907
+ return [`import ${typeKw}${safeDefault} from '${safePath}';`];
934
908
  }
935
909
  if (namedList) {
936
- return [`import ${typeKw}{ ${namedList} } from '${from}';`];
910
+ return [`import ${typeKw}{ ${namedList} } from '${safePath}';`];
937
911
  }
938
912
  // Side-effect import
939
- return [`import '${from}';`];
913
+ return [`import '${safePath}';`];
940
914
  }
941
915
  // ── Const ───────────────────────────────────────────────────────────────
942
916
  // const name=AGON_HOME type=string
@@ -949,12 +923,12 @@ export function generateImport(node) {
949
923
  // → export const DEFAULT_WEIGHTS: ScoreWeights = { pass: 50 };
950
924
  export function generateConst(node) {
951
925
  const props = p(node);
952
- const name = props.name;
926
+ const name = emitIdentifier(props.name, 'unknownConst', node);
953
927
  const constType = props.type;
954
928
  const value = props.value;
955
929
  const exp = exportPrefix(node);
956
930
  const code = handlerCode(node);
957
- const typeAnnotation = constType ? `: ${constType}` : '';
931
+ const typeAnnotation = constType ? `: ${emitTypeAnnotation(constType, 'unknown', node)}` : '';
958
932
  if (code) {
959
933
  return [`${exp}const ${name}${typeAnnotation} = ${code.trim()};`];
960
934
  }
@@ -963,125 +937,9 @@ export function generateConst(node) {
963
937
  }
964
938
  return [`${exp}const ${name}${typeAnnotation};`];
965
939
  }
966
- // ── Shared Helpers (exported for @kernlang/react) ────────────────────────────
967
- /** Parse "name:Type,name2:Type2,spread:number=8" → "name: Type, name2: Type2, spread: number = 8"
968
- * Supports default values via = after the type. */
969
- export function parseParamList(params) {
970
- if (!params)
971
- return '';
972
- return splitParamsRespectingDepth(params).map(s => {
973
- const trimmed = s.trim();
974
- // Split name from type:default — find the first ':'
975
- const colonIdx = trimmed.indexOf(':');
976
- if (colonIdx === -1)
977
- return trimmed;
978
- const pname = trimmed.slice(0, colonIdx).trim();
979
- const rest = trimmed.slice(colonIdx + 1).trim();
980
- // Split type from default value — find '=' not inside angle brackets or parens
981
- const eqIdx = findDefaultSeparator(rest);
982
- if (eqIdx === -1) {
983
- return `${pname}: ${rest}`;
984
- }
985
- const ptype = rest.slice(0, eqIdx).trim();
986
- const pdefault = rest.slice(eqIdx + 1).trim();
987
- return `${pname}: ${ptype} = ${pdefault}`;
988
- }).join(', ');
989
- }
990
- /** Split param string on commas while respecting <>, (), {} depth.
991
- * Handles => (arrow) without decrementing depth. */
992
- function splitParamsRespectingDepth(s) {
993
- const parts = [];
994
- let depth = 0;
995
- let current = '';
996
- for (let i = 0; i < s.length; i++) {
997
- const ch = s[i];
998
- if (ch === '<' || ch === '(' || ch === '{')
999
- depth++;
1000
- else if ((ch === '>' || ch === ')' || ch === '}') && depth > 0)
1001
- depth--;
1002
- if (ch === ',' && depth === 0) {
1003
- parts.push(current);
1004
- current = '';
1005
- }
1006
- else {
1007
- current += ch;
1008
- }
1009
- }
1010
- if (current.trim())
1011
- parts.push(current);
1012
- return parts;
1013
- }
1014
- /** Find the index of '=' that separates type from default value,
1015
- * skipping '=' inside arrow functions (=>), generics, or parens. */
1016
- function findDefaultSeparator(rest) {
1017
- let depth = 0;
1018
- for (let i = 0; i < rest.length; i++) {
1019
- const ch = rest[i];
1020
- if (ch === '<' || ch === '(' || ch === '{')
1021
- depth++;
1022
- else if (ch === '>' || ch === ')' || ch === '}')
1023
- depth--;
1024
- else if (ch === '=' && depth === 0) {
1025
- // Skip '=>' (arrow function in type)
1026
- if (rest[i + 1] === '>')
1027
- continue;
1028
- return i;
1029
- }
1030
- }
1031
- return -1;
1032
- }
1033
- export function capitalize(s) {
1034
- return s.charAt(0).toUpperCase() + s.slice(1);
1035
- }
1036
- // Hook codegen moved to @kernlang/react (generateHook in codegen-react.ts)
1037
- // ── Reason & Confidence Annotations ──────────────────────────────────────
1038
- export function emitReasonAnnotations(node) {
1039
- const reasonNode = firstChild(node, 'reason');
1040
- const evidenceNode = firstChild(node, 'evidence');
1041
- const needsNodes = kids(node, 'needs');
1042
- const confidence = p(node).confidence;
1043
- if (!reasonNode && !evidenceNode && !confidence && needsNodes.length === 0)
1044
- return [];
1045
- const lines = ['/**'];
1046
- if (confidence)
1047
- lines.push(` * @confidence ${confidence}`);
1048
- if (reasonNode) {
1049
- const rp = p(reasonNode);
1050
- lines.push(` * @reason ${rp.because || ''}`);
1051
- if (rp.basis)
1052
- lines.push(` * @basis ${rp.basis}`);
1053
- if (rp.survives)
1054
- lines.push(` * @survives ${rp.survives}`);
1055
- }
1056
- if (evidenceNode) {
1057
- const ep = p(evidenceNode);
1058
- const parts = [`source=${ep.source}`];
1059
- if (ep.method)
1060
- parts.push(`method=${ep.method}`);
1061
- if (ep.authority)
1062
- parts.push(`authority=${ep.authority}`);
1063
- lines.push(` * @evidence ${parts.join(', ')}`);
1064
- }
1065
- for (const needsNode of needsNodes) {
1066
- const np = p(needsNode);
1067
- const desc = np.what || np.description || '';
1068
- const wouldRaise = np['would-raise-to'];
1069
- const tag = wouldRaise ? `${desc} (would raise to ${wouldRaise})` : desc;
1070
- lines.push(` * @needs ${tag}`);
1071
- }
1072
- lines.push(' */');
1073
- return lines;
1074
- }
1075
- /** Emit a TODO comment for nodes with low literal confidence (< 0.5). */
1076
- export function emitLowConfidenceTodo(node, confidence) {
1077
- if (!confidence)
1078
- return [];
1079
- const val = parseFloat(confidence);
1080
- if (isNaN(val) || val >= 0.5 || confidence.includes(':'))
1081
- return [];
1082
- const name = p(node).name || node.type;
1083
- return [`// TODO(low-confidence): ${name} confidence=${confidence}`];
1084
- }
940
+ // ── Shared Helpers ───────────────────────────────────────────────────────
941
+ // parseParamList, capitalize, emitReasonAnnotations, emitLowConfidenceTodo
942
+ // implementations extracted to codegen/helpers.ts. Re-exported at top of file.
1085
943
  // ── Ground Layer: derive ─────────────────────────────────────────────────
1086
944
  // derive name=loudness expr={{average(stems)}} type=number deps="stems"
1087
945
  // → export const loudness: number = average(stems);
@@ -1090,11 +948,12 @@ export function generateDerive(node) {
1090
948
  const conf = p(node).confidence;
1091
949
  const todo = emitLowConfidenceTodo(node, conf);
1092
950
  const props = p(node);
1093
- const name = props.name;
951
+ const name = emitIdentifier(props.name, 'derived', node);
952
+ // expr is by-design raw code (escape hatch)
1094
953
  const expr = props.expr;
1095
954
  const constType = props.type;
1096
955
  const exp = exportPrefix(node);
1097
- const typeAnnotation = constType ? `: ${constType}` : '';
956
+ const typeAnnotation = constType ? `: ${emitTypeAnnotation(constType, 'unknown', node)}` : '';
1098
957
  return [...todo, ...annotations, `${exp}const ${name}${typeAnnotation} = ${expr};`];
1099
958
  }
1100
959
  // ── Ground Layer: transform ──────────────────────────────────────────────
@@ -1105,13 +964,14 @@ export function generateTransform(node) {
1105
964
  const conf = p(node).confidence;
1106
965
  const todo = emitLowConfidenceTodo(node, conf);
1107
966
  const props = p(node);
1108
- const name = props.name;
967
+ const name = emitIdentifier(props.name, 'transform', node);
968
+ // target and via are by-design raw code (escape hatches)
1109
969
  const target = props.target;
1110
970
  const via = props.via;
1111
971
  const constType = props.type;
1112
972
  const exp = exportPrefix(node);
1113
973
  const code = handlerCode(node);
1114
- const typeAnnotation = constType ? `: ${constType}` : '';
974
+ const typeAnnotation = constType ? `: ${emitTypeAnnotation(constType, 'unknown', node)}` : '';
1115
975
  if (code) {
1116
976
  // Handler block form — generate a function
1117
977
  const lines = [...todo, ...annotations];
@@ -1138,7 +998,7 @@ export function generateAction(node) {
1138
998
  const conf = p(node).confidence;
1139
999
  const todo = emitLowConfidenceTodo(node, conf);
1140
1000
  const props = p(node);
1141
- const name = props.name;
1001
+ const name = emitIdentifier(props.name, 'action', node);
1142
1002
  const idempotent = props.idempotent === 'true' || props.idempotent === true;
1143
1003
  const reversible = props.reversible === 'true' || props.reversible === true;
1144
1004
  const params = props.params || '';
@@ -1156,7 +1016,7 @@ export function generateAction(node) {
1156
1016
  lines.push(`/** @action ${metaParts.join(' ')} */`);
1157
1017
  }
1158
1018
  const paramList = params ? parseParamList(params) : '';
1159
- const retClause = returns ? `: Promise<${returns}>` : ': Promise<void>';
1019
+ const retClause = returns ? `: Promise<${emitTypeAnnotation(returns, 'void', node)}>` : ': Promise<void>';
1160
1020
  lines.push(`${exp}async function ${name}(${paramList})${retClause} {`);
1161
1021
  if (code) {
1162
1022
  for (const line of code.split('\n')) {
@@ -1257,7 +1117,7 @@ export function generateCollect(node) {
1257
1117
  const conf = p(node).confidence;
1258
1118
  const todo = emitLowConfidenceTodo(node, conf);
1259
1119
  const props = p(node);
1260
- const name = props.name;
1120
+ const name = emitIdentifier(props.name, 'collected', node);
1261
1121
  const from = props.from;
1262
1122
  const where = props.where;
1263
1123
  const limit = props.limit;
@@ -1314,7 +1174,7 @@ export function generateResolve(node) {
1314
1174
  const conf = p(node).confidence;
1315
1175
  const todo = emitLowConfidenceTodo(node, conf);
1316
1176
  const props = p(node);
1317
- const name = props.name;
1177
+ const name = emitIdentifier(props.name, 'resolver', node);
1318
1178
  const candidates = kids(node, 'candidate');
1319
1179
  const discriminator = firstChild(node, 'discriminator');
1320
1180
  if (!discriminator)
@@ -1328,7 +1188,7 @@ export function generateResolve(node) {
1328
1188
  lines.push(`const _${name}_candidates = [`);
1329
1189
  for (const c of candidates) {
1330
1190
  const cp = p(c);
1331
- const cname = cp.name;
1191
+ const cname = emitIdentifier(cp.name, 'candidate', c);
1332
1192
  const code = handlerCode(c);
1333
1193
  lines.push(` { name: '${cname}', fn: (signal: unknown) => { ${code.trim()} } },`);
1334
1194
  }
@@ -1392,7 +1252,7 @@ export function generateRecover(node) {
1392
1252
  const conf = p(node).confidence;
1393
1253
  const todo = emitLowConfidenceTodo(node, conf);
1394
1254
  const props = p(node);
1395
- const name = props.name;
1255
+ const name = emitIdentifier(props.name, 'recovery', node);
1396
1256
  const strategies = kids(node, 'strategy');
1397
1257
  const hasFallback = strategies.some(s => p(s).name === 'fallback');
1398
1258
  if (!hasFallback)
@@ -1402,7 +1262,7 @@ export function generateRecover(node) {
1402
1262
  lines.push(`async function ${name}WithRecovery<T>(fn: () => Promise<T>): Promise<T> {`);
1403
1263
  for (const strategy of strategies) {
1404
1264
  const sp = p(strategy);
1405
- const sname = sp.name;
1265
+ const sname = emitIdentifier(sp.name, 'strategy', strategy);
1406
1266
  const code = handlerCode(strategy);
1407
1267
  if (sname === 'retry') {
1408
1268
  const max = Number(sp.max) || 3;
@@ -1446,16 +1306,16 @@ export function generatePattern(node) {
1446
1306
  // pattern nodes are registered as templates — no direct output
1447
1307
  return [];
1448
1308
  }
1449
- export function generateApply(node) {
1309
+ export function generateApply(node, _depth = 0) {
1450
1310
  // apply nodes expand the referenced pattern
1451
1311
  const props = p(node);
1452
1312
  const patternName = props.pattern;
1453
1313
  if (!patternName)
1454
1314
  return [];
1455
- // Delegate to template expansion with the pattern name as node type
1315
+ // Delegate to template expansion propagate depth to prevent infinite recursion
1456
1316
  const syntheticNode = { ...node, type: patternName };
1457
1317
  if (isTemplateNode(patternName)) {
1458
- return expandTemplateNode(syntheticNode);
1318
+ return expandTemplateNode(syntheticNode, _depth + 1);
1459
1319
  }
1460
1320
  return [`// apply: pattern '${patternName}' not found`];
1461
1321
  }
@@ -1509,12 +1369,12 @@ export function generateSelect(node) {
1509
1369
  const lines = onChange ? [`{/* kern:use-client */}`] : [];
1510
1370
  lines.push(`<select ${attrs.join(' ')}>`);
1511
1371
  if (placeholder) {
1512
- lines.push(` <option value="" disabled>${placeholder}</option>`);
1372
+ lines.push(` <option value="" disabled>${emitTemplateSafe(placeholder)}</option>`);
1513
1373
  }
1514
1374
  for (const opt of kids(node, 'option')) {
1515
1375
  const op = p(opt);
1516
- const optValue = op.value || '';
1517
- const optLabel = op.label || optValue;
1376
+ const optValue = emitTemplateSafe(op.value || '');
1377
+ const optLabel = emitTemplateSafe(op.label || op.value || '');
1518
1378
  lines.push(` <option value="${optValue}">${optLabel}</option>`);
1519
1379
  }
1520
1380
  lines.push(`</select>`);
@@ -1527,7 +1387,7 @@ export function generateSelect(node) {
1527
1387
  // relation name=posts target=Post kind=one-to-many cascade=delete
1528
1388
  export function generateModel(node) {
1529
1389
  const props = p(node);
1530
- const name = props.name;
1390
+ const name = emitIdentifier(props.name, 'UnknownModel', node);
1531
1391
  const table = props.table;
1532
1392
  const exp = exportPrefix(node);
1533
1393
  const lines = [];
@@ -1535,14 +1395,14 @@ export function generateModel(node) {
1535
1395
  lines.push(`${exp}interface ${name} {`);
1536
1396
  for (const col of kids(node, 'column')) {
1537
1397
  const cp = p(col);
1538
- const colName = cp.name;
1398
+ const colName = emitIdentifier(cp.name, 'column', col);
1539
1399
  const colType = mapColumnType(cp.type);
1540
1400
  const opt = cp.optional === 'true' || cp.optional === true ? '?' : '';
1541
1401
  lines.push(` ${colName}${opt}: ${colType};`);
1542
1402
  }
1543
1403
  for (const rel of kids(node, 'relation')) {
1544
1404
  const rp = p(rel);
1545
- const relName = rp.name;
1405
+ const relName = emitIdentifier(rp.name, 'relation', rel);
1546
1406
  const target = rp.target;
1547
1407
  const kind = rp.kind || 'one-to-many';
1548
1408
  const relType = kind.includes('many') ? `${target}[]` : target;
@@ -1572,7 +1432,7 @@ function mapColumnType(kernType) {
1572
1432
  // handler <<<return this.findOne({ email });>>>
1573
1433
  export function generateRepository(node) {
1574
1434
  const props = p(node);
1575
- const name = props.name;
1435
+ const name = emitIdentifier(props.name, 'UnknownRepo', node);
1576
1436
  const model = props.model;
1577
1437
  const exp = exportPrefix(node);
1578
1438
  const lines = [];
@@ -1583,11 +1443,11 @@ export function generateRepository(node) {
1583
1443
  }
1584
1444
  for (const method of kids(node, 'method')) {
1585
1445
  const mp = p(method);
1586
- const mname = mp.name;
1446
+ const mname = emitIdentifier(mp.name, 'method', method);
1587
1447
  const mparams = mp.params ? parseParamList(mp.params) : '';
1588
1448
  const isAsync = mp.async === 'true' || mp.async === true;
1589
1449
  const asyncKw = isAsync ? 'async ' : '';
1590
- const mreturns = mp.returns ? `: ${mp.returns}` : '';
1450
+ const mreturns = mp.returns ? `: ${emitTypeAnnotation(mp.returns, 'unknown', method)}` : '';
1591
1451
  const mcode = handlerCode(method);
1592
1452
  lines.push(` ${asyncKw}${mname}(${mparams})${mreturns} {`);
1593
1453
  if (mcode) {
@@ -1608,7 +1468,7 @@ export function generateRepository(node) {
1608
1468
  // returns AuthService with=repo
1609
1469
  export function generateDependency(node) {
1610
1470
  const props = p(node);
1611
- const name = props.name;
1471
+ const name = emitIdentifier(props.name, 'unknownDep', node);
1612
1472
  const scope = props.scope || 'transient';
1613
1473
  const exp = exportPrefix(node);
1614
1474
  const lines = [];
@@ -1625,7 +1485,7 @@ export function generateDependency(node) {
1625
1485
  }
1626
1486
  for (const inj of injects) {
1627
1487
  const ip = p(inj);
1628
- const injName = ip.name;
1488
+ const injName = emitIdentifier(ip.name, 'dep', inj);
1629
1489
  const injType = ip.type;
1630
1490
  const injFrom = ip.from;
1631
1491
  const injWith = ip.with;
@@ -1660,7 +1520,7 @@ export function generateDependency(node) {
1660
1520
  // invalidate on=userUpdate tags="user:{id}"
1661
1521
  export function generateCache(node) {
1662
1522
  const props = p(node);
1663
- const name = props.name;
1523
+ const name = emitIdentifier(props.name, 'unknownCache', node);
1664
1524
  const backend = props.backend || 'memory';
1665
1525
  const prefix = props.prefix || '';
1666
1526
  const ttl = props.ttl;
@@ -1675,7 +1535,7 @@ export function generateCache(node) {
1675
1535
  // Entry methods
1676
1536
  for (const entry of kids(node, 'entry')) {
1677
1537
  const ep = p(entry);
1678
- const entryName = ep.name;
1538
+ const entryName = emitIdentifier(ep.name, 'entry', entry);
1679
1539
  const key = ep.key || entryName;
1680
1540
  const strategyNode = firstChild(entry, 'strategy');
1681
1541
  const strategy = strategyNode ? (p(strategyNode).name || 'cache-aside') : 'cache-aside';
@@ -1744,7 +1604,8 @@ export function isCoreNode(type) {
1744
1604
  return CORE_NODE_TYPES.has(type);
1745
1605
  }
1746
1606
  /** Generate TypeScript for any core language node. */
1747
- export function generateCoreNode(node, target) {
1607
+ export function generateCoreNode(node, target, runtime) {
1608
+ const rt = runtime ?? defaultRuntime;
1748
1609
  switch (node.type) {
1749
1610
  case 'type': return generateType(node);
1750
1611
  case 'interface': return generateInterface(node);
@@ -1812,14 +1673,14 @@ export function generateCoreNode(node, target) {
1812
1673
  case 'option': return [];
1813
1674
  default: {
1814
1675
  // Check evolved generators (v4) — target-specific first, then default
1815
- const targetMap = target ? _evolvedTargetGenerators.get(node.type) : undefined;
1676
+ const targetMap = target ? rt.evolvedTargetGenerators.get(node.type) : undefined;
1816
1677
  const targetGen = targetMap && target ? targetMap.get(target) : undefined;
1817
- const evolvedGen = targetGen || _evolvedGenerators.get(node.type);
1678
+ const evolvedGen = targetGen || rt.evolvedGenerators.get(node.type);
1818
1679
  if (evolvedGen)
1819
1680
  return evolvedGen(node);
1820
1681
  // Check if this is a template instance
1821
- if (isTemplateNode(node.type))
1822
- return expandTemplateNode(node);
1682
+ if (isTemplateNode(node.type, rt))
1683
+ return expandTemplateNode(node, 0, rt);
1823
1684
  return [];
1824
1685
  }
1825
1686
  }