@kernlang/core 3.1.3 → 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,135 +8,49 @@
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
+ 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)
55
24
  // ── Evolved Generators (v4) ─────────────────────────────────────────────
56
25
  // Populated at startup by evolved-node-loader. Checked in generateCoreNode
57
26
  // before the default case, allowing graduated nodes to produce output.
58
- const _evolvedGenerators = new Map();
59
- const _evolvedTargetGenerators = new Map();
27
+ // Evolved generators now live in defaultRuntime. These functions delegate for backward compatibility.
60
28
  /** Register an evolved generator (called at startup). */
61
29
  export function registerEvolvedGenerator(keyword, fn) {
62
- _evolvedGenerators.set(keyword, fn);
30
+ defaultRuntime.registerEvolvedGenerator(keyword, fn);
63
31
  }
64
32
  /** Register a target-specific evolved generator (called at startup). */
65
33
  export function registerEvolvedTargetGenerator(keyword, target, fn) {
66
- if (!_evolvedTargetGenerators.has(keyword)) {
67
- _evolvedTargetGenerators.set(keyword, new Map());
68
- }
69
- _evolvedTargetGenerators.get(keyword).set(target, fn);
34
+ defaultRuntime.registerEvolvedTargetGenerator(keyword, target, fn);
70
35
  }
71
36
  /** Unregister an evolved generator (for rollback/testing). */
72
37
  export function unregisterEvolvedGenerator(keyword) {
73
- _evolvedGenerators.delete(keyword);
74
- _evolvedTargetGenerators.delete(keyword);
38
+ defaultRuntime.unregisterEvolvedGenerator(keyword);
75
39
  }
76
40
  /** Clear all evolved generators (for test isolation). */
77
41
  export function clearEvolvedGenerators() {
78
- _evolvedGenerators.clear();
79
- _evolvedTargetGenerators.clear();
42
+ defaultRuntime.clearEvolvedGenerators();
80
43
  }
81
44
  /** Check if an evolved generator exists for a type. */
82
45
  export function hasEvolvedGenerator(type) {
83
- return _evolvedGenerators.has(type);
46
+ return defaultRuntime.hasEvolvedGenerator(type);
84
47
  }
85
48
  // ── Shared IR node helpers ───────────────────────────────────────────────
86
- // These are used by every transpiler. Exported for reuse.
87
- /** Extract props from an IR node. */
88
- export function getProps(node) {
89
- return node.props || {};
90
- }
91
- /** Get children, optionally filtered by type. */
92
- export function getChildren(node, type) {
93
- const c = node.children || [];
94
- return type ? c.filter(n => n.type === type) : c;
95
- }
96
- /** Get first child of a given type. */
97
- export function getFirstChild(node, type) {
98
- return getChildren(node, type)[0];
99
- }
100
- /** Extract styles from node props. */
101
- export function getStyles(node) {
102
- return getProps(node).styles || {};
103
- }
104
- /** Extract pseudo-styles from node props. */
105
- export function getPseudoStyles(node) {
106
- return getProps(node).pseudoStyles || {};
107
- }
108
- /** Extract theme refs from node props. */
109
- export function getThemeRefs(node) {
110
- return getProps(node).themeRefs || [];
111
- }
112
- /** Strip common leading whitespace from multiline handler code. */
113
- export function dedent(code) {
114
- const lines = code.split('\n');
115
- const nonEmpty = lines.filter(l => l.trim().length > 0);
116
- if (nonEmpty.length === 0)
117
- return code;
118
- const min = Math.min(...nonEmpty.map(l => l.match(/^(\s*)/)?.[1].length ?? 0));
119
- return lines.map(l => l.slice(min)).join('\n');
120
- }
121
- /** Convert camelCase to kebab-case for CSS property names. */
122
- export function cssPropertyName(camel) {
123
- return camel.replace(/([A-Z])/g, '-$1').toLowerCase();
124
- }
125
- /** Extract handler code from a node (finds handler child, dedents). */
126
- export function handlerCode(node) {
127
- const handler = getFirstChild(node, 'handler');
128
- if (!handler)
129
- return '';
130
- const raw = getProps(handler).code || '';
131
- return dedent(raw);
132
- }
133
- // 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
134
51
  const p = getProps;
135
52
  const kids = getChildren;
136
53
  const firstChild = getFirstChild;
137
- export function exportPrefix(node) {
138
- return p(node).export === 'false' ? '' : 'export ';
139
- }
140
54
  // ── Type Alias ───────────────────────────────────────────────────────────
141
55
  // type name=PlanState values="draft|approved|running|paused|completed|failed|cancelled"
142
56
  // → export type PlanState = 'draft' | 'approved' | 'running' | ...;
@@ -149,7 +63,7 @@ export function generateType(node) {
149
63
  return [`${exp}type ${name} = ${members};`];
150
64
  }
151
65
  if (alias) {
152
- return [`${exp}type ${name} = ${alias};`];
66
+ return [`${exp}type ${name} = ${emitTypeAnnotation(alias, 'unknown', node)};`];
153
67
  }
154
68
  return [`${exp}type ${name} = unknown;`];
155
69
  }
@@ -162,7 +76,7 @@ export function generateType(node) {
162
76
  export function generateInterface(node) {
163
77
  const props = p(node);
164
78
  const name = emitIdentifier(props.name, 'UnknownInterface', node);
165
- const ext = props.extends ? ` extends ${props.extends}` : '';
79
+ const ext = props.extends ? ` extends ${emitTypeAnnotation(props.extends, 'unknown', node)}` : '';
166
80
  const exp = exportPrefix(node);
167
81
  const lines = [];
168
82
  lines.push(`${exp}interface ${name}${ext} {`);
@@ -170,7 +84,7 @@ export function generateInterface(node) {
170
84
  const fp = p(field);
171
85
  const fieldName = emitIdentifier(fp.name, 'field', field);
172
86
  const opt = fp.optional === 'true' || fp.optional === true ? '?' : '';
173
- lines.push(` ${fieldName}${opt}: ${fp.type};`);
87
+ lines.push(` ${fieldName}${opt}: ${emitTypeAnnotation(fp.type, 'unknown', field)};`);
174
88
  }
175
89
  lines.push('}');
176
90
  return lines;
@@ -203,7 +117,7 @@ export function generateUnion(node) {
203
117
  for (const field of fields) {
204
118
  const fp = p(field);
205
119
  const opt = fp.optional === 'true' || fp.optional === true ? '?' : '';
206
- fieldParts.push(`${fp.name}${opt}: ${fp.type}`);
120
+ fieldParts.push(`${emitIdentifier(fp.name, 'field', field)}${opt}: ${emitTypeAnnotation(fp.type, 'unknown', field)}`);
207
121
  }
208
122
  const semi = i === variants.length - 1 ? ';' : '';
209
123
  lines.push(` | { ${fieldParts.join('; ')} }${semi}`);
@@ -230,17 +144,19 @@ export function generateService(node) {
230
144
  const impl = props.implements;
231
145
  const exp = exportPrefix(node);
232
146
  const lines = [];
233
- const implClause = impl ? ` implements ${impl}` : '';
147
+ const implClause = impl ? ` implements ${emitTypeAnnotation(impl, 'unknown', node)}` : '';
234
148
  lines.push(`${exp}class ${name}${implClause} {`);
235
149
  // Fields
236
150
  for (const field of kids(node, 'field')) {
237
151
  const fp = p(field);
152
+ const fieldName = emitIdentifier(fp.name, 'field', field);
238
153
  const vis = fp.private === 'true' || fp.private === true ? 'private ' : '';
239
154
  const readonly = fp.readonly === 'true' || fp.readonly === true ? 'readonly ' : '';
240
- const typeAnnotation = fp.type ? `: ${fp.type}` : '';
155
+ const typeAnnotation = fp.type ? `: ${emitTypeAnnotation(fp.type, 'unknown', field)}` : '';
241
156
  const defaultVal = fp.default;
157
+ // default values are by-design raw code (escape hatch) — documented, not sanitized
242
158
  const init = defaultVal !== undefined ? ` = ${defaultVal}` : '';
243
- lines.push(` ${vis}${readonly}${fp.name}${typeAnnotation}${init};`);
159
+ lines.push(` ${vis}${readonly}${fieldName}${typeAnnotation}${init};`);
244
160
  }
245
161
  // Constructor (if any constructor child exists)
246
162
  const ctorNode = firstChild(node, 'constructor');
@@ -272,8 +188,8 @@ export function generateService(node) {
272
188
  const mcode = handlerCode(method);
273
189
  // stream=true → AsyncGenerator return type
274
190
  const mreturns = isStream
275
- ? `: AsyncGenerator<${mp.returns || 'unknown'}>`
276
- : mp.returns ? `: ${mp.returns}` : '';
191
+ ? `: AsyncGenerator<${emitTypeAnnotation(mp.returns, 'unknown', method)}>`
192
+ : mp.returns ? `: ${emitTypeAnnotation(mp.returns, 'unknown', method)}` : '';
277
193
  lines.push('');
278
194
  lines.push(` ${vis}${staticKw}${asyncKw}${star}${mname}(${mparams})${mreturns} {`);
279
195
  if (mcode) {
@@ -313,7 +229,7 @@ export function generateFunction(node) {
313
229
  const paramList = params ? parseParamList(params) : '';
314
230
  // stream=true → async generator function
315
231
  if (isStream) {
316
- const yieldType = returns || 'unknown';
232
+ const yieldType = emitTypeAnnotation(returns, 'unknown', node);
317
233
  const retClause = `: AsyncGenerator<${yieldType}>`;
318
234
  const code = handlerCode(node);
319
235
  lines.push(`${exp}async function* ${name}(${paramList})${retClause} {`);
@@ -325,7 +241,7 @@ export function generateFunction(node) {
325
241
  lines.push('}');
326
242
  return lines;
327
243
  }
328
- const retClause = returns ? `: ${returns}` : '';
244
+ const retClause = returns ? `: ${emitTypeAnnotation(returns, 'unknown', node)}` : '';
329
245
  const asyncKw = isAsync ? 'async ' : '';
330
246
  const code = handlerCode(node);
331
247
  // Gap 3: signal + cleanup support for async functions
@@ -390,11 +306,13 @@ export function generateError(node) {
390
306
  const opt = fp.optional === 'true' || fp.optional === true ? '?' : '';
391
307
  const isMessage = fp.name === 'message';
392
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);
393
311
  if (isMessage) {
394
- lines.push(` ${fp.name}${opt}: ${fp.type},`);
312
+ lines.push(` ${fName}${opt}: ${fType},`);
395
313
  }
396
314
  else {
397
- lines.push(` public readonly ${fp.name}${opt}: ${fp.type},`);
315
+ lines.push(` public readonly ${fName}${opt}: ${fType},`);
398
316
  }
399
317
  }
400
318
  lines.push(` ) {`);
@@ -583,7 +501,7 @@ export function generateConfig(node) {
583
501
  const fp = p(field);
584
502
  const fieldName = emitIdentifier(fp.name, 'field', field);
585
503
  const opt = fp.default !== undefined ? '?' : '';
586
- lines.push(` ${fieldName}${opt}: ${fp.type};`);
504
+ lines.push(` ${fieldName}${opt}: ${emitTypeAnnotation(fp.type, 'unknown', field)};`);
587
505
  }
588
506
  lines.push('}');
589
507
  lines.push('');
@@ -592,7 +510,7 @@ export function generateConfig(node) {
592
510
  for (const field of fields) {
593
511
  const fp = p(field);
594
512
  const fieldName = emitIdentifier(fp.name, 'field', field);
595
- const ftype = fp.type;
513
+ const ftype = emitTypeAnnotation(fp.type, 'unknown', field);
596
514
  let def = fp.default;
597
515
  if (def === undefined) {
598
516
  if (ftype === 'number')
@@ -749,7 +667,7 @@ export function generateEvent(node) {
749
667
  for (const t of types) {
750
668
  const tp = p(t);
751
669
  const tname = emitTemplateSafe((tp.name || tp.value));
752
- const data = tp.data || 'Record<string, unknown>';
670
+ const data = emitTypeAnnotation(tp.data, 'Record<string, unknown>', t);
753
671
  lines.push(` '${tname}': ${data};`);
754
672
  }
755
673
  lines.push('}');
@@ -910,38 +828,41 @@ export function generateModule(node) {
910
828
  lines.push('');
911
829
  for (const exp of kids(node, 'export')) {
912
830
  const ep = p(exp);
913
- const from = ep.from;
914
- const names = ep.names;
915
- 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(', ') : '';
916
837
  const star = ep.star === 'true' || ep.star === true;
917
- const defaultExport = ep.default;
838
+ const safeDefault = ep.default ? emitIdentifier(ep.default, 'default', exp) : '';
918
839
  // export * from './foo.js'
919
- if (from && !names && !typeNames && star) {
920
- lines.push(`export * from '${from}';`);
840
+ if (safeFrom && !safeNames && !safeTypeNames && star) {
841
+ lines.push(`export * from '${safeFrom}';`);
921
842
  }
922
843
  // export { a, b } from './foo.js'
923
- if (from && names) {
924
- lines.push(`export { ${names.split(',').map(s => s.trim()).join(', ')} } from '${from}';`);
844
+ if (safeFrom && safeNames) {
845
+ lines.push(`export { ${safeNames} } from '${safeFrom}';`);
925
846
  }
926
847
  // export type { A, B } from './types.js'
927
- if (from && typeNames) {
928
- 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}';`);
929
850
  }
930
851
  // export default foo
931
- if (defaultExport && !from) {
932
- lines.push(`export default ${defaultExport};`);
852
+ if (safeDefault && !safeFrom) {
853
+ lines.push(`export default ${safeDefault};`);
933
854
  }
934
855
  // export default from './foo.js' (re-export default)
935
- if (defaultExport && from) {
936
- lines.push(`export { default as ${defaultExport} } from '${from}';`);
856
+ if (safeDefault && safeFrom) {
857
+ lines.push(`export { default as ${safeDefault} } from '${safeFrom}';`);
937
858
  }
938
859
  // export { a, b } (no from — local re-export)
939
- if (!from && names && !defaultExport) {
940
- lines.push(`export { ${names.split(',').map(s => s.trim()).join(', ')} };`);
860
+ if (!safeFrom && safeNames && !safeDefault) {
861
+ lines.push(`export { ${safeNames} };`);
941
862
  }
942
863
  // export type { A, B } (no from — local type re-export)
943
- if (!from && typeNames && !defaultExport) {
944
- lines.push(`export type { ${typeNames.split(',').map(s => s.trim()).join(', ')} };`);
864
+ if (!safeFrom && safeTypeNames && !safeDefault) {
865
+ lines.push(`export type { ${safeTypeNames} };`);
945
866
  }
946
867
  }
947
868
  // Inline child definitions
@@ -973,21 +894,23 @@ export function generateImport(node) {
973
894
  const isTypeOnly = props.types === 'true' || props.types === true;
974
895
  if (!from)
975
896
  return [];
897
+ const safePath = emitImportSpecifier(from, node);
976
898
  const typeKw = isTypeOnly ? 'type ' : '';
899
+ const safeDefault = defaultImport ? emitIdentifier(defaultImport, 'default', node) : '';
977
900
  const namedList = names
978
- ? names.split(',').map(s => s.trim()).join(', ')
901
+ ? names.split(',').map(s => emitIdentifier(s.trim(), 'import', node)).join(', ')
979
902
  : '';
980
- if (defaultImport && namedList) {
981
- return [`import ${typeKw}${defaultImport}, { ${namedList} } from '${from}';`];
903
+ if (safeDefault && namedList) {
904
+ return [`import ${typeKw}${safeDefault}, { ${namedList} } from '${safePath}';`];
982
905
  }
983
- if (defaultImport) {
984
- return [`import ${typeKw}${defaultImport} from '${from}';`];
906
+ if (safeDefault) {
907
+ return [`import ${typeKw}${safeDefault} from '${safePath}';`];
985
908
  }
986
909
  if (namedList) {
987
- return [`import ${typeKw}{ ${namedList} } from '${from}';`];
910
+ return [`import ${typeKw}{ ${namedList} } from '${safePath}';`];
988
911
  }
989
912
  // Side-effect import
990
- return [`import '${from}';`];
913
+ return [`import '${safePath}';`];
991
914
  }
992
915
  // ── Const ───────────────────────────────────────────────────────────────
993
916
  // const name=AGON_HOME type=string
@@ -1005,7 +928,7 @@ export function generateConst(node) {
1005
928
  const value = props.value;
1006
929
  const exp = exportPrefix(node);
1007
930
  const code = handlerCode(node);
1008
- const typeAnnotation = constType ? `: ${constType}` : '';
931
+ const typeAnnotation = constType ? `: ${emitTypeAnnotation(constType, 'unknown', node)}` : '';
1009
932
  if (code) {
1010
933
  return [`${exp}const ${name}${typeAnnotation} = ${code.trim()};`];
1011
934
  }
@@ -1014,125 +937,9 @@ export function generateConst(node) {
1014
937
  }
1015
938
  return [`${exp}const ${name}${typeAnnotation};`];
1016
939
  }
1017
- // ── Shared Helpers (exported for @kernlang/react) ────────────────────────────
1018
- /** Parse "name:Type,name2:Type2,spread:number=8" → "name: Type, name2: Type2, spread: number = 8"
1019
- * Supports default values via = after the type. */
1020
- export function parseParamList(params) {
1021
- if (!params)
1022
- return '';
1023
- return splitParamsRespectingDepth(params).map(s => {
1024
- const trimmed = s.trim();
1025
- // Split name from type:default — find the first ':'
1026
- const colonIdx = trimmed.indexOf(':');
1027
- if (colonIdx === -1)
1028
- return trimmed;
1029
- const pname = trimmed.slice(0, colonIdx).trim();
1030
- const rest = trimmed.slice(colonIdx + 1).trim();
1031
- // Split type from default value — find '=' not inside angle brackets or parens
1032
- const eqIdx = findDefaultSeparator(rest);
1033
- if (eqIdx === -1) {
1034
- return `${pname}: ${rest}`;
1035
- }
1036
- const ptype = rest.slice(0, eqIdx).trim();
1037
- const pdefault = rest.slice(eqIdx + 1).trim();
1038
- return `${pname}: ${ptype} = ${pdefault}`;
1039
- }).join(', ');
1040
- }
1041
- /** Split param string on commas while respecting <>, (), {} depth.
1042
- * Handles => (arrow) without decrementing depth. */
1043
- function splitParamsRespectingDepth(s) {
1044
- const parts = [];
1045
- let depth = 0;
1046
- let current = '';
1047
- for (let i = 0; i < s.length; i++) {
1048
- const ch = s[i];
1049
- if (ch === '<' || ch === '(' || ch === '{')
1050
- depth++;
1051
- else if ((ch === '>' || ch === ')' || ch === '}') && depth > 0)
1052
- depth--;
1053
- if (ch === ',' && depth === 0) {
1054
- parts.push(current);
1055
- current = '';
1056
- }
1057
- else {
1058
- current += ch;
1059
- }
1060
- }
1061
- if (current.trim())
1062
- parts.push(current);
1063
- return parts;
1064
- }
1065
- /** Find the index of '=' that separates type from default value,
1066
- * skipping '=' inside arrow functions (=>), generics, or parens. */
1067
- function findDefaultSeparator(rest) {
1068
- let depth = 0;
1069
- for (let i = 0; i < rest.length; i++) {
1070
- const ch = rest[i];
1071
- if (ch === '<' || ch === '(' || ch === '{')
1072
- depth++;
1073
- else if (ch === '>' || ch === ')' || ch === '}')
1074
- depth--;
1075
- else if (ch === '=' && depth === 0) {
1076
- // Skip '=>' (arrow function in type)
1077
- if (rest[i + 1] === '>')
1078
- continue;
1079
- return i;
1080
- }
1081
- }
1082
- return -1;
1083
- }
1084
- export function capitalize(s) {
1085
- return s.charAt(0).toUpperCase() + s.slice(1);
1086
- }
1087
- // Hook codegen moved to @kernlang/react (generateHook in codegen-react.ts)
1088
- // ── Reason & Confidence Annotations ──────────────────────────────────────
1089
- export function emitReasonAnnotations(node) {
1090
- const reasonNode = firstChild(node, 'reason');
1091
- const evidenceNode = firstChild(node, 'evidence');
1092
- const needsNodes = kids(node, 'needs');
1093
- const confidence = p(node).confidence;
1094
- if (!reasonNode && !evidenceNode && !confidence && needsNodes.length === 0)
1095
- return [];
1096
- const lines = ['/**'];
1097
- if (confidence)
1098
- lines.push(` * @confidence ${confidence}`);
1099
- if (reasonNode) {
1100
- const rp = p(reasonNode);
1101
- lines.push(` * @reason ${rp.because || ''}`);
1102
- if (rp.basis)
1103
- lines.push(` * @basis ${rp.basis}`);
1104
- if (rp.survives)
1105
- lines.push(` * @survives ${rp.survives}`);
1106
- }
1107
- if (evidenceNode) {
1108
- const ep = p(evidenceNode);
1109
- const parts = [`source=${ep.source}`];
1110
- if (ep.method)
1111
- parts.push(`method=${ep.method}`);
1112
- if (ep.authority)
1113
- parts.push(`authority=${ep.authority}`);
1114
- lines.push(` * @evidence ${parts.join(', ')}`);
1115
- }
1116
- for (const needsNode of needsNodes) {
1117
- const np = p(needsNode);
1118
- const desc = np.what || np.description || '';
1119
- const wouldRaise = np['would-raise-to'];
1120
- const tag = wouldRaise ? `${desc} (would raise to ${wouldRaise})` : desc;
1121
- lines.push(` * @needs ${tag}`);
1122
- }
1123
- lines.push(' */');
1124
- return lines;
1125
- }
1126
- /** Emit a TODO comment for nodes with low literal confidence (< 0.5). */
1127
- export function emitLowConfidenceTodo(node, confidence) {
1128
- if (!confidence)
1129
- return [];
1130
- const val = parseFloat(confidence);
1131
- if (isNaN(val) || val >= 0.5 || confidence.includes(':'))
1132
- return [];
1133
- const name = p(node).name || node.type;
1134
- return [`// TODO(low-confidence): ${name} confidence=${confidence}`];
1135
- }
940
+ // ── Shared Helpers ───────────────────────────────────────────────────────
941
+ // parseParamList, capitalize, emitReasonAnnotations, emitLowConfidenceTodo
942
+ // implementations extracted to codegen/helpers.ts. Re-exported at top of file.
1136
943
  // ── Ground Layer: derive ─────────────────────────────────────────────────
1137
944
  // derive name=loudness expr={{average(stems)}} type=number deps="stems"
1138
945
  // → export const loudness: number = average(stems);
@@ -1142,10 +949,11 @@ export function generateDerive(node) {
1142
949
  const todo = emitLowConfidenceTodo(node, conf);
1143
950
  const props = p(node);
1144
951
  const name = emitIdentifier(props.name, 'derived', node);
952
+ // expr is by-design raw code (escape hatch)
1145
953
  const expr = props.expr;
1146
954
  const constType = props.type;
1147
955
  const exp = exportPrefix(node);
1148
- const typeAnnotation = constType ? `: ${constType}` : '';
956
+ const typeAnnotation = constType ? `: ${emitTypeAnnotation(constType, 'unknown', node)}` : '';
1149
957
  return [...todo, ...annotations, `${exp}const ${name}${typeAnnotation} = ${expr};`];
1150
958
  }
1151
959
  // ── Ground Layer: transform ──────────────────────────────────────────────
@@ -1157,12 +965,13 @@ export function generateTransform(node) {
1157
965
  const todo = emitLowConfidenceTodo(node, conf);
1158
966
  const props = p(node);
1159
967
  const name = emitIdentifier(props.name, 'transform', node);
968
+ // target and via are by-design raw code (escape hatches)
1160
969
  const target = props.target;
1161
970
  const via = props.via;
1162
971
  const constType = props.type;
1163
972
  const exp = exportPrefix(node);
1164
973
  const code = handlerCode(node);
1165
- const typeAnnotation = constType ? `: ${constType}` : '';
974
+ const typeAnnotation = constType ? `: ${emitTypeAnnotation(constType, 'unknown', node)}` : '';
1166
975
  if (code) {
1167
976
  // Handler block form — generate a function
1168
977
  const lines = [...todo, ...annotations];
@@ -1207,7 +1016,7 @@ export function generateAction(node) {
1207
1016
  lines.push(`/** @action ${metaParts.join(' ')} */`);
1208
1017
  }
1209
1018
  const paramList = params ? parseParamList(params) : '';
1210
- const retClause = returns ? `: Promise<${returns}>` : ': Promise<void>';
1019
+ const retClause = returns ? `: Promise<${emitTypeAnnotation(returns, 'void', node)}>` : ': Promise<void>';
1211
1020
  lines.push(`${exp}async function ${name}(${paramList})${retClause} {`);
1212
1021
  if (code) {
1213
1022
  for (const line of code.split('\n')) {
@@ -1638,7 +1447,7 @@ export function generateRepository(node) {
1638
1447
  const mparams = mp.params ? parseParamList(mp.params) : '';
1639
1448
  const isAsync = mp.async === 'true' || mp.async === true;
1640
1449
  const asyncKw = isAsync ? 'async ' : '';
1641
- const mreturns = mp.returns ? `: ${mp.returns}` : '';
1450
+ const mreturns = mp.returns ? `: ${emitTypeAnnotation(mp.returns, 'unknown', method)}` : '';
1642
1451
  const mcode = handlerCode(method);
1643
1452
  lines.push(` ${asyncKw}${mname}(${mparams})${mreturns} {`);
1644
1453
  if (mcode) {
@@ -1795,7 +1604,8 @@ export function isCoreNode(type) {
1795
1604
  return CORE_NODE_TYPES.has(type);
1796
1605
  }
1797
1606
  /** Generate TypeScript for any core language node. */
1798
- export function generateCoreNode(node, target) {
1607
+ export function generateCoreNode(node, target, runtime) {
1608
+ const rt = runtime ?? defaultRuntime;
1799
1609
  switch (node.type) {
1800
1610
  case 'type': return generateType(node);
1801
1611
  case 'interface': return generateInterface(node);
@@ -1863,14 +1673,14 @@ export function generateCoreNode(node, target) {
1863
1673
  case 'option': return [];
1864
1674
  default: {
1865
1675
  // Check evolved generators (v4) — target-specific first, then default
1866
- const targetMap = target ? _evolvedTargetGenerators.get(node.type) : undefined;
1676
+ const targetMap = target ? rt.evolvedTargetGenerators.get(node.type) : undefined;
1867
1677
  const targetGen = targetMap && target ? targetMap.get(target) : undefined;
1868
- const evolvedGen = targetGen || _evolvedGenerators.get(node.type);
1678
+ const evolvedGen = targetGen || rt.evolvedGenerators.get(node.type);
1869
1679
  if (evolvedGen)
1870
1680
  return evolvedGen(node);
1871
1681
  // Check if this is a template instance
1872
- if (isTemplateNode(node.type))
1873
- return expandTemplateNode(node);
1682
+ if (isTemplateNode(node.type, rt))
1683
+ return expandTemplateNode(node, 0, rt);
1874
1684
  return [];
1875
1685
  }
1876
1686
  }