@kernlang/core 3.1.5 → 3.1.6

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.
Files changed (93) hide show
  1. package/LICENSE +17 -0
  2. package/README.md +5 -2
  3. package/dist/codegen/data-layer.d.ts +12 -0
  4. package/dist/codegen/data-layer.js +292 -0
  5. package/dist/codegen/data-layer.js.map +1 -0
  6. package/dist/codegen/events.d.ts +9 -0
  7. package/dist/codegen/events.js +158 -0
  8. package/dist/codegen/events.js.map +1 -0
  9. package/dist/codegen/functions.d.ts +8 -0
  10. package/dist/codegen/functions.js +147 -0
  11. package/dist/codegen/functions.js.map +1 -0
  12. package/dist/codegen/ground-layer.d.ts +22 -0
  13. package/dist/codegen/ground-layer.js +317 -0
  14. package/dist/codegen/ground-layer.js.map +1 -0
  15. package/dist/codegen/machines.d.ts +9 -0
  16. package/dist/codegen/machines.js +127 -0
  17. package/dist/codegen/machines.js.map +1 -0
  18. package/dist/codegen/modules.d.ts +10 -0
  19. package/dist/codegen/modules.js +40 -0
  20. package/dist/codegen/modules.js.map +1 -0
  21. package/dist/codegen/semantic-types.d.ts +14 -0
  22. package/dist/codegen/semantic-types.js +31 -0
  23. package/dist/codegen/semantic-types.js.map +1 -0
  24. package/dist/codegen/test-gen.d.ts +7 -0
  25. package/dist/codegen/test-gen.js +56 -0
  26. package/dist/codegen/test-gen.js.map +1 -0
  27. package/dist/codegen/type-system.d.ts +11 -0
  28. package/dist/codegen/type-system.js +162 -0
  29. package/dist/codegen/type-system.js.map +1 -0
  30. package/dist/codegen-core.d.ts +26 -33
  31. package/dist/codegen-core.js +58 -1367
  32. package/dist/codegen-core.js.map +1 -1
  33. package/dist/config.d.ts +20 -1
  34. package/dist/config.js +23 -3
  35. package/dist/config.js.map +1 -1
  36. package/dist/coverage-gap.js +6 -2
  37. package/dist/coverage-gap.js.map +1 -1
  38. package/dist/decompiler.d.ts +9 -0
  39. package/dist/decompiler.js +17 -2
  40. package/dist/decompiler.js.map +1 -1
  41. package/dist/errors.d.ts +5 -0
  42. package/dist/errors.js +10 -0
  43. package/dist/errors.js.map +1 -1
  44. package/dist/index.d.ts +11 -4
  45. package/dist/index.js +9 -3
  46. package/dist/index.js.map +1 -1
  47. package/dist/node-props.d.ts +253 -0
  48. package/dist/node-props.js +35 -0
  49. package/dist/node-props.js.map +1 -0
  50. package/dist/parser-core.d.ts +5 -0
  51. package/dist/parser-core.js +363 -0
  52. package/dist/parser-core.js.map +1 -0
  53. package/dist/parser-diagnostics.d.ts +14 -0
  54. package/dist/parser-diagnostics.js +31 -0
  55. package/dist/parser-diagnostics.js.map +1 -0
  56. package/dist/parser-keywords.d.ts +5 -0
  57. package/dist/parser-keywords.js +135 -0
  58. package/dist/parser-keywords.js.map +1 -0
  59. package/dist/parser-style.d.ts +3 -0
  60. package/dist/parser-style.js +73 -0
  61. package/dist/parser-style.js.map +1 -0
  62. package/dist/parser-token-stream.d.ts +27 -0
  63. package/dist/parser-token-stream.js +69 -0
  64. package/dist/parser-token-stream.js.map +1 -0
  65. package/dist/parser-tokenizer.d.ts +11 -0
  66. package/dist/parser-tokenizer.js +188 -0
  67. package/dist/parser-tokenizer.js.map +1 -0
  68. package/dist/parser.d.ts +59 -12
  69. package/dist/parser.js +51 -862
  70. package/dist/parser.js.map +1 -1
  71. package/dist/schema.d.ts +7 -2
  72. package/dist/schema.js +7 -2
  73. package/dist/schema.js.map +1 -1
  74. package/dist/source-map.d.ts +27 -0
  75. package/dist/source-map.js +82 -0
  76. package/dist/source-map.js.map +1 -0
  77. package/dist/spec.d.ts +1 -1
  78. package/dist/spec.js +2 -0
  79. package/dist/spec.js.map +1 -1
  80. package/dist/styles-tailwind.d.ts +10 -0
  81. package/dist/styles-tailwind.js +10 -0
  82. package/dist/styles-tailwind.js.map +1 -1
  83. package/dist/template-engine.d.ts +10 -5
  84. package/dist/template-engine.js +10 -5
  85. package/dist/template-engine.js.map +1 -1
  86. package/dist/types.d.ts +8 -3
  87. package/dist/utils.d.ts +20 -0
  88. package/dist/utils.js +20 -0
  89. package/dist/utils.js.map +1 -1
  90. package/dist/walk.d.ts +40 -0
  91. package/dist/walk.js +107 -0
  92. package/dist/walk.js.map +1 -0
  93. package/package.json +2 -2
@@ -5,22 +5,43 @@
5
5
  * These are target-agnostic — they compile to TypeScript regardless of target.
6
6
  *
7
7
  * Machine nodes are KERN's killer feature: 12 lines of KERN → 140+ lines of TS.
8
+ *
9
+ * Generator implementations are split into domain modules under codegen/.
10
+ * This file is the thin dispatcher: imports, re-exports, and generateCoreNode switch.
11
+ * Generators that call generateCoreNode recursively remain here to avoid circular imports.
8
12
  */
13
+ import { propsOf } from './node-props.js';
9
14
  import { isTemplateNode, expandTemplateNode } from './template-engine.js';
10
15
  import { KernCodegenError } from './errors.js';
11
16
  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.
17
+ // ── Re-exports: emitters & helpers (backward compatibility) ─────────────
14
18
  export { emitIdentifier, emitStringLiteral, emitPath, emitTemplateSafe, emitTypeAnnotation, emitImportSpecifier } from './codegen/emitters.js';
15
19
  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)
20
+ // ── Re-exports: domain generators (backward compatibility) ──────────────
21
+ export { generateType, generateInterface, generateUnion, generateService, generateConst } from './codegen/type-system.js';
22
+ export { generateFunction, generateError } from './codegen/functions.js';
23
+ export { generateMachine, generateMachineReducer } from './codegen/machines.js';
24
+ export { generateConfig, generateStore, generateRepository, generateCache, generateDependency, generateModel } from './codegen/data-layer.js';
25
+ export { generateDerive, generateTransform, generateAction, generateGuard, generateAssume, generateInvariant, generateCollect, generateResolve, generateExpect, generateRecover, generatePattern, generateApply } from './codegen/ground-layer.js';
26
+ export { generateEvent, generateOn, generateWebSocket } from './codegen/events.js';
27
+ export { generateImport } from './codegen/modules.js';
28
+ export { generateTest } from './codegen/test-gen.js';
29
+ export { mapSemanticType, SEMANTIC_TYPE_MAP } from './codegen/semantic-types.js';
30
+ // ── Imports for local use within this file ──────────────────────────────
31
+ import { emitIdentifier, emitTemplateSafe, emitImportSpecifier } from './codegen/emitters.js';
32
+ import { getProps, getChildren, getFirstChild, emitReasonAnnotations, emitLowConfidenceTodo } from './codegen/helpers.js';
33
+ import { generateType, generateInterface, generateUnion, generateService, generateConst } from './codegen/type-system.js';
34
+ import { generateFunction, generateError } from './codegen/functions.js';
35
+ import { generateMachine } from './codegen/machines.js';
36
+ import { generateConfig, generateStore, generateRepository, generateCache, generateDependency, generateModel } from './codegen/data-layer.js';
37
+ import { generateDerive, generateTransform, generateAction, generateGuard, generateAssume, generateInvariant, generateCollect, generateResolve, generateExpect, generateRecover, generatePattern, generateApply } from './codegen/ground-layer.js';
38
+ import { generateEvent, generateOn, generateWebSocket } from './codegen/events.js';
39
+ import { generateImport } from './codegen/modules.js';
40
+ import { generateTest } from './codegen/test-gen.js';
41
+ // ── Internal aliases ────────────────────────────────────────────────────
42
+ const p = getProps;
43
+ const kids = getChildren;
44
+ const firstChild = getFirstChild;
24
45
  // ── Evolved Generators (v4) ─────────────────────────────────────────────
25
46
  // Populated at startup by evolved-node-loader. Checked in generateCoreNode
26
47
  // before the default case, allowing graduated nodes to produce output.
@@ -45,787 +66,17 @@ export function clearEvolvedGenerators() {
45
66
  export function hasEvolvedGenerator(type) {
46
67
  return defaultRuntime.hasEvolvedGenerator(type);
47
68
  }
48
- // ── Shared IR node helpers ───────────────────────────────────────────────
49
- // Implementations extracted to codegen/helpers.ts. Re-exported above.
50
- // Internal aliases for local use within this file
51
- const p = getProps;
52
- const kids = getChildren;
53
- const firstChild = getFirstChild;
54
- // ── Type Alias ───────────────────────────────────────────────────────────
55
- // type name=PlanState values="draft|approved|running|paused|completed|failed|cancelled"
56
- // → export type PlanState = 'draft' | 'approved' | 'running' | ...;
57
- export function generateType(node) {
58
- const { name: rawName, values, alias } = p(node);
59
- const name = emitIdentifier(rawName, 'UnknownType', node);
60
- const exp = exportPrefix(node);
61
- if (values) {
62
- const members = values.split('|').map(v => `'${emitTemplateSafe(v.trim())}'`).join(' | ');
63
- return [`${exp}type ${name} = ${members};`];
64
- }
65
- if (alias) {
66
- return [`${exp}type ${name} = ${emitTypeAnnotation(alias, 'unknown', node)};`];
67
- }
68
- return [`${exp}type ${name} = unknown;`];
69
- }
70
- // ── Interface ────────────────────────────────────────────────────────────
71
- // interface name=Plan extends=Base
72
- // field name=id type=string
73
- // field name=state type=PlanState
74
- // field name=steps type="PlanStep[]"
75
- // field name=engineId type=string optional=true
76
- export function generateInterface(node) {
77
- const props = p(node);
78
- const name = emitIdentifier(props.name, 'UnknownInterface', node);
79
- const ext = props.extends ? ` extends ${emitTypeAnnotation(props.extends, 'unknown', node)}` : '';
80
- const exp = exportPrefix(node);
81
- const lines = [];
82
- lines.push(`${exp}interface ${name}${ext} {`);
83
- for (const field of kids(node, 'field')) {
84
- const fp = p(field);
85
- const fieldName = emitIdentifier(fp.name, 'field', field);
86
- const opt = fp.optional === 'true' || fp.optional === true ? '?' : '';
87
- lines.push(` ${fieldName}${opt}: ${emitTypeAnnotation(fp.type, 'unknown', field)};`);
88
- }
89
- lines.push('}');
90
- return lines;
91
- }
92
- // ── Discriminated Union ──────────────────────────────────────────────────
93
- // union name=ContentSegment discriminant=type
94
- // variant name=prose
95
- // field name=text type=string
96
- // variant name=code
97
- // field name=language type=string
98
- // field name=code type=string
99
- // → export type ContentSegment =
100
- // | { type: 'prose'; text: string }
101
- // | { type: 'code'; language: string; code: string };
102
- export function generateUnion(node) {
103
- const props = p(node);
104
- const name = emitIdentifier(props.name, 'UnknownUnion', node);
105
- const discriminant = emitIdentifier(props.discriminant, 'type', node);
106
- const exp = exportPrefix(node);
107
- const variants = kids(node, 'variant');
108
- if (variants.length === 0) {
109
- return [`${exp}type ${name} = never;`];
110
- }
111
- const lines = [`${exp}type ${name} =`];
112
- for (let i = 0; i < variants.length; i++) {
113
- const vp = p(variants[i]);
114
- const vname = emitIdentifier(vp.name, 'variant', variants[i]);
115
- const fields = kids(variants[i], 'field');
116
- const fieldParts = [`${discriminant}: '${emitTemplateSafe(vname)}'`];
117
- for (const field of fields) {
118
- const fp = p(field);
119
- const opt = fp.optional === 'true' || fp.optional === true ? '?' : '';
120
- fieldParts.push(`${emitIdentifier(fp.name, 'field', field)}${opt}: ${emitTypeAnnotation(fp.type, 'unknown', field)}`);
121
- }
122
- const semi = i === variants.length - 1 ? ';' : '';
123
- lines.push(` | { ${fieldParts.join('; ')} }${semi}`);
124
- }
125
- return lines;
126
- }
127
- // ── Service (Class) ─────────────────────────────────────────────────────
128
- // service name=TokenTracker
129
- // field name=entries type="TokenUsage[]" default="[]" private=true
130
- // method name=record params="usage:TokenUsage" returns=void
131
- // handler <<<
132
- // this.entries.push(usage);
133
- // >>>
134
- // method name=getStats returns=SessionStats
135
- // handler <<<
136
- // return { ... };
137
- // >>>
138
- // singleton name=tracker type=TokenTracker
139
- // → export class TokenTracker { ... }
140
- // → export const tracker = new TokenTracker();
141
- export function generateService(node) {
142
- const props = p(node);
143
- const name = emitIdentifier(props.name, 'UnknownService', node);
144
- const impl = props.implements;
145
- const exp = exportPrefix(node);
146
- const lines = [];
147
- const implClause = impl ? ` implements ${emitTypeAnnotation(impl, 'unknown', node)}` : '';
148
- lines.push(`${exp}class ${name}${implClause} {`);
149
- // Fields
150
- for (const field of kids(node, 'field')) {
151
- const fp = p(field);
152
- const fieldName = emitIdentifier(fp.name, 'field', field);
153
- const vis = fp.private === 'true' || fp.private === true ? 'private ' : '';
154
- const readonly = fp.readonly === 'true' || fp.readonly === true ? 'readonly ' : '';
155
- const typeAnnotation = fp.type ? `: ${emitTypeAnnotation(fp.type, 'unknown', field)}` : '';
156
- const defaultVal = fp.default;
157
- // default values are by-design raw code (escape hatch) — documented, not sanitized
158
- const init = defaultVal !== undefined ? ` = ${defaultVal}` : '';
159
- lines.push(` ${vis}${readonly}${fieldName}${typeAnnotation}${init};`);
160
- }
161
- // Constructor (if any constructor child exists)
162
- const ctorNode = firstChild(node, 'constructor');
163
- if (ctorNode) {
164
- const ctorProps = p(ctorNode);
165
- const ctorParams = ctorProps.params ? parseParamList(ctorProps.params) : '';
166
- const ctorCode = handlerCode(ctorNode);
167
- lines.push('');
168
- lines.push(` constructor(${ctorParams}) {`);
169
- if (ctorCode) {
170
- for (const line of ctorCode.split('\n')) {
171
- lines.push(` ${line}`);
172
- }
173
- }
174
- lines.push(' }');
175
- }
176
- // Methods
177
- for (const method of kids(node, 'method')) {
178
- const mp = p(method);
179
- const mname = emitIdentifier(mp.name, 'method', method);
180
- const mparams = mp.params ? parseParamList(mp.params) : '';
181
- const isAsync = mp.async === 'true' || mp.async === true;
182
- const isStream = mp.stream === 'true' || mp.stream === true;
183
- const isStatic = mp.static === 'true' || mp.static === true;
184
- const vis = mp.private === 'true' || mp.private === true ? 'private ' : '';
185
- const staticKw = isStatic ? 'static ' : '';
186
- const star = isStream ? '*' : '';
187
- const asyncKw = (isAsync || isStream) ? 'async ' : '';
188
- const mcode = handlerCode(method);
189
- // stream=true → AsyncGenerator return type
190
- const mreturns = isStream
191
- ? `: AsyncGenerator<${emitTypeAnnotation(mp.returns, 'unknown', method)}>`
192
- : mp.returns ? `: ${emitTypeAnnotation(mp.returns, 'unknown', method)}` : '';
193
- lines.push('');
194
- lines.push(` ${vis}${staticKw}${asyncKw}${star}${mname}(${mparams})${mreturns} {`);
195
- if (mcode) {
196
- for (const line of mcode.split('\n')) {
197
- lines.push(` ${line}`);
198
- }
199
- }
200
- lines.push(' }');
201
- }
202
- lines.push('}');
203
- // Singleton instances
204
- for (const singleton of kids(node, 'singleton')) {
205
- const sp = p(singleton);
206
- const sname = emitIdentifier(sp.name, 'instance', singleton);
207
- const stype = emitIdentifier(sp.type, name, singleton);
208
- lines.push('');
209
- lines.push(`${exp}const ${sname} = new ${stype}();`);
210
- }
211
- return lines;
212
- }
213
- // ── Function ─────────────────────────────────────────────────────────────
214
- // fn name=createPlan params="action:PlanAction,ws:WorkspaceSnapshot" returns=Plan
215
- // handler <<<
216
- // return { ... };
217
- // >>>
218
- export function generateFunction(node) {
219
- const props = p(node);
220
- const name = emitIdentifier(props.name, 'unknownFn', node);
221
- const params = props.params || '';
222
- const returns = props.returns;
223
- const isAsync = props.async === 'true' || props.async === true;
224
- const isStream = props.stream === 'true' || props.stream === true;
225
- const exp = exportPrefix(node);
226
- const lines = [];
227
- // Parse params: "action:PlanAction,ws:WorkspaceSnapshot,spread:number=8"
228
- // → "action: PlanAction, ws: WorkspaceSnapshot, spread: number = 8"
229
- const paramList = params ? parseParamList(params) : '';
230
- // stream=true → async generator function
231
- if (isStream) {
232
- const yieldType = emitTypeAnnotation(returns, 'unknown', node);
233
- const retClause = `: AsyncGenerator<${yieldType}>`;
234
- const code = handlerCode(node);
235
- lines.push(`${exp}async function* ${name}(${paramList})${retClause} {`);
236
- if (code) {
237
- for (const line of code.split('\n')) {
238
- lines.push(` ${line}`);
239
- }
240
- }
241
- lines.push('}');
242
- return lines;
243
- }
244
- const retClause = returns ? `: ${emitTypeAnnotation(returns, 'unknown', node)}` : '';
245
- const asyncKw = isAsync ? 'async ' : '';
246
- const code = handlerCode(node);
247
- // Gap 3: signal + cleanup support for async functions
248
- const signalNode = firstChild(node, 'signal');
249
- const cleanupNode = firstChild(node, 'cleanup');
250
- const hasSignal = !!signalNode;
251
- const hasCleanup = !!cleanupNode;
252
- lines.push(`${exp}${asyncKw}function ${name}(${paramList})${retClause} {`);
253
- // Signal → AbortController setup
254
- if (hasSignal) {
255
- const signalName = emitIdentifier(p(signalNode).name, 'abort', signalNode);
256
- lines.push(` const ${signalName} = new AbortController();`);
257
- }
258
- // Wrap body in try/finally if cleanup exists
259
- if (hasCleanup) {
260
- lines.push(' try {');
261
- if (code) {
262
- for (const line of code.split('\n')) {
263
- lines.push(` ${line}`);
264
- }
265
- }
266
- lines.push(' } finally {');
267
- const cleanupCode = p(cleanupNode).code || '';
268
- if (cleanupCode) {
269
- const dedented = dedent(cleanupCode);
270
- for (const line of dedented.split('\n')) {
271
- lines.push(` ${line}`);
272
- }
273
- }
274
- lines.push(' }');
275
- }
276
- else if (code) {
277
- for (const line of code.split('\n')) {
278
- lines.push(` ${line}`);
279
- }
280
- }
281
- lines.push('}');
282
- return lines;
283
- }
284
- // ── Error Class ──────────────────────────────────────────────────────────
285
- // error name=AgonError extends=Error
286
- // error name=PlanStateError extends=AgonError
287
- // field name=expected type="string | string[]"
288
- // field name=actual type=string
289
- // message "Invalid plan state: expected ${expected}, got ${actual}"
290
- export function generateError(node) {
291
- const props = p(node);
292
- const name = emitIdentifier(props.name, 'UnknownError', node);
293
- const ext = emitIdentifier(props.extends, 'Error', node);
294
- const message = props.message;
295
- const exp = exportPrefix(node);
296
- const fields = kids(node, 'field');
297
- const lines = [];
298
- lines.push(`${exp}class ${name} extends ${ext} {`);
299
- const code = handlerCode(node);
300
- if (fields.length > 0) {
301
- lines.push(` constructor(`);
302
- // Check if first field is 'message' — special case: pass to super
303
- const hasMessageParam = p(fields[0]).name === 'message';
304
- for (const field of fields) {
305
- const fp = p(field);
306
- const opt = fp.optional === 'true' || fp.optional === true ? '?' : '';
307
- const isMessage = fp.name === 'message';
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);
311
- if (isMessage) {
312
- lines.push(` ${fName}${opt}: ${fType},`);
313
- }
314
- else {
315
- lines.push(` public readonly ${fName}${opt}: ${fType},`);
316
- }
317
- }
318
- lines.push(` ) {`);
319
- if (code) {
320
- // Custom handler body — replaces auto-generated constructor logic
321
- for (const line of code.split('\n')) {
322
- lines.push(` ${line}`);
323
- }
324
- }
325
- else if (message) {
326
- // Check if message references array fields that need formatting
327
- const arrayFields = fields.filter(f => {
328
- const ft = p(f).type;
329
- return ft.includes('[]') || ft.includes('string |') || ft.includes('| string');
330
- });
331
- for (const f of arrayFields) {
332
- const fn = p(f).name;
333
- lines.push(` const ${fn}Str = Array.isArray(${fn}) ? ${fn}.join(' | ') : ${fn};`);
334
- }
335
- lines.push(` super(\`${message}\`);`);
336
- lines.push(` this.name = '${name}';`);
337
- }
338
- else if (hasMessageParam) {
339
- lines.push(` super(message);`);
340
- lines.push(` this.name = '${name}';`);
341
- }
342
- else {
343
- lines.push(` super();`);
344
- lines.push(` this.name = '${name}';`);
345
- }
346
- lines.push(` }`);
347
- }
348
- else {
349
- lines.push(` constructor(message: string) {`);
350
- lines.push(` super(message);`);
351
- lines.push(` this.name = '${name}';`);
352
- lines.push(` }`);
353
- }
354
- lines.push('}');
355
- return lines;
356
- }
357
- // ── State Machine ────────────────────────────────────────────────────────
358
- // KERN's killer feature. 12 lines of KERN → 140+ lines of TypeScript.
359
- //
360
- // machine name=Plan
361
- // state name=draft initial=true
362
- // state name=approved
363
- // state name=running
364
- // state name=paused
365
- // state name=completed
366
- // state name=failed
367
- // state name=cancelled
368
- // transition name=approve from=draft to=approved
369
- // transition name=start from=approved to=running
370
- // transition name=cancel from="draft|approved|running|paused|failed" to=cancelled
371
- // transition name=fail from="running|paused" to=failed
372
- //
373
- // Generates:
374
- // - PlanState type
375
- // - PlanStateError class
376
- // - approvePlan(), startPlan(), cancelPlan(), failPlan() functions
377
- export function generateMachine(node) {
378
- const props = p(node);
379
- const name = emitIdentifier(props.name, 'UnknownMachine', node);
380
- const exp = exportPrefix(node);
381
- const lines = [];
382
- // Collect states
383
- const states = kids(node, 'state');
384
- const stateNames = states.map(s => {
385
- const sp = p(s);
386
- return emitIdentifier((sp.name || sp.value), 'state', s);
387
- });
388
- // State type
389
- const stateType = `${name}State`;
390
- lines.push(`${exp}type ${stateType} = ${stateNames.map(s => `'${emitTemplateSafe(s)}'`).join(' | ')};`);
391
- lines.push('');
392
- // Error class
393
- const errorName = `${name}StateError`;
394
- lines.push(`${exp}class ${errorName} extends Error {`);
395
- lines.push(` constructor(`);
396
- lines.push(` public readonly expected: string | string[],`);
397
- lines.push(` public readonly actual: string,`);
398
- lines.push(` ) {`);
399
- lines.push(` const expectedStr = Array.isArray(expected) ? expected.join(' | ') : expected;`);
400
- lines.push(` super(\`Invalid ${name.toLowerCase()} state: expected \${expectedStr}, got \${actual}\`);`);
401
- lines.push(` this.name = '${errorName}';`);
402
- lines.push(` }`);
403
- lines.push('}');
404
- lines.push('');
405
- // Transition functions
406
- const transitions = kids(node, 'transition');
407
- for (const t of transitions) {
408
- const tp = p(t);
409
- const tname = emitIdentifier(tp.name, 'transition', t);
410
- const from = tp.from;
411
- const to = tp.to;
412
- const fromStates = from.split('|').map(s => s.trim());
413
- const isMultiFrom = fromStates.length > 1;
414
- const fnName = `${tname}${name}`;
415
- const code = handlerCode(t);
416
- lines.push(`/** ${from} → ${to} */`);
417
- lines.push(`${exp}function ${fnName}<T extends { state: ${stateType} }>(entity: T): T {`);
418
- if (isMultiFrom) {
419
- lines.push(` const validStates: ${stateType}[] = [${fromStates.map(s => `'${s}'`).join(', ')}];`);
420
- lines.push(` if (!validStates.includes(entity.state)) {`);
421
- lines.push(` throw new ${errorName}(validStates, entity.state);`);
422
- lines.push(` }`);
423
- }
424
- else {
425
- lines.push(` if (entity.state !== '${fromStates[0]}') {`);
426
- lines.push(` throw new ${errorName}('${fromStates[0]}', entity.state);`);
427
- lines.push(` }`);
428
- }
429
- if (code) {
430
- for (const line of code.split('\n')) {
431
- lines.push(` ${line}`);
432
- }
433
- }
434
- else {
435
- lines.push(` return { ...entity, state: '${to}' as ${stateType} };`);
436
- }
437
- lines.push('}');
438
- lines.push('');
439
- }
440
- return lines;
441
- }
442
- // ── Machine → useReducer (Ink target) ────────────────────────────────────
443
- // Additive: also emit a React useReducer hook wrapping the transition functions.
444
- // Called by transpiler-ink.ts when target=ink.
445
- export function generateMachineReducer(node) {
446
- const props = p(node);
447
- const name = emitIdentifier(props.name, 'UnknownMachine', node);
448
- const exp = exportPrefix(node);
449
- const lines = [];
450
- // First emit the standard machine output
451
- lines.push(...generateMachine(node));
452
- // Collect states + transitions
453
- const states = kids(node, 'state');
454
- const stateNames = states.map(s => {
455
- const sp = p(s);
456
- return (sp.name || sp.value);
457
- });
458
- const initialState = states.find(s => p(s).initial === 'true' || p(s).initial === true);
459
- const initialName = initialState ? (p(initialState).name || p(initialState).value) : stateNames[0];
460
- const transitions = kids(node, 'transition');
461
- const stateType = `${name}State`;
462
- // Action type union
463
- const actionNames = transitions.map(t => emitIdentifier(p(t).name, 'action', t));
464
- lines.push(`${exp}type ${name}Action = ${actionNames.map(a => `'${a}'`).join(' | ')};`);
465
- lines.push('');
466
- // Reducer function
467
- lines.push(`${exp}function ${name.charAt(0).toLowerCase() + name.slice(1)}Reducer(state: ${stateType}, action: ${name}Action): ${stateType} {`);
468
- lines.push(` const entity = { state };`);
469
- lines.push(` switch (action) {`);
470
- for (const t of transitions) {
471
- const tp = p(t);
472
- const tname = emitIdentifier(tp.name, 'action', t);
473
- const fnName = `${tname}${name}`;
474
- lines.push(` case '${emitTemplateSafe(tname)}': return ${fnName}(entity).state;`);
475
- }
476
- lines.push(` default: return state;`);
477
- lines.push(` }`);
478
- lines.push('}');
479
- lines.push('');
480
- // useReducer hook
481
- lines.push(`${exp}function use${name}Reducer() {`);
482
- lines.push(` const [state, dispatch] = useReducer(${name.charAt(0).toLowerCase() + name.slice(1)}Reducer, '${initialName}' as ${stateType});`);
483
- lines.push(` return { state, dispatch } as const;`);
484
- lines.push('}');
485
- lines.push('');
486
- return lines;
487
- }
488
- // ── Config ───────────────────────────────────────────────────────────────
489
- // config name=AgonConfig
490
- // field name=timeout type=number default=120
491
- // field name=approvalLevel type=ApprovalLevel default="plan"
492
- export function generateConfig(node) {
493
- const props = p(node);
494
- const name = emitIdentifier(props.name, 'Config', node);
495
- const exp = exportPrefix(node);
496
- const fields = kids(node, 'field');
497
- const lines = [];
498
- // Interface
499
- lines.push(`${exp}interface ${name} {`);
500
- for (const field of fields) {
501
- const fp = p(field);
502
- const fieldName = emitIdentifier(fp.name, 'field', field);
503
- const opt = fp.default !== undefined ? '?' : '';
504
- lines.push(` ${fieldName}${opt}: ${emitTypeAnnotation(fp.type, 'unknown', field)};`);
505
- }
506
- lines.push('}');
507
- lines.push('');
508
- // Defaults object
509
- lines.push(`${exp}const DEFAULT_${name.replace(/([a-z])([A-Z])/g, '$1_$2').toUpperCase()}: Required<${name}> = {`);
510
- for (const field of fields) {
511
- const fp = p(field);
512
- const fieldName = emitIdentifier(fp.name, 'field', field);
513
- const ftype = emitTypeAnnotation(fp.type, 'unknown', field);
514
- let def = fp.default;
515
- if (def === undefined) {
516
- if (ftype === 'number')
517
- def = '0';
518
- else if (ftype === 'boolean')
519
- def = 'false';
520
- else if (ftype.endsWith('[]'))
521
- def = '[]';
522
- else
523
- def = "''";
524
- }
525
- else if (ftype === 'string' || (!['number', 'boolean'].includes(ftype) && !ftype.endsWith('[]') && !def.startsWith("'") && !def.startsWith('"'))) {
526
- def = emitStringLiteral(def);
527
- }
528
- lines.push(` ${fieldName}: ${def},`);
529
- }
530
- lines.push('};');
531
- return lines;
532
- }
533
- // ── Store ────────────────────────────────────────────────────────────────
534
- // store name=Plan path="~/.agon/plans" key=id
535
- // model Plan
536
- export function generateStore(node) {
537
- const props = p(node);
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);
542
- const exp = exportPrefix(node);
543
- const lines = [];
544
- const dirConst = `${name.toUpperCase()}_DIR`;
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);
549
- lines.push(`import { readFileSync, writeFileSync, mkdirSync, readdirSync, unlinkSync } from 'node:fs';`);
550
- lines.push(`import { join, resolve } from 'node:path';`);
551
- lines.push(`import { homedir } from 'node:os';`);
552
- lines.push('');
553
- lines.push(`const ${dirConst} = ${resolvedPath};`);
554
- lines.push('');
555
- lines.push(`function ensure${name}Dir(): void {`);
556
- lines.push(` mkdirSync(${dirConst}, { recursive: true });`);
557
- lines.push('}');
558
- lines.push('');
559
- lines.push(`function safe${name}Path(id: string): string {`);
560
- lines.push(` const sanitized = id.replace(/[^a-zA-Z0-9_-]/g, '');`);
561
- lines.push(` if (!sanitized) throw new Error(\`Invalid ID: \${id}\`);`);
562
- lines.push(` const full = resolve(${dirConst}, \`\${sanitized}.json\`);`);
563
- lines.push(` if (!full.startsWith(resolve(${dirConst}))) throw new Error(\`Invalid ID: \${id}\`);`);
564
- lines.push(` return full;`);
565
- lines.push('}');
566
- lines.push('');
567
- lines.push(`${exp}function save${name}(item: ${model}): void {`);
568
- lines.push(` ensure${name}Dir();`);
569
- lines.push(` writeFileSync(safe${name}Path((item as any).${key}), JSON.stringify(item, null, 2) + '\\n');`);
570
- lines.push('}');
571
- lines.push('');
572
- lines.push(`${exp}function load${name}(id: string): ${model} | null {`);
573
- lines.push(` try { return JSON.parse(readFileSync(safe${name}Path(id), 'utf-8')) as ${model}; }`);
574
- lines.push(` catch (e) { if ((e as NodeJS.ErrnoException).code === 'ENOENT') return null; throw e; }`);
575
- lines.push('}');
576
- lines.push('');
577
- lines.push(`${exp}function list${name}s(limit = 20): ${model}[] {`);
578
- lines.push(` ensure${name}Dir();`);
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);`);
586
- lines.push('}');
587
- lines.push('');
588
- lines.push(`${exp}function delete${name}(id: string): boolean {`);
589
- lines.push(` try { unlinkSync(safe${name}Path(id)); return true; }`);
590
- lines.push(` catch { return false; }`);
591
- lines.push('}');
592
- return lines;
593
- }
594
- // ── Test ─────────────────────────────────────────────────────────────────
595
- // test name="Plan Transitions"
596
- // describe name=approvePlan
597
- // it name="transitions draft → approved"
598
- // handler <<<
599
- // expect(approvePlan(makePlan('draft')).state).toBe('approved');
600
- // >>>
601
- export function generateTest(node) {
602
- const props = p(node);
603
- const name = emitTemplateSafe(props.name || 'UnknownTest');
604
- const lines = [];
605
- lines.push(`import { describe, it, expect } from 'vitest';`);
606
- lines.push('');
607
- // Top-level setup handler
608
- const setup = handlerCode(node);
609
- if (setup) {
610
- for (const line of setup.split('\n'))
611
- lines.push(line);
612
- lines.push('');
613
- }
614
- lines.push(`describe('${name}', () => {`);
615
- for (const desc of kids(node, 'describe')) {
616
- const dname = emitTemplateSafe(p(desc).name || 'describe');
617
- lines.push(` describe('${dname}', () => {`);
618
- for (const test of kids(desc, 'it')) {
619
- const tname = emitTemplateSafe(p(test).name || 'test');
620
- const code = handlerCode(test);
621
- lines.push(` it('${tname}', () => {`);
622
- if (code) {
623
- for (const line of code.split('\n'))
624
- lines.push(` ${line}`);
625
- }
626
- lines.push(` });`);
627
- }
628
- lines.push(` });`);
629
- }
630
- // Top-level it blocks
631
- for (const test of kids(node, 'it')) {
632
- const tname = emitTemplateSafe(p(test).name || 'test');
633
- const code = handlerCode(test);
634
- lines.push(` it('${tname}', () => {`);
635
- if (code) {
636
- for (const line of code.split('\n'))
637
- lines.push(` ${line}`);
638
- }
639
- lines.push(` });`);
640
- }
641
- lines.push('});');
642
- return lines;
643
- }
644
- // ── Event ────────────────────────────────────────────────────────────────
645
- // event name=ForgeEvent
646
- // type name="baseline:start"
647
- // type name="baseline:done" data="{ passes: boolean }"
648
- // type name="winner:determined" data="{ winner: string, bestScore: number }"
649
- export function generateEvent(node) {
650
- const props = p(node);
651
- const name = emitIdentifier(props.name, 'UnknownEvent', node);
652
- const exp = exportPrefix(node);
653
- const types = kids(node, 'type');
654
- const lines = [];
655
- // Event type union
656
- lines.push(`${exp}type ${name}Type = ${types.map(t => `'${emitTemplateSafe((p(t).name || p(t).value))}'`).join(' | ')};`);
657
- lines.push('');
658
- // Event interface
659
- lines.push(`${exp}interface ${name} {`);
660
- lines.push(` type: ${name}Type;`);
661
- lines.push(` engineId?: string;`);
662
- lines.push(` data?: Record<string, unknown>;`);
663
- lines.push('}');
664
- lines.push('');
665
- // Typed event map
666
- lines.push(`${exp}interface ${name}Map {`);
667
- for (const t of types) {
668
- const tp = p(t);
669
- const tname = emitTemplateSafe((tp.name || tp.value));
670
- const data = emitTypeAnnotation(tp.data, 'Record<string, unknown>', t);
671
- lines.push(` '${tname}': ${data};`);
672
- }
673
- lines.push('}');
674
- lines.push('');
675
- // Callback type
676
- lines.push(`${exp}type ${name}Callback = (event: ${name}) => void;`);
677
- return lines;
678
- }
679
- // ── On — generic event handler ────────────────────────────────────────────
680
- // on event=click handler=handleClick
681
- // on event=submit
682
- // handler <<<
683
- // e.preventDefault();
684
- // await submitForm(data);
685
- // >>>
686
- // on event=key key=Enter
687
- // handler <<<
688
- // processInput(buffer);
689
- // >>>
690
- // on event=message
691
- // handler <<<
692
- // const data = JSON.parse(event.data);
693
- // dispatch(data);
694
- // >>>
695
- export function generateOn(node) {
696
- const props = p(node);
697
- const event = (props.event || props.name);
698
- const handlerName = props.handler;
699
- const key = props.key;
700
- const code = handlerCode(node);
701
- const exp = exportPrefix(node);
702
- const lines = [];
703
- if (handlerName && !code) {
704
- // Reference to existing handler: on event=click handler=handleClick
705
- lines.push(`${exp}const on${capitalize(event)} = ${handlerName};`);
706
- return lines;
707
- }
708
- // Determine event parameter type (plain DOM types — target-agnostic)
709
- const paramType = event === 'key' || event === 'keydown' || event === 'keyup' ? 'e: KeyboardEvent'
710
- : event === 'message' ? 'event: MessageEvent'
711
- : event === 'submit' ? 'e: SubmitEvent'
712
- : event === 'click' ? 'e: MouseEvent'
713
- : event === 'change' ? 'e: Event'
714
- : event === 'focus' || event === 'blur' ? 'e: FocusEvent'
715
- : event === 'drag' || event === 'drop' ? 'e: DragEvent'
716
- : event === 'scroll' ? 'e: Event'
717
- : event === 'resize' ? 'e: UIEvent'
718
- : event === 'connect' || event === 'disconnect' ? 'ws: WebSocket'
719
- : event === 'error' ? 'error: Error'
720
- : `e: Event`;
721
- const fnName = handlerName || `handle${capitalize(event)}`;
722
- const isAsync = props.async === 'true' || props.async === true;
723
- const asyncKw = isAsync ? 'async ' : '';
724
- // Key filter guard
725
- const keyGuard = key ? ` if (key !== '${key}') return;\n` : '';
726
- lines.push(`${exp}${asyncKw}function ${fnName}(${paramType}) {`);
727
- if (keyGuard)
728
- lines.push(keyGuard.trimEnd());
729
- if (code) {
730
- for (const line of code.split('\n')) {
731
- lines.push(` ${line}`);
732
- }
733
- }
734
- lines.push('}');
735
- return lines;
736
- }
737
- // ── WebSocket — bidirectional communication ──────────────────────────────
738
- // websocket path=/ws
739
- // on event=connect
740
- // handler <<<ws.send(JSON.stringify({ type: 'hello' }));>>>
741
- // on event=message
742
- // handler <<<
743
- // const data = JSON.parse(event.data);
744
- // broadcast(data);
745
- // >>>
746
- // on event=disconnect
747
- // handler <<<console.log('client disconnected');>>>
748
- export function generateWebSocket(node) {
749
- const props = p(node);
750
- const path = (props.path || '/ws');
751
- const name = props.name || 'ws';
752
- const exp = exportPrefix(node);
753
- const lines = [];
754
- const onNodes = kids(node, 'on');
755
- const connectHandler = onNodes.find(n => {
756
- const e = (p(n).event || p(n).name);
757
- return e === 'connect' || e === 'connection';
758
- });
759
- const messageHandler = onNodes.find(n => {
760
- const e = (p(n).event || p(n).name);
761
- return e === 'message';
762
- });
763
- const disconnectHandler = onNodes.find(n => {
764
- const e = (p(n).event || p(n).name);
765
- return e === 'disconnect' || e === 'close';
766
- });
767
- const errorHandler = onNodes.find(n => {
768
- const e = (p(n).event || p(n).name);
769
- return e === 'error';
770
- });
771
- lines.push(`${exp}function setup${capitalize(name)}(wss: WebSocketServer) {`);
772
- lines.push(` wss.on('connection', (ws, req) => {`);
773
- lines.push(` const path = req.url || '${path}';`);
774
- if (connectHandler) {
775
- const code = handlerCode(connectHandler);
776
- if (code) {
777
- for (const line of code.split('\n')) {
778
- lines.push(` ${line}`);
779
- }
780
- }
781
- }
782
- lines.push('');
783
- lines.push(` ws.on('message', (raw) => {`);
784
- if (messageHandler) {
785
- const code = handlerCode(messageHandler);
786
- lines.push(` const data = JSON.parse(raw.toString());`);
787
- if (code) {
788
- for (const line of code.split('\n')) {
789
- lines.push(` ${line}`);
790
- }
791
- }
792
- }
793
- lines.push(` });`);
794
- if (errorHandler) {
795
- lines.push('');
796
- lines.push(` ws.on('error', (error) => {`);
797
- const code = handlerCode(errorHandler);
798
- if (code) {
799
- for (const line of code.split('\n')) {
800
- lines.push(` ${line}`);
801
- }
802
- }
803
- lines.push(` });`);
804
- }
805
- lines.push('');
806
- lines.push(` ws.on('close', () => {`);
807
- if (disconnectHandler) {
808
- const code = handlerCode(disconnectHandler);
809
- if (code) {
810
- for (const line of code.split('\n')) {
811
- lines.push(` ${line}`);
812
- }
813
- }
814
- }
815
- lines.push(` });`);
816
- lines.push(` });`);
817
- lines.push('}');
818
- return lines;
819
- }
69
+ // ── Generators that call generateCoreNode (kept here to avoid circular imports) ──
820
70
  // ── Module ───────────────────────────────────────────────────────────────
821
71
  // module name=@agon/core
822
72
  // export from="./plan.js" names="createPlan,advanceStep"
823
73
  export function generateModule(node) {
824
- const props = p(node);
74
+ const props = propsOf(node);
825
75
  const name = emitTemplateSafe(props.name || 'unknown');
826
76
  const lines = [];
827
77
  lines.push(`// ── Module: ${name} ──`);
828
78
  lines.push('');
79
+ // 'export' children don't have a typed interface in NodePropsMap
829
80
  for (const exp of kids(node, 'export')) {
830
81
  const ep = p(exp);
831
82
  const rawFrom = ep.from;
@@ -874,223 +125,14 @@ export function generateModule(node) {
874
125
  }
875
126
  return lines;
876
127
  }
877
- // ── Import ──────────────────────────────────────────────────────────────
878
- // import from="node:fs" names="readFileSync,writeFileSync"
879
- // → import { readFileSync, writeFileSync } from 'node:fs';
880
- //
881
- // import from="./types.js" names="Plan" types=true
882
- // → import type { Plan } from './types.js';
883
- //
884
- // import from="node:path" default=path
885
- // → import path from 'node:path';
886
- //
887
- // import from="node:fs" default=fs names="readFileSync"
888
- // → import fs, { readFileSync } from 'node:fs';
889
- export function generateImport(node) {
890
- const props = p(node);
891
- const from = props.from;
892
- const names = props.names;
893
- const defaultImport = props.default;
894
- const isTypeOnly = props.types === 'true' || props.types === true;
895
- if (!from)
896
- return [];
897
- const safePath = emitImportSpecifier(from, node);
898
- const typeKw = isTypeOnly ? 'type ' : '';
899
- const safeDefault = defaultImport ? emitIdentifier(defaultImport, 'default', node) : '';
900
- const namedList = names
901
- ? names.split(',').map(s => emitIdentifier(s.trim(), 'import', node)).join(', ')
902
- : '';
903
- if (safeDefault && namedList) {
904
- return [`import ${typeKw}${safeDefault}, { ${namedList} } from '${safePath}';`];
905
- }
906
- if (safeDefault) {
907
- return [`import ${typeKw}${safeDefault} from '${safePath}';`];
908
- }
909
- if (namedList) {
910
- return [`import ${typeKw}{ ${namedList} } from '${safePath}';`];
911
- }
912
- // Side-effect import
913
- return [`import '${safePath}';`];
914
- }
915
- // ── Const ───────────────────────────────────────────────────────────────
916
- // const name=AGON_HOME type=string
917
- // handler <<<
918
- // join(homedir(), '.agon')
919
- // >>>
920
- // → export const AGON_HOME: string = join(homedir(), '.agon');
921
- //
922
- // const name=DEFAULT_WEIGHTS type=ScoreWeights value="{ pass: 50 }"
923
- // → export const DEFAULT_WEIGHTS: ScoreWeights = { pass: 50 };
924
- export function generateConst(node) {
925
- const props = p(node);
926
- const name = emitIdentifier(props.name, 'unknownConst', node);
927
- const constType = props.type;
928
- const value = props.value;
929
- const exp = exportPrefix(node);
930
- const code = handlerCode(node);
931
- const typeAnnotation = constType ? `: ${emitTypeAnnotation(constType, 'unknown', node)}` : '';
932
- if (code) {
933
- return [`${exp}const ${name}${typeAnnotation} = ${code.trim()};`];
934
- }
935
- if (value) {
936
- return [`${exp}const ${name}${typeAnnotation} = ${value};`];
937
- }
938
- return [`${exp}const ${name}${typeAnnotation};`];
939
- }
940
- // ── Shared Helpers ───────────────────────────────────────────────────────
941
- // parseParamList, capitalize, emitReasonAnnotations, emitLowConfidenceTodo
942
- // implementations extracted to codegen/helpers.ts. Re-exported at top of file.
943
- // ── Ground Layer: derive ─────────────────────────────────────────────────
944
- // derive name=loudness expr={{average(stems)}} type=number deps="stems"
945
- // → export const loudness: number = average(stems);
946
- export function generateDerive(node) {
947
- const annotations = emitReasonAnnotations(node);
948
- const conf = p(node).confidence;
949
- const todo = emitLowConfidenceTodo(node, conf);
950
- const props = p(node);
951
- const name = emitIdentifier(props.name, 'derived', node);
952
- // expr is by-design raw code (escape hatch)
953
- const expr = props.expr;
954
- const constType = props.type;
955
- const exp = exportPrefix(node);
956
- const typeAnnotation = constType ? `: ${emitTypeAnnotation(constType, 'unknown', node)}` : '';
957
- return [...todo, ...annotations, `${exp}const ${name}${typeAnnotation} = ${expr};`];
958
- }
959
- // ── Ground Layer: transform ──────────────────────────────────────────────
960
- // transform name=limitStems target="track.stems" via="limit(4)" type="Stem[]"
961
- // → export const limitStems: Stem[] = limit(track.stems, 4);
962
- export function generateTransform(node) {
963
- const annotations = emitReasonAnnotations(node);
964
- const conf = p(node).confidence;
965
- const todo = emitLowConfidenceTodo(node, conf);
966
- const props = p(node);
967
- const name = emitIdentifier(props.name, 'transform', node);
968
- // target and via are by-design raw code (escape hatches)
969
- const target = props.target;
970
- const via = props.via;
971
- const constType = props.type;
972
- const exp = exportPrefix(node);
973
- const code = handlerCode(node);
974
- const typeAnnotation = constType ? `: ${emitTypeAnnotation(constType, 'unknown', node)}` : '';
975
- if (code) {
976
- // Handler block form — generate a function
977
- const lines = [...todo, ...annotations];
978
- lines.push(`${exp}function ${name}(state: unknown)${typeAnnotation} {`);
979
- for (const line of code.split('\n')) {
980
- lines.push(` ${line}`);
981
- }
982
- lines.push('}');
983
- return lines;
984
- }
985
- if (target && via) {
986
- return [...todo, ...annotations, `${exp}const ${name}${typeAnnotation} = ${via.replace(/\(/, `(${target}, `).replace(/, \)/, ')')};`];
987
- }
988
- if (via) {
989
- return [...todo, ...annotations, `${exp}const ${name}${typeAnnotation} = ${via};`];
990
- }
991
- return [...todo, ...annotations, `${exp}const ${name}${typeAnnotation};`];
992
- }
993
- // ── Ground Layer: action ─────────────────────────────────────────────────
994
- // action name=notifyOwner idempotent=true reversible=true
995
- // handler <<<await email.send(track.owner, 'processed');>>>
996
- export function generateAction(node) {
997
- const annotations = emitReasonAnnotations(node);
998
- const conf = p(node).confidence;
999
- const todo = emitLowConfidenceTodo(node, conf);
1000
- const props = p(node);
1001
- const name = emitIdentifier(props.name, 'action', node);
1002
- const idempotent = props.idempotent === 'true' || props.idempotent === true;
1003
- const reversible = props.reversible === 'true' || props.reversible === true;
1004
- const params = props.params || '';
1005
- const returns = props.returns;
1006
- const exp = exportPrefix(node);
1007
- const code = handlerCode(node);
1008
- const lines = [...todo, ...annotations];
1009
- // JSDoc for action metadata
1010
- const metaParts = [];
1011
- if (idempotent)
1012
- metaParts.push('idempotent=true');
1013
- if (reversible)
1014
- metaParts.push('reversible=true');
1015
- if (metaParts.length > 0) {
1016
- lines.push(`/** @action ${metaParts.join(' ')} */`);
1017
- }
1018
- const paramList = params ? parseParamList(params) : '';
1019
- const retClause = returns ? `: Promise<${emitTypeAnnotation(returns, 'void', node)}>` : ': Promise<void>';
1020
- lines.push(`${exp}async function ${name}(${paramList})${retClause} {`);
1021
- if (code) {
1022
- for (const line of code.split('\n')) {
1023
- lines.push(` ${line}`);
1024
- }
1025
- }
1026
- lines.push('}');
1027
- return lines;
1028
- }
1029
- // ── Ground Layer: guard (extended — already in NODE_TYPES) ───────────────
1030
- // guard name=published expr={{track.status == "published"}} else=403
1031
- export function generateGuard(node) {
1032
- const annotations = emitReasonAnnotations(node);
1033
- const conf = p(node).confidence;
1034
- const todo = emitLowConfidenceTodo(node, conf);
1035
- const props = p(node);
1036
- const name = props.name || 'guard';
1037
- const expr = props.expr;
1038
- const elseCode = props.else;
1039
- const lines = [...todo, ...annotations];
1040
- if (elseCode && /^\d+$/.test(elseCode)) {
1041
- lines.push(`if (!(${expr})) { throw new HttpError(${elseCode}, 'Guard: ${name}'); }`);
1042
- }
1043
- else if (elseCode) {
1044
- lines.push(`if (!(${expr})) { ${elseCode}; }`);
1045
- }
1046
- else {
1047
- lines.push(`if (!(${expr})) { throw new Error('Guard failed: ${name}'); }`);
1048
- }
1049
- return lines;
1050
- }
1051
- // ── Ground Layer: assume ─────────────────────────────────────────────────
1052
- // assume expr={{track.owner == $auth.user}} scope=request evidence="route-signing" fallback="throw AuthError()"
1053
- export function generateAssume(node) {
1054
- const annotations = emitReasonAnnotations(node);
1055
- const conf = p(node).confidence;
1056
- const todo = emitLowConfidenceTodo(node, conf);
1057
- const props = p(node);
1058
- const expr = props.expr;
1059
- const scope = props.scope || 'request';
1060
- const evidence = props.evidence;
1061
- const fallback = props.fallback;
1062
- if (!evidence)
1063
- throw new KernCodegenError('assume requires evidence prop', node);
1064
- if (!fallback)
1065
- throw new KernCodegenError('assume requires fallback prop', node);
1066
- const lines = [...todo, ...annotations];
1067
- lines.push(`/** @assume ${expr} @scope ${scope} @evidence ${evidence} */`);
1068
- lines.push(`if (process.env.NODE_ENV !== 'production') {`);
1069
- lines.push(` if (!(${expr})) { ${fallback}; }`);
1070
- lines.push(`}`);
1071
- return lines;
1072
- }
1073
- // ── Ground Layer: invariant ──────────────────────────────────────────────
1074
- // invariant name=stemLimit expr={{visible_stems <= policy.max_stems($auth.tier)}}
1075
- export function generateInvariant(node) {
1076
- const annotations = emitReasonAnnotations(node);
1077
- const conf = p(node).confidence;
1078
- const todo = emitLowConfidenceTodo(node, conf);
1079
- const props = p(node);
1080
- const name = props.name || 'invariant';
1081
- const expr = props.expr;
1082
- const lines = [...todo, ...annotations];
1083
- lines.push(`console.assert(${expr}, 'Invariant: ${name}');`);
1084
- return lines;
1085
- }
1086
- // ── Ground Layer: each (ground-layer, not inside parallel) ───────────────
128
+ // ── Each (ground-layer, calls generateCoreNode) ─────────────────────────
1087
129
  // each name=stem in="track.stems"
1088
130
  // derive name=normalized expr={{normalize(stem.amplitude)}}
1089
131
  export function generateEach(node) {
1090
132
  const annotations = emitReasonAnnotations(node);
1091
- const conf = p(node).confidence;
133
+ const props = propsOf(node);
134
+ const conf = props.confidence;
1092
135
  const todo = emitLowConfidenceTodo(node, conf);
1093
- const props = p(node);
1094
136
  const name = props.name || 'item';
1095
137
  const collection = props.in;
1096
138
  const index = props.index;
@@ -1110,43 +152,22 @@ export function generateEach(node) {
1110
152
  lines.push('}');
1111
153
  return lines;
1112
154
  }
1113
- // ── Ground Layer: collect ────────────────────────────────────────────────
1114
- // collect name=overThreshold from="track.stems" where={{measure(stem.loudness) > threshold}} limit=10
1115
- export function generateCollect(node) {
1116
- const annotations = emitReasonAnnotations(node);
1117
- const conf = p(node).confidence;
1118
- const todo = emitLowConfidenceTodo(node, conf);
1119
- const props = p(node);
1120
- const name = emitIdentifier(props.name, 'collected', node);
1121
- const from = props.from;
1122
- const where = props.where;
1123
- const limit = props.limit;
1124
- const order = props.order;
1125
- const exp = exportPrefix(node);
1126
- let chain = from;
1127
- if (where)
1128
- chain += `.filter(item => ${where})`;
1129
- if (order)
1130
- chain += `.sort((a, b) => ${order})`;
1131
- if (limit)
1132
- chain += `.slice(0, ${limit})`;
1133
- return [...todo, ...annotations, `${exp}const ${name} = ${chain};`];
1134
- }
1135
- // ── Ground Layer: branch / path ──────────────────────────────────────────
155
+ // ── Branch / path (ground-layer, calls generateCoreNode) ────────────────
1136
156
  // branch name=tierRoute on="user.tier"
1137
157
  // path value="free"
1138
158
  // derive name=maxStems expr={{4}} type=number
1139
159
  export function generateBranch(node) {
1140
160
  const annotations = emitReasonAnnotations(node);
1141
- const conf = p(node).confidence;
161
+ const props = propsOf(node);
162
+ const conf = props.confidence;
1142
163
  const todo = emitLowConfidenceTodo(node, conf);
1143
- const props = p(node);
1144
164
  const name = props.name || 'branch';
1145
165
  const on = props.on;
1146
166
  const paths = kids(node, 'path');
1147
167
  const lines = [...todo, ...annotations];
1148
168
  lines.push(`/** branch: ${name} */`);
1149
169
  lines.push(`switch (${on}) {`);
170
+ // 'path' children don't have a typed interface in NodePropsMap
1150
171
  for (const pathNode of paths) {
1151
172
  const pp = p(pathNode);
1152
173
  const value = pp.value;
@@ -1163,175 +184,19 @@ export function generateBranch(node) {
1163
184
  lines.push('}');
1164
185
  return lines;
1165
186
  }
1166
- // ── Ground Layer: resolve / candidate / discriminator ────────────────────
1167
- // resolve name=normStrategy
1168
- // candidate name=aggressive
1169
- // handler <<<return aggressiveNormalize(signal);>>>
1170
- // discriminator method=benchmark metric="snr"
1171
- // handler <<<...>>>
1172
- export function generateResolve(node) {
1173
- const annotations = emitReasonAnnotations(node);
1174
- const conf = p(node).confidence;
1175
- const todo = emitLowConfidenceTodo(node, conf);
1176
- const props = p(node);
1177
- const name = emitIdentifier(props.name, 'resolver', node);
1178
- const candidates = kids(node, 'candidate');
1179
- const discriminator = firstChild(node, 'discriminator');
1180
- if (!discriminator)
1181
- throw new KernCodegenError('resolve requires discriminator', node);
1182
- const lines = [...todo, ...annotations];
1183
- const dp = p(discriminator);
1184
- const method = dp.method || 'select';
1185
- const metric = dp.metric || '';
1186
- // Candidate array
1187
- lines.push(`/** resolve: ${name} */`);
1188
- lines.push(`const _${name}_candidates = [`);
1189
- for (const c of candidates) {
1190
- const cp = p(c);
1191
- const cname = emitIdentifier(cp.name, 'candidate', c);
1192
- const code = handlerCode(c);
1193
- lines.push(` { name: '${cname}', fn: (signal: unknown) => { ${code.trim()} } },`);
1194
- }
1195
- lines.push(`];`);
1196
- lines.push('');
1197
- // Resolver function
1198
- const discCode = handlerCode(discriminator);
1199
- lines.push(`async function resolve${capitalize(name)}(signal: unknown): Promise<unknown> {`);
1200
- lines.push(` const candidates = _${name}_candidates;`);
1201
- lines.push(` // discriminator: ${method}(${metric})`);
1202
- if (discCode) {
1203
- for (const line of discCode.split('\n')) {
1204
- lines.push(` ${line}`);
1205
- }
1206
- }
1207
- lines.push(` return candidates[winnerIdx].fn(signal);`);
1208
- lines.push('}');
1209
- return lines;
1210
- }
1211
- // ── Ground Layer: expect ─────────────────────────────────────────────────
1212
- // expect name=clipRate expr={{clip_flags_rate}} within="0.02..0.08"
1213
- export function generateExpect(node) {
1214
- const annotations = emitReasonAnnotations(node);
1215
- const conf = p(node).confidence;
1216
- const todo = emitLowConfidenceTodo(node, conf);
1217
- const props = p(node);
1218
- const name = props.name || 'expected';
1219
- const expr = props.expr;
1220
- const within = props.within;
1221
- const max = props.max;
1222
- const min = props.min;
1223
- const lines = [...todo, ...annotations];
1224
- lines.push(`if (process.env.NODE_ENV !== 'production') {`);
1225
- lines.push(` const _${name} = ${expr};`);
1226
- if (within) {
1227
- const [lo, hi] = within.split('..');
1228
- lines.push(` console.assert(_${name} >= ${lo} && _${name} <= ${hi}, 'Expected ${name} in [${lo}, ${hi}], got ' + _${name});`);
1229
- }
1230
- else if (min && max) {
1231
- lines.push(` console.assert(_${name} >= ${min} && _${name} <= ${max}, 'Expected ${name} in [${min}, ${max}], got ' + _${name});`);
1232
- }
1233
- else if (max) {
1234
- lines.push(` console.assert(_${name} <= ${max}, 'Expected ${name} <= ${max}, got ' + _${name});`);
1235
- }
1236
- else if (min) {
1237
- lines.push(` console.assert(_${name} >= ${min}, 'Expected ${name} >= ${min}, got ' + _${name});`);
1238
- }
1239
- else {
1240
- lines.push(` console.assert(_${name} != null, 'Expected ${name} to be defined');`);
1241
- }
1242
- lines.push('}');
1243
- return lines;
1244
- }
1245
- // ── Ground Layer: recover / strategy ─────────────────────────────────────
1246
- // recover name=paymentFlow
1247
- // strategy name=retry max=3 delay=1000
1248
- // strategy name=fallback
1249
- // handler <<<throw new PaymentError('All recovery exhausted');>>>
1250
- export function generateRecover(node) {
1251
- const annotations = emitReasonAnnotations(node);
1252
- const conf = p(node).confidence;
1253
- const todo = emitLowConfidenceTodo(node, conf);
1254
- const props = p(node);
1255
- const name = emitIdentifier(props.name, 'recovery', node);
1256
- const strategies = kids(node, 'strategy');
1257
- const hasFallback = strategies.some(s => p(s).name === 'fallback');
1258
- if (!hasFallback)
1259
- throw new KernCodegenError('recover requires a fallback strategy', node);
1260
- const lines = [...todo, ...annotations];
1261
- lines.push(`/** recover: ${name} */`);
1262
- lines.push(`async function ${name}WithRecovery<T>(fn: () => Promise<T>): Promise<T> {`);
1263
- for (const strategy of strategies) {
1264
- const sp = p(strategy);
1265
- const sname = emitIdentifier(sp.name, 'strategy', strategy);
1266
- const code = handlerCode(strategy);
1267
- if (sname === 'retry') {
1268
- const max = Number(sp.max) || 3;
1269
- const delay = Number(sp.delay) || 1000;
1270
- lines.push(` // strategy: retry (max=${max}, delay=${delay}ms)`);
1271
- lines.push(` for (let _attempt = 0; _attempt < ${max}; _attempt++) {`);
1272
- lines.push(` try { return await fn(); }`);
1273
- lines.push(` catch { if (_attempt < ${max - 1}) await new Promise(r => setTimeout(r, ${delay})); }`);
1274
- lines.push(` }`);
1275
- }
1276
- else if (sname === 'fallback') {
1277
- lines.push(` // strategy: fallback (terminal)`);
1278
- if (code) {
1279
- for (const line of code.split('\n')) {
1280
- lines.push(` ${line}`);
1281
- }
1282
- }
1283
- else {
1284
- lines.push(` throw new Error('All recovery strategies exhausted for ${name}');`);
1285
- }
1286
- }
1287
- else {
1288
- // compensate, degrade, or custom
1289
- lines.push(` // strategy: ${sname}`);
1290
- lines.push(` try {`);
1291
- if (code) {
1292
- for (const line of code.split('\n')) {
1293
- lines.push(` ${line}`);
1294
- }
1295
- }
1296
- lines.push(` } catch {}`);
1297
- }
1298
- }
1299
- lines.push('}');
1300
- return lines;
1301
- }
1302
- // ── Ground Layer: pattern / apply ────────────────────────────────────────
1303
- // pattern → registerTemplate() alias (handled by template engine)
1304
- // apply → expandTemplateNode() alias
1305
- export function generatePattern(node) {
1306
- // pattern nodes are registered as templates — no direct output
1307
- return [];
1308
- }
1309
- export function generateApply(node, _depth = 0) {
1310
- // apply nodes expand the referenced pattern
1311
- const props = p(node);
1312
- const patternName = props.pattern;
1313
- if (!patternName)
1314
- return [];
1315
- // Delegate to template expansion — propagate depth to prevent infinite recursion
1316
- const syntheticNode = { ...node, type: patternName };
1317
- if (isTemplateNode(patternName)) {
1318
- return expandTemplateNode(syntheticNode, _depth + 1);
1319
- }
1320
- return [`// apply: pattern '${patternName}' not found`];
1321
- }
1322
- // ── Conditional ──────────────────────────────────────────────────────────
187
+ // ── Conditional (calls generateCoreNode) ─────────────────────────────────
1323
188
  // conditional if=isPro
1324
189
  // text value="Pro features unlocked"
1325
190
  // → {isPro && (<>..children..</>)}
1326
191
  export function generateConditional(node) {
1327
- const props = p(node);
192
+ const props = propsOf(node);
1328
193
  const rawCondition = props.if;
1329
194
  // Handle expression objects: { __expr: true, code: 'loading' }
1330
195
  const condition = rawCondition && typeof rawCondition === 'object' && rawCondition.__expr
1331
196
  ? rawCondition.code
1332
197
  : rawCondition;
1333
198
  if (!condition)
1334
- return [`// conditional: missing 'if' prop`];
199
+ throw new KernCodegenError("conditional node requires an 'if' prop", node);
1335
200
  const childLines = [];
1336
201
  for (const child of kids(node)) {
1337
202
  childLines.push(...generateCoreNode(child));
@@ -1355,7 +220,7 @@ export function generateConditional(node) {
1355
220
  // option value=active label="Active"
1356
221
  // option value=pending label="Pending"
1357
222
  export function generateSelect(node) {
1358
- const props = p(node);
223
+ const props = propsOf(node);
1359
224
  const name = props.name || 'select';
1360
225
  const value = props.value;
1361
226
  const placeholder = props.placeholder;
@@ -1372,7 +237,7 @@ export function generateSelect(node) {
1372
237
  lines.push(` <option value="" disabled>${emitTemplateSafe(placeholder)}</option>`);
1373
238
  }
1374
239
  for (const opt of kids(node, 'option')) {
1375
- const op = p(opt);
240
+ const op = propsOf(opt);
1376
241
  const optValue = emitTemplateSafe(op.value || '');
1377
242
  const optLabel = emitTemplateSafe(op.label || op.value || '');
1378
243
  lines.push(` <option value="${optValue}">${optLabel}</option>`);
@@ -1380,191 +245,6 @@ export function generateSelect(node) {
1380
245
  lines.push(`</select>`);
1381
246
  return lines;
1382
247
  }
1383
- // ── Model ────────────────────────────────────────────────────────────────
1384
- // model name=User table=users
1385
- // column name=id type=uuid primary=true
1386
- // column name=email type=string unique=true index=true
1387
- // relation name=posts target=Post kind=one-to-many cascade=delete
1388
- export function generateModel(node) {
1389
- const props = p(node);
1390
- const name = emitIdentifier(props.name, 'UnknownModel', node);
1391
- const table = props.table;
1392
- const exp = exportPrefix(node);
1393
- const lines = [];
1394
- // Generate TypeScript interface
1395
- lines.push(`${exp}interface ${name} {`);
1396
- for (const col of kids(node, 'column')) {
1397
- const cp = p(col);
1398
- const colName = emitIdentifier(cp.name, 'column', col);
1399
- const colType = mapColumnType(cp.type);
1400
- const opt = cp.optional === 'true' || cp.optional === true ? '?' : '';
1401
- lines.push(` ${colName}${opt}: ${colType};`);
1402
- }
1403
- for (const rel of kids(node, 'relation')) {
1404
- const rp = p(rel);
1405
- const relName = emitIdentifier(rp.name, 'relation', rel);
1406
- const target = rp.target;
1407
- const kind = rp.kind || 'one-to-many';
1408
- const relType = kind.includes('many') ? `${target}[]` : target;
1409
- lines.push(` ${relName}?: ${relType};`);
1410
- }
1411
- lines.push('}');
1412
- // Prisma-hint comment
1413
- if (table) {
1414
- lines.push('');
1415
- lines.push(`// Prisma: @@map("${table}")`);
1416
- }
1417
- return lines;
1418
- }
1419
- function mapColumnType(kernType) {
1420
- const typeMap = {
1421
- uuid: 'string', string: 'string', text: 'string',
1422
- int: 'number', integer: 'number', float: 'number', decimal: 'number',
1423
- boolean: 'boolean', bool: 'boolean',
1424
- date: 'Date', datetime: 'Date', timestamp: 'Date',
1425
- json: 'Record<string, unknown>',
1426
- };
1427
- return typeMap[kernType] || kernType;
1428
- }
1429
- // ── Repository ───────────────────────────────────────────────────────────
1430
- // repository name=UserRepo model=User
1431
- // method name=findByEmail params="email:string" returns="User|null"
1432
- // handler <<<return this.findOne({ email });>>>
1433
- export function generateRepository(node) {
1434
- const props = p(node);
1435
- const name = emitIdentifier(props.name, 'UnknownRepo', node);
1436
- const model = props.model;
1437
- const exp = exportPrefix(node);
1438
- const lines = [];
1439
- lines.push(`${exp}class ${name} {`);
1440
- if (model) {
1441
- lines.push(` constructor(private readonly model: typeof ${model}) {}`);
1442
- lines.push('');
1443
- }
1444
- for (const method of kids(node, 'method')) {
1445
- const mp = p(method);
1446
- const mname = emitIdentifier(mp.name, 'method', method);
1447
- const mparams = mp.params ? parseParamList(mp.params) : '';
1448
- const isAsync = mp.async === 'true' || mp.async === true;
1449
- const asyncKw = isAsync ? 'async ' : '';
1450
- const mreturns = mp.returns ? `: ${emitTypeAnnotation(mp.returns, 'unknown', method)}` : '';
1451
- const mcode = handlerCode(method);
1452
- lines.push(` ${asyncKw}${mname}(${mparams})${mreturns} {`);
1453
- if (mcode) {
1454
- for (const line of mcode.split('\n')) {
1455
- lines.push(` ${line}`);
1456
- }
1457
- }
1458
- lines.push(' }');
1459
- lines.push('');
1460
- }
1461
- lines.push('}');
1462
- return lines;
1463
- }
1464
- // ── Dependency ───────────────────────────────────────────────────────────
1465
- // dependency name=authService scope=singleton
1466
- // inject name=db from=database
1467
- // inject name=repo type=UserRepository with=db
1468
- // returns AuthService with=repo
1469
- export function generateDependency(node) {
1470
- const props = p(node);
1471
- const name = emitIdentifier(props.name, 'unknownDep', node);
1472
- const scope = props.scope || 'transient';
1473
- const exp = exportPrefix(node);
1474
- const lines = [];
1475
- const injects = kids(node, 'inject');
1476
- const returnsNode = firstChild(node, 'returns');
1477
- const returnsType = returnsNode ? (p(returnsNode).name || p(returnsNode).type || 'unknown') : 'unknown';
1478
- if (scope === 'singleton') {
1479
- lines.push(`let _${name}Instance: ${returnsType} | null = null;`);
1480
- lines.push('');
1481
- }
1482
- lines.push(`${exp}function create${name[0].toUpperCase()}${name.slice(1)}(): ${returnsType} {`);
1483
- if (scope === 'singleton') {
1484
- lines.push(` if (_${name}Instance) return _${name}Instance;`);
1485
- }
1486
- for (const inj of injects) {
1487
- const ip = p(inj);
1488
- const injName = emitIdentifier(ip.name, 'dep', inj);
1489
- const injType = ip.type;
1490
- const injFrom = ip.from;
1491
- const injWith = ip.with;
1492
- if (injFrom) {
1493
- lines.push(` const ${injName} = ${injFrom};`);
1494
- }
1495
- else if (injType && injWith) {
1496
- lines.push(` const ${injName} = new ${injType}(${injWith});`);
1497
- }
1498
- else if (injType) {
1499
- lines.push(` const ${injName} = new ${injType}();`);
1500
- }
1501
- }
1502
- const returnsWith = returnsNode ? p(returnsNode).with : undefined;
1503
- if (returnsWith) {
1504
- lines.push(` const instance = new ${returnsType}(${returnsWith});`);
1505
- }
1506
- else {
1507
- lines.push(` const instance = new ${returnsType}();`);
1508
- }
1509
- if (scope === 'singleton') {
1510
- lines.push(` _${name}Instance = instance;`);
1511
- }
1512
- lines.push(` return instance;`);
1513
- lines.push('}');
1514
- return lines;
1515
- }
1516
- // ── Cache ────────────────────────────────────────────────────────────────
1517
- // cache name=userCache backend=redis prefix="user:" ttl=3600
1518
- // entry name=profile key="user:{id}"
1519
- // strategy read-through
1520
- // invalidate on=userUpdate tags="user:{id}"
1521
- export function generateCache(node) {
1522
- const props = p(node);
1523
- const name = emitIdentifier(props.name, 'unknownCache', node);
1524
- const backend = props.backend || 'memory';
1525
- const prefix = props.prefix || '';
1526
- const ttl = props.ttl;
1527
- const exp = exportPrefix(node);
1528
- const lines = [];
1529
- lines.push(`${exp}const ${name} = {`);
1530
- lines.push(` prefix: '${prefix}',`);
1531
- if (ttl)
1532
- lines.push(` ttl: ${ttl},`);
1533
- lines.push(` backend: '${backend}',`);
1534
- lines.push('');
1535
- // Entry methods
1536
- for (const entry of kids(node, 'entry')) {
1537
- const ep = p(entry);
1538
- const entryName = emitIdentifier(ep.name, 'entry', entry);
1539
- const key = ep.key || entryName;
1540
- const strategyNode = firstChild(entry, 'strategy');
1541
- const strategy = strategyNode ? (p(strategyNode).name || 'cache-aside') : 'cache-aside';
1542
- lines.push(` async get${entryName[0].toUpperCase()}${entryName.slice(1)}(id: string) {`);
1543
- lines.push(` const key = \`${prefix}${key.replace(/\{id\}/g, '${id}')}\`;`);
1544
- if (strategy === 'read-through') {
1545
- lines.push(` // read-through: check cache, fetch if miss, populate cache`);
1546
- }
1547
- lines.push(` return ${backend === 'redis' ? `await redis.get(key)` : `cache.get(key)`};`);
1548
- lines.push(` },`);
1549
- lines.push('');
1550
- }
1551
- // Invalidation methods
1552
- for (const inv of kids(node, 'invalidate')) {
1553
- const ip = p(inv);
1554
- const on = ip.on || 'update';
1555
- const tags = ip.tags || '';
1556
- lines.push(` async invalidateOn${on[0].toUpperCase()}${on.slice(1)}(id: string) {`);
1557
- const invalidateKey = tags
1558
- ? `\`${prefix}${tags.replace(/\{id\}/g, '${id}')}\``
1559
- : `\`${prefix}\${id}\``;
1560
- lines.push(` const key = ${invalidateKey};`);
1561
- lines.push(` ${backend === 'redis' ? `await redis.del(key)` : `cache.delete(key)`};`);
1562
- lines.push(` },`);
1563
- lines.push('');
1564
- }
1565
- lines.push('} as const;');
1566
- return lines;
1567
- }
1568
248
  // ── Dispatcher ───────────────────────────────────────────────────────────
1569
249
  export const CORE_NODE_TYPES = new Set([
1570
250
  'type', 'interface', 'field', 'fn',
@@ -1603,7 +283,18 @@ export const CORE_NODE_TYPES = new Set([
1603
283
  export function isCoreNode(type) {
1604
284
  return CORE_NODE_TYPES.has(type);
1605
285
  }
1606
- /** Generate TypeScript for any core language node. */
286
+ /**
287
+ * Generate TypeScript lines for a core IR node (type system, functions, machines, etc.).
288
+ *
289
+ * Returns an empty array if the node type is unknown and no evolved generator is registered.
290
+ * Template nodes are expanded automatically via the template engine.
291
+ *
292
+ * @param node - The IR node to generate code for
293
+ * @param target - Optional target hint (e.g., `'ink'` for machine → useReducer)
294
+ * @param runtime - Optional KernRuntime instance
295
+ * @returns Array of TypeScript source lines
296
+ * @throws {KernCodegenError} For nodes with invalid/missing required props
297
+ */
1607
298
  export function generateCoreNode(node, target, runtime) {
1608
299
  const rt = runtime ?? defaultRuntime;
1609
300
  switch (node.type) {