@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.
- package/dist/codegen/emitters.d.ts +25 -0
- package/dist/codegen/emitters.js +124 -0
- package/dist/codegen/emitters.js.map +1 -0
- package/dist/codegen/helpers.d.ts +22 -0
- package/dist/codegen/helpers.js +159 -0
- package/dist/codegen/helpers.js.map +1 -0
- package/dist/codegen-core.d.ts +5 -28
- package/dist/codegen-core.js +166 -305
- 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/errors.d.ts +2 -0
- package/dist/errors.js +1 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +8 -4
- package/dist/index.js +12 -10
- package/dist/index.js.map +1 -1
- package/dist/parser.d.ts +15 -10
- package/dist/parser.js +234 -57
- package/dist/parser.js.map +1 -1
- package/dist/runtime.d.ts +57 -0
- package/dist/runtime.js +121 -0
- package/dist/runtime.js.map +1 -0
- package/dist/schema.d.ts +39 -0
- package/dist/schema.js +481 -0
- package/dist/schema.js.map +1 -0
- package/dist/spec.d.ts +5 -4
- package/dist/spec.js +30 -19
- package/dist/spec.js.map +1 -1
- package/dist/styles-tailwind.js +29 -0
- package/dist/styles-tailwind.js.map +1 -1
- package/dist/template-engine.d.ts +3 -2
- package/dist/template-engine.js +24 -18
- package/dist/template-engine.js.map +1 -1
- package/dist/types.d.ts +15 -0
- package/dist/utils.js +7 -1
- package/dist/utils.js.map +1 -1
- package/package.json +30 -1
package/dist/codegen-core.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
46
|
+
return defaultRuntime.hasEvolvedGenerator(type);
|
|
40
47
|
}
|
|
41
48
|
// ── Shared IR node helpers ───────────────────────────────────────────────
|
|
42
|
-
//
|
|
43
|
-
|
|
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(` ${
|
|
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
|
|
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}${
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(` ${
|
|
312
|
+
lines.push(` ${fName}${opt}: ${fType},`);
|
|
349
313
|
}
|
|
350
314
|
else {
|
|
351
|
-
lines.push(` public readonly ${
|
|
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(` ${
|
|
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
|
|
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 =
|
|
526
|
+
def = emitStringLiteral(def);
|
|
561
527
|
}
|
|
562
|
-
lines.push(` ${
|
|
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
|
|
574
|
-
const key = props.key
|
|
575
|
-
const model = props.model
|
|
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
|
-
|
|
580
|
-
|
|
581
|
-
|
|
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(`
|
|
612
|
-
lines.push(`
|
|
613
|
-
lines.push(`
|
|
614
|
-
lines.push(`
|
|
615
|
-
lines.push(`
|
|
616
|
-
lines.push(` }
|
|
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
|
|
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
|
|
863
|
-
const
|
|
864
|
-
const
|
|
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
|
|
838
|
+
const safeDefault = ep.default ? emitIdentifier(ep.default, 'default', exp) : '';
|
|
867
839
|
// export * from './foo.js'
|
|
868
|
-
if (
|
|
869
|
-
lines.push(`export * 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 (
|
|
873
|
-
lines.push(`export { ${
|
|
844
|
+
if (safeFrom && safeNames) {
|
|
845
|
+
lines.push(`export { ${safeNames} } from '${safeFrom}';`);
|
|
874
846
|
}
|
|
875
847
|
// export type { A, B } from './types.js'
|
|
876
|
-
if (
|
|
877
|
-
lines.push(`export type { ${
|
|
848
|
+
if (safeFrom && safeTypeNames) {
|
|
849
|
+
lines.push(`export type { ${safeTypeNames} } from '${safeFrom}';`);
|
|
878
850
|
}
|
|
879
851
|
// export default foo
|
|
880
|
-
if (
|
|
881
|
-
lines.push(`export default ${
|
|
852
|
+
if (safeDefault && !safeFrom) {
|
|
853
|
+
lines.push(`export default ${safeDefault};`);
|
|
882
854
|
}
|
|
883
855
|
// export default from './foo.js' (re-export default)
|
|
884
|
-
if (
|
|
885
|
-
lines.push(`export { default as ${
|
|
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 (!
|
|
889
|
-
lines.push(`export { ${
|
|
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 (!
|
|
893
|
-
lines.push(`export type { ${
|
|
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 (
|
|
930
|
-
return [`import ${typeKw}${
|
|
903
|
+
if (safeDefault && namedList) {
|
|
904
|
+
return [`import ${typeKw}${safeDefault}, { ${namedList} } from '${safePath}';`];
|
|
931
905
|
}
|
|
932
|
-
if (
|
|
933
|
-
return [`import ${typeKw}${
|
|
906
|
+
if (safeDefault) {
|
|
907
|
+
return [`import ${typeKw}${safeDefault} from '${safePath}';`];
|
|
934
908
|
}
|
|
935
909
|
if (namedList) {
|
|
936
|
-
return [`import ${typeKw}{ ${namedList} } from '${
|
|
910
|
+
return [`import ${typeKw}{ ${namedList} } from '${safePath}';`];
|
|
937
911
|
}
|
|
938
912
|
// Side-effect import
|
|
939
|
-
return [`import '${
|
|
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
|
|
967
|
-
|
|
968
|
-
|
|
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
|
|
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 ||
|
|
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 ?
|
|
1676
|
+
const targetMap = target ? rt.evolvedTargetGenerators.get(node.type) : undefined;
|
|
1816
1677
|
const targetGen = targetMap && target ? targetMap.get(target) : undefined;
|
|
1817
|
-
const evolvedGen = targetGen ||
|
|
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
|
}
|