@kernlang/core 2.0.0 → 3.1.0

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.
@@ -7,19 +7,66 @@
7
7
  * Machine nodes are KERN's killer feature: 12 lines of KERN → 140+ lines of TS.
8
8
  */
9
9
  import { isTemplateNode, expandTemplateNode } from './template-engine.js';
10
- // ── Helpers ──────────────────────────────────────────────────────────────
11
- function p(node) {
10
+ import { KernCodegenError } from './errors.js';
11
+ // ── Evolved Generators (v4) ─────────────────────────────────────────────
12
+ // Populated at startup by evolved-node-loader. Checked in generateCoreNode
13
+ // before the default case, allowing graduated nodes to produce output.
14
+ const _evolvedGenerators = new Map();
15
+ const _evolvedTargetGenerators = new Map();
16
+ /** Register an evolved generator (called at startup). */
17
+ export function registerEvolvedGenerator(keyword, fn) {
18
+ _evolvedGenerators.set(keyword, fn);
19
+ }
20
+ /** Register a target-specific evolved generator (called at startup). */
21
+ export function registerEvolvedTargetGenerator(keyword, target, fn) {
22
+ if (!_evolvedTargetGenerators.has(keyword)) {
23
+ _evolvedTargetGenerators.set(keyword, new Map());
24
+ }
25
+ _evolvedTargetGenerators.get(keyword).set(target, fn);
26
+ }
27
+ /** Unregister an evolved generator (for rollback/testing). */
28
+ export function unregisterEvolvedGenerator(keyword) {
29
+ _evolvedGenerators.delete(keyword);
30
+ _evolvedTargetGenerators.delete(keyword);
31
+ }
32
+ /** Clear all evolved generators (for test isolation). */
33
+ export function clearEvolvedGenerators() {
34
+ _evolvedGenerators.clear();
35
+ _evolvedTargetGenerators.clear();
36
+ }
37
+ /** Check if an evolved generator exists for a type. */
38
+ export function hasEvolvedGenerator(type) {
39
+ return _evolvedGenerators.has(type);
40
+ }
41
+ // ── Shared IR node helpers ───────────────────────────────────────────────
42
+ // These are used by every transpiler. Exported for reuse.
43
+ /** Extract props from an IR node. */
44
+ export function getProps(node) {
12
45
  return node.props || {};
13
46
  }
14
- function kids(node, type) {
47
+ /** Get children, optionally filtered by type. */
48
+ export function getChildren(node, type) {
15
49
  const c = node.children || [];
16
50
  return type ? c.filter(n => n.type === type) : c;
17
51
  }
18
- function firstChild(node, type) {
19
- return kids(node, type)[0];
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 || [];
20
67
  }
21
68
  /** Strip common leading whitespace from multiline handler code. */
22
- function dedent(code) {
69
+ export function dedent(code) {
23
70
  const lines = code.split('\n');
24
71
  const nonEmpty = lines.filter(l => l.trim().length > 0);
25
72
  if (nonEmpty.length === 0)
@@ -27,13 +74,22 @@ function dedent(code) {
27
74
  const min = Math.min(...nonEmpty.map(l => l.match(/^(\s*)/)?.[1].length ?? 0));
28
75
  return lines.map(l => l.slice(min)).join('\n');
29
76
  }
30
- function handlerCode(node) {
31
- const handler = firstChild(node, 'handler');
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');
32
84
  if (!handler)
33
85
  return '';
34
- const raw = p(handler).code || '';
86
+ const raw = getProps(handler).code || '';
35
87
  return dedent(raw);
36
88
  }
89
+ // Internal aliases for backward compat within this file
90
+ const p = getProps;
91
+ const kids = getChildren;
92
+ const firstChild = getFirstChild;
37
93
  function exportPrefix(node) {
38
94
  return p(node).export === 'false' ? '' : 'export ';
39
95
  }
@@ -73,6 +129,125 @@ export function generateInterface(node) {
73
129
  lines.push('}');
74
130
  return lines;
75
131
  }
132
+ // ── Discriminated Union ──────────────────────────────────────────────────
133
+ // union name=ContentSegment discriminant=type
134
+ // variant name=prose
135
+ // field name=text type=string
136
+ // variant name=code
137
+ // field name=language type=string
138
+ // field name=code type=string
139
+ // → export type ContentSegment =
140
+ // | { type: 'prose'; text: string }
141
+ // | { type: 'code'; language: string; code: string };
142
+ export function generateUnion(node) {
143
+ const props = p(node);
144
+ const name = props.name;
145
+ const discriminant = props.discriminant || 'type';
146
+ const exp = exportPrefix(node);
147
+ const variants = kids(node, 'variant');
148
+ if (variants.length === 0) {
149
+ return [`${exp}type ${name} = never;`];
150
+ }
151
+ const lines = [`${exp}type ${name} =`];
152
+ for (let i = 0; i < variants.length; i++) {
153
+ const vp = p(variants[i]);
154
+ const vname = vp.name;
155
+ const fields = kids(variants[i], 'field');
156
+ const fieldParts = [`${discriminant}: '${vname}'`];
157
+ for (const field of fields) {
158
+ const fp = p(field);
159
+ const opt = fp.optional === 'true' || fp.optional === true ? '?' : '';
160
+ fieldParts.push(`${fp.name}${opt}: ${fp.type}`);
161
+ }
162
+ const semi = i === variants.length - 1 ? ';' : '';
163
+ lines.push(` | { ${fieldParts.join('; ')} }${semi}`);
164
+ }
165
+ return lines;
166
+ }
167
+ // ── Service (Class) ─────────────────────────────────────────────────────
168
+ // service name=TokenTracker
169
+ // field name=entries type="TokenUsage[]" default="[]" private=true
170
+ // method name=record params="usage:TokenUsage" returns=void
171
+ // handler <<<
172
+ // this.entries.push(usage);
173
+ // >>>
174
+ // method name=getStats returns=SessionStats
175
+ // handler <<<
176
+ // return { ... };
177
+ // >>>
178
+ // singleton name=tracker type=TokenTracker
179
+ // → export class TokenTracker { ... }
180
+ // → export const tracker = new TokenTracker();
181
+ export function generateService(node) {
182
+ const props = p(node);
183
+ const name = props.name;
184
+ const impl = props.implements;
185
+ const exp = exportPrefix(node);
186
+ const lines = [];
187
+ const implClause = impl ? ` implements ${impl}` : '';
188
+ lines.push(`${exp}class ${name}${implClause} {`);
189
+ // Fields
190
+ for (const field of kids(node, 'field')) {
191
+ const fp = p(field);
192
+ const vis = fp.private === 'true' || fp.private === true ? 'private ' : '';
193
+ const readonly = fp.readonly === 'true' || fp.readonly === true ? 'readonly ' : '';
194
+ const typeAnnotation = fp.type ? `: ${fp.type}` : '';
195
+ const defaultVal = fp.default;
196
+ const init = defaultVal !== undefined ? ` = ${defaultVal}` : '';
197
+ lines.push(` ${vis}${readonly}${fp.name}${typeAnnotation}${init};`);
198
+ }
199
+ // Constructor (if any constructor child exists)
200
+ const ctorNode = firstChild(node, 'constructor');
201
+ if (ctorNode) {
202
+ const ctorProps = p(ctorNode);
203
+ const ctorParams = ctorProps.params ? parseParamList(ctorProps.params) : '';
204
+ const ctorCode = handlerCode(ctorNode);
205
+ lines.push('');
206
+ lines.push(` constructor(${ctorParams}) {`);
207
+ if (ctorCode) {
208
+ for (const line of ctorCode.split('\n')) {
209
+ lines.push(` ${line}`);
210
+ }
211
+ }
212
+ lines.push(' }');
213
+ }
214
+ // Methods
215
+ for (const method of kids(node, 'method')) {
216
+ const mp = p(method);
217
+ const mname = mp.name;
218
+ const mparams = mp.params ? parseParamList(mp.params) : '';
219
+ const isAsync = mp.async === 'true' || mp.async === true;
220
+ const isStream = mp.stream === 'true' || mp.stream === true;
221
+ const isStatic = mp.static === 'true' || mp.static === true;
222
+ const vis = mp.private === 'true' || mp.private === true ? 'private ' : '';
223
+ const staticKw = isStatic ? 'static ' : '';
224
+ const star = isStream ? '*' : '';
225
+ const asyncKw = (isAsync || isStream) ? 'async ' : '';
226
+ const mcode = handlerCode(method);
227
+ // stream=true → AsyncGenerator return type
228
+ const mreturns = isStream
229
+ ? `: AsyncGenerator<${mp.returns || 'unknown'}>`
230
+ : mp.returns ? `: ${mp.returns}` : '';
231
+ lines.push('');
232
+ lines.push(` ${vis}${staticKw}${asyncKw}${star}${mname}(${mparams})${mreturns} {`);
233
+ if (mcode) {
234
+ for (const line of mcode.split('\n')) {
235
+ lines.push(` ${line}`);
236
+ }
237
+ }
238
+ lines.push(' }');
239
+ }
240
+ lines.push('}');
241
+ // Singleton instances
242
+ for (const singleton of kids(node, 'singleton')) {
243
+ const sp = p(singleton);
244
+ const sname = sp.name;
245
+ const stype = sp.type || name;
246
+ lines.push('');
247
+ lines.push(`${exp}const ${sname} = new ${stype}();`);
248
+ }
249
+ return lines;
250
+ }
76
251
  // ── Function ─────────────────────────────────────────────────────────────
77
252
  // fn name=createPlan params="action:PlanAction,ws:WorkspaceSnapshot" returns=Plan
78
253
  // handler <<<
@@ -84,20 +259,59 @@ export function generateFunction(node) {
84
259
  const params = props.params || '';
85
260
  const returns = props.returns;
86
261
  const isAsync = props.async === 'true' || props.async === true;
262
+ const isStream = props.stream === 'true' || props.stream === true;
87
263
  const exp = exportPrefix(node);
88
264
  const lines = [];
89
- // Parse params: "action:PlanAction,ws:WorkspaceSnapshot" → "action: PlanAction, ws: WorkspaceSnapshot"
90
- const paramList = params
91
- ? params.split(',').map(s => {
92
- const [pname, ...ptype] = s.split(':').map(t => t.trim());
93
- return ptype.length > 0 ? `${pname}: ${ptype.join(':')}` : pname;
94
- }).join(', ')
95
- : '';
265
+ // Parse params: "action:PlanAction,ws:WorkspaceSnapshot,spread:number=8"
266
+ // "action: PlanAction, ws: WorkspaceSnapshot, spread: number = 8"
267
+ const paramList = params ? parseParamList(params) : '';
268
+ // stream=true async generator function
269
+ if (isStream) {
270
+ const yieldType = returns || 'unknown';
271
+ const retClause = `: AsyncGenerator<${yieldType}>`;
272
+ const code = handlerCode(node);
273
+ lines.push(`${exp}async function* ${name}(${paramList})${retClause} {`);
274
+ if (code) {
275
+ for (const line of code.split('\n')) {
276
+ lines.push(` ${line}`);
277
+ }
278
+ }
279
+ lines.push('}');
280
+ return lines;
281
+ }
96
282
  const retClause = returns ? `: ${returns}` : '';
97
283
  const asyncKw = isAsync ? 'async ' : '';
98
284
  const code = handlerCode(node);
285
+ // Gap 3: signal + cleanup support for async functions
286
+ const signalNode = firstChild(node, 'signal');
287
+ const cleanupNode = firstChild(node, 'cleanup');
288
+ const hasSignal = !!signalNode;
289
+ const hasCleanup = !!cleanupNode;
99
290
  lines.push(`${exp}${asyncKw}function ${name}(${paramList})${retClause} {`);
100
- if (code) {
291
+ // Signal → AbortController setup
292
+ if (hasSignal) {
293
+ const signalName = p(signalNode).name || 'abort';
294
+ lines.push(` const ${signalName} = new AbortController();`);
295
+ }
296
+ // Wrap body in try/finally if cleanup exists
297
+ if (hasCleanup) {
298
+ lines.push(' try {');
299
+ if (code) {
300
+ for (const line of code.split('\n')) {
301
+ lines.push(` ${line}`);
302
+ }
303
+ }
304
+ lines.push(' } finally {');
305
+ const cleanupCode = p(cleanupNode).code || '';
306
+ if (cleanupCode) {
307
+ const dedented = dedent(cleanupCode);
308
+ for (const line of dedented.split('\n')) {
309
+ lines.push(` ${line}`);
310
+ }
311
+ }
312
+ lines.push(' }');
313
+ }
314
+ else if (code) {
101
315
  for (const line of code.split('\n')) {
102
316
  lines.push(` ${line}`);
103
317
  }
@@ -261,6 +475,52 @@ export function generateMachine(node) {
261
475
  }
262
476
  return lines;
263
477
  }
478
+ // ── Machine → useReducer (Ink target) ────────────────────────────────────
479
+ // Additive: also emit a React useReducer hook wrapping the transition functions.
480
+ // Called by transpiler-ink.ts when target=ink.
481
+ export function generateMachineReducer(node) {
482
+ const props = p(node);
483
+ const name = props.name;
484
+ const exp = exportPrefix(node);
485
+ const lines = [];
486
+ // First emit the standard machine output
487
+ lines.push(...generateMachine(node));
488
+ // Collect states + transitions
489
+ const states = kids(node, 'state');
490
+ const stateNames = states.map(s => {
491
+ const sp = p(s);
492
+ return (sp.name || sp.value);
493
+ });
494
+ const initialState = states.find(s => p(s).initial === 'true' || p(s).initial === true);
495
+ const initialName = initialState ? (p(initialState).name || p(initialState).value) : stateNames[0];
496
+ const transitions = kids(node, 'transition');
497
+ const stateType = `${name}State`;
498
+ // Action type union
499
+ const actionNames = transitions.map(t => p(t).name);
500
+ lines.push(`${exp}type ${name}Action = ${actionNames.map(a => `'${a}'`).join(' | ')};`);
501
+ lines.push('');
502
+ // Reducer function
503
+ lines.push(`${exp}function ${name.charAt(0).toLowerCase() + name.slice(1)}Reducer(state: ${stateType}, action: ${name}Action): ${stateType} {`);
504
+ lines.push(` const entity = { state };`);
505
+ lines.push(` switch (action) {`);
506
+ for (const t of transitions) {
507
+ const tp = p(t);
508
+ const tname = tp.name;
509
+ const fnName = `${tname}${name}`;
510
+ lines.push(` case '${tname}': return ${fnName}(entity).state;`);
511
+ }
512
+ lines.push(` default: return state;`);
513
+ lines.push(` }`);
514
+ lines.push('}');
515
+ lines.push('');
516
+ // useReducer hook
517
+ lines.push(`${exp}function use${name}Reducer() {`);
518
+ lines.push(` const [state, dispatch] = useReducer(${name.charAt(0).toLowerCase() + name.slice(1)}Reducer, '${initialName}' as ${stateType});`);
519
+ lines.push(` return { state, dispatch } as const;`);
520
+ lines.push('}');
521
+ lines.push('');
522
+ return lines;
523
+ }
264
524
  // ── Config ───────────────────────────────────────────────────────────────
265
525
  // config name=AgonConfig
266
526
  // field name=timeout type=number default=120
@@ -447,6 +707,147 @@ export function generateEvent(node) {
447
707
  lines.push(`${exp}type ${name}Callback = (event: ${name}) => void;`);
448
708
  return lines;
449
709
  }
710
+ // ── On — generic event handler ────────────────────────────────────────────
711
+ // on event=click handler=handleClick
712
+ // on event=submit
713
+ // handler <<<
714
+ // e.preventDefault();
715
+ // await submitForm(data);
716
+ // >>>
717
+ // on event=key key=Enter
718
+ // handler <<<
719
+ // processInput(buffer);
720
+ // >>>
721
+ // on event=message
722
+ // handler <<<
723
+ // const data = JSON.parse(event.data);
724
+ // dispatch(data);
725
+ // >>>
726
+ export function generateOn(node) {
727
+ const props = p(node);
728
+ const event = (props.event || props.name);
729
+ const handlerName = props.handler;
730
+ const key = props.key;
731
+ const code = handlerCode(node);
732
+ const exp = exportPrefix(node);
733
+ const lines = [];
734
+ if (handlerName && !code) {
735
+ // Reference to existing handler: on event=click handler=handleClick
736
+ lines.push(`${exp}const on${capitalize(event)} = ${handlerName};`);
737
+ return lines;
738
+ }
739
+ // Determine event parameter type (plain DOM types — target-agnostic)
740
+ const paramType = event === 'key' || event === 'keydown' || event === 'keyup' ? 'e: KeyboardEvent'
741
+ : event === 'message' ? 'event: MessageEvent'
742
+ : event === 'submit' ? 'e: SubmitEvent'
743
+ : event === 'click' ? 'e: MouseEvent'
744
+ : event === 'change' ? 'e: Event'
745
+ : event === 'focus' || event === 'blur' ? 'e: FocusEvent'
746
+ : event === 'drag' || event === 'drop' ? 'e: DragEvent'
747
+ : event === 'scroll' ? 'e: Event'
748
+ : event === 'resize' ? 'e: UIEvent'
749
+ : event === 'connect' || event === 'disconnect' ? 'ws: WebSocket'
750
+ : event === 'error' ? 'error: Error'
751
+ : `e: Event`;
752
+ const fnName = handlerName || `handle${capitalize(event)}`;
753
+ const isAsync = props.async === 'true' || props.async === true;
754
+ const asyncKw = isAsync ? 'async ' : '';
755
+ // Key filter guard
756
+ const keyGuard = key ? ` if (key !== '${key}') return;\n` : '';
757
+ lines.push(`${exp}${asyncKw}function ${fnName}(${paramType}) {`);
758
+ if (keyGuard)
759
+ lines.push(keyGuard.trimEnd());
760
+ if (code) {
761
+ for (const line of code.split('\n')) {
762
+ lines.push(` ${line}`);
763
+ }
764
+ }
765
+ lines.push('}');
766
+ return lines;
767
+ }
768
+ // ── WebSocket — bidirectional communication ──────────────────────────────
769
+ // websocket path=/ws
770
+ // on event=connect
771
+ // handler <<<ws.send(JSON.stringify({ type: 'hello' }));>>>
772
+ // on event=message
773
+ // handler <<<
774
+ // const data = JSON.parse(event.data);
775
+ // broadcast(data);
776
+ // >>>
777
+ // on event=disconnect
778
+ // handler <<<console.log('client disconnected');>>>
779
+ export function generateWebSocket(node) {
780
+ const props = p(node);
781
+ const path = (props.path || '/ws');
782
+ const name = props.name || 'ws';
783
+ const exp = exportPrefix(node);
784
+ const lines = [];
785
+ const onNodes = kids(node, 'on');
786
+ const connectHandler = onNodes.find(n => {
787
+ const e = (p(n).event || p(n).name);
788
+ return e === 'connect' || e === 'connection';
789
+ });
790
+ const messageHandler = onNodes.find(n => {
791
+ const e = (p(n).event || p(n).name);
792
+ return e === 'message';
793
+ });
794
+ const disconnectHandler = onNodes.find(n => {
795
+ const e = (p(n).event || p(n).name);
796
+ return e === 'disconnect' || e === 'close';
797
+ });
798
+ const errorHandler = onNodes.find(n => {
799
+ const e = (p(n).event || p(n).name);
800
+ return e === 'error';
801
+ });
802
+ lines.push(`${exp}function setup${capitalize(name)}(wss: WebSocketServer) {`);
803
+ lines.push(` wss.on('connection', (ws, req) => {`);
804
+ lines.push(` const path = req.url || '${path}';`);
805
+ if (connectHandler) {
806
+ const code = handlerCode(connectHandler);
807
+ if (code) {
808
+ for (const line of code.split('\n')) {
809
+ lines.push(` ${line}`);
810
+ }
811
+ }
812
+ }
813
+ lines.push('');
814
+ lines.push(` ws.on('message', (raw) => {`);
815
+ if (messageHandler) {
816
+ const code = handlerCode(messageHandler);
817
+ lines.push(` const data = JSON.parse(raw.toString());`);
818
+ if (code) {
819
+ for (const line of code.split('\n')) {
820
+ lines.push(` ${line}`);
821
+ }
822
+ }
823
+ }
824
+ lines.push(` });`);
825
+ if (errorHandler) {
826
+ lines.push('');
827
+ lines.push(` ws.on('error', (error) => {`);
828
+ const code = handlerCode(errorHandler);
829
+ if (code) {
830
+ for (const line of code.split('\n')) {
831
+ lines.push(` ${line}`);
832
+ }
833
+ }
834
+ lines.push(` });`);
835
+ }
836
+ lines.push('');
837
+ lines.push(` ws.on('close', () => {`);
838
+ if (disconnectHandler) {
839
+ const code = handlerCode(disconnectHandler);
840
+ if (code) {
841
+ for (const line of code.split('\n')) {
842
+ lines.push(` ${line}`);
843
+ }
844
+ }
845
+ }
846
+ lines.push(` });`);
847
+ lines.push(` });`);
848
+ lines.push('}');
849
+ return lines;
850
+ }
450
851
  // ── Module ───────────────────────────────────────────────────────────────
451
852
  // module name=@agon/core
452
853
  // export from="./plan.js" names="createPlan,advanceStep"
@@ -462,15 +863,35 @@ export function generateModule(node) {
462
863
  const names = ep.names;
463
864
  const typeNames = ep.types;
464
865
  const star = ep.star === 'true' || ep.star === true;
866
+ const defaultExport = ep.default;
867
+ // export * from './foo.js'
465
868
  if (from && !names && !typeNames && star) {
466
869
  lines.push(`export * from '${from}';`);
467
870
  }
871
+ // export { a, b } from './foo.js'
468
872
  if (from && names) {
469
873
  lines.push(`export { ${names.split(',').map(s => s.trim()).join(', ')} } from '${from}';`);
470
874
  }
875
+ // export type { A, B } from './types.js'
471
876
  if (from && typeNames) {
472
877
  lines.push(`export type { ${typeNames.split(',').map(s => s.trim()).join(', ')} } from '${from}';`);
473
878
  }
879
+ // export default foo
880
+ if (defaultExport && !from) {
881
+ lines.push(`export default ${defaultExport};`);
882
+ }
883
+ // export default from './foo.js' (re-export default)
884
+ if (defaultExport && from) {
885
+ lines.push(`export { default as ${defaultExport} } from '${from}';`);
886
+ }
887
+ // export { a, b } (no from — local re-export)
888
+ if (!from && names && !defaultExport) {
889
+ lines.push(`export { ${names.split(',').map(s => s.trim()).join(', ')} };`);
890
+ }
891
+ // export type { A, B } (no from — local type re-export)
892
+ if (!from && typeNames && !defaultExport) {
893
+ lines.push(`export type { ${typeNames.split(',').map(s => s.trim()).join(', ')} };`);
894
+ }
474
895
  }
475
896
  // Inline child definitions
476
897
  for (const child of kids(node)) {
@@ -543,15 +964,72 @@ export function generateConst(node) {
543
964
  return [`${exp}const ${name}${typeAnnotation};`];
544
965
  }
545
966
  // ── Shared Helpers (exported for @kernlang/react) ────────────────────────────
546
- /** Parse "name:Type,name2:Type2" → "name: Type, name2: Type2" */
967
+ /** Parse "name:Type,name2:Type2,spread:number=8" → "name: Type, name2: Type2, spread: number = 8"
968
+ * Supports default values via = after the type. */
547
969
  export function parseParamList(params) {
548
970
  if (!params)
549
971
  return '';
550
- return params.split(',').map(s => {
551
- const [pname, ...ptype] = s.split(':').map(t => t.trim());
552
- return ptype.length > 0 ? `${pname}: ${ptype.join(':')}` : pname;
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}`;
553
988
  }).join(', ');
554
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
+ }
555
1033
  export function capitalize(s) {
556
1034
  return s.charAt(0).toUpperCase() + s.slice(1);
557
1035
  }
@@ -575,7 +1053,7 @@ export function capitalize(s) {
575
1053
  // handler <<<
576
1054
  // trackSearch(query);
577
1055
  // >>>
578
- // returns names="products:data?.products,isLoading,handleFilter,cacheKey"
1056
+ // returns names="articles:data?.articles,isLoading,handleFilter,cacheKey"
579
1057
  export function generateHook(node) {
580
1058
  const props = p(node);
581
1059
  const name = props.name;
@@ -706,26 +1184,722 @@ export function generateHook(node) {
706
1184
  }
707
1185
  return lines;
708
1186
  }
1187
+ // ── Reason & Confidence Annotations ──────────────────────────────────────
1188
+ export function emitReasonAnnotations(node) {
1189
+ const reasonNode = firstChild(node, 'reason');
1190
+ const evidenceNode = firstChild(node, 'evidence');
1191
+ const needsNodes = kids(node, 'needs');
1192
+ const confidence = p(node).confidence;
1193
+ if (!reasonNode && !evidenceNode && !confidence && needsNodes.length === 0)
1194
+ return [];
1195
+ const lines = ['/**'];
1196
+ if (confidence)
1197
+ lines.push(` * @confidence ${confidence}`);
1198
+ if (reasonNode) {
1199
+ const rp = p(reasonNode);
1200
+ lines.push(` * @reason ${rp.because || ''}`);
1201
+ if (rp.basis)
1202
+ lines.push(` * @basis ${rp.basis}`);
1203
+ if (rp.survives)
1204
+ lines.push(` * @survives ${rp.survives}`);
1205
+ }
1206
+ if (evidenceNode) {
1207
+ const ep = p(evidenceNode);
1208
+ const parts = [`source=${ep.source}`];
1209
+ if (ep.method)
1210
+ parts.push(`method=${ep.method}`);
1211
+ if (ep.authority)
1212
+ parts.push(`authority=${ep.authority}`);
1213
+ lines.push(` * @evidence ${parts.join(', ')}`);
1214
+ }
1215
+ for (const needsNode of needsNodes) {
1216
+ const np = p(needsNode);
1217
+ const desc = np.what || np.description || '';
1218
+ const wouldRaise = np['would-raise-to'];
1219
+ const tag = wouldRaise ? `${desc} (would raise to ${wouldRaise})` : desc;
1220
+ lines.push(` * @needs ${tag}`);
1221
+ }
1222
+ lines.push(' */');
1223
+ return lines;
1224
+ }
1225
+ /** Emit a TODO comment for nodes with low literal confidence (< 0.5). */
1226
+ export function emitLowConfidenceTodo(node, confidence) {
1227
+ if (!confidence)
1228
+ return [];
1229
+ const val = parseFloat(confidence);
1230
+ if (isNaN(val) || val >= 0.5 || confidence.includes(':'))
1231
+ return [];
1232
+ const name = p(node).name || node.type;
1233
+ return [`// TODO(low-confidence): ${name} confidence=${confidence}`];
1234
+ }
1235
+ // ── Ground Layer: derive ─────────────────────────────────────────────────
1236
+ // derive name=loudness expr={{average(stems)}} type=number deps="stems"
1237
+ // → export const loudness: number = average(stems);
1238
+ export function generateDerive(node) {
1239
+ const annotations = emitReasonAnnotations(node);
1240
+ const conf = p(node).confidence;
1241
+ const todo = emitLowConfidenceTodo(node, conf);
1242
+ const props = p(node);
1243
+ const name = props.name;
1244
+ const expr = props.expr;
1245
+ const constType = props.type;
1246
+ const exp = exportPrefix(node);
1247
+ const typeAnnotation = constType ? `: ${constType}` : '';
1248
+ return [...todo, ...annotations, `${exp}const ${name}${typeAnnotation} = ${expr};`];
1249
+ }
1250
+ // ── Ground Layer: transform ──────────────────────────────────────────────
1251
+ // transform name=limitStems target="track.stems" via="limit(4)" type="Stem[]"
1252
+ // → export const limitStems: Stem[] = limit(track.stems, 4);
1253
+ export function generateTransform(node) {
1254
+ const annotations = emitReasonAnnotations(node);
1255
+ const conf = p(node).confidence;
1256
+ const todo = emitLowConfidenceTodo(node, conf);
1257
+ const props = p(node);
1258
+ const name = props.name;
1259
+ const target = props.target;
1260
+ const via = props.via;
1261
+ const constType = props.type;
1262
+ const exp = exportPrefix(node);
1263
+ const code = handlerCode(node);
1264
+ const typeAnnotation = constType ? `: ${constType}` : '';
1265
+ if (code) {
1266
+ // Handler block form — generate a function
1267
+ const lines = [...todo, ...annotations];
1268
+ lines.push(`${exp}function ${name}(state: unknown)${typeAnnotation} {`);
1269
+ for (const line of code.split('\n')) {
1270
+ lines.push(` ${line}`);
1271
+ }
1272
+ lines.push('}');
1273
+ return lines;
1274
+ }
1275
+ if (target && via) {
1276
+ return [...todo, ...annotations, `${exp}const ${name}${typeAnnotation} = ${via.replace(/\(/, `(${target}, `).replace(/, \)/, ')')};`];
1277
+ }
1278
+ if (via) {
1279
+ return [...todo, ...annotations, `${exp}const ${name}${typeAnnotation} = ${via};`];
1280
+ }
1281
+ return [...todo, ...annotations, `${exp}const ${name}${typeAnnotation};`];
1282
+ }
1283
+ // ── Ground Layer: action ─────────────────────────────────────────────────
1284
+ // action name=notifyOwner idempotent=true reversible=true
1285
+ // handler <<<await email.send(track.owner, 'processed');>>>
1286
+ export function generateAction(node) {
1287
+ const annotations = emitReasonAnnotations(node);
1288
+ const conf = p(node).confidence;
1289
+ const todo = emitLowConfidenceTodo(node, conf);
1290
+ const props = p(node);
1291
+ const name = props.name;
1292
+ const idempotent = props.idempotent === 'true' || props.idempotent === true;
1293
+ const reversible = props.reversible === 'true' || props.reversible === true;
1294
+ const params = props.params || '';
1295
+ const returns = props.returns;
1296
+ const exp = exportPrefix(node);
1297
+ const code = handlerCode(node);
1298
+ const lines = [...todo, ...annotations];
1299
+ // JSDoc for action metadata
1300
+ const metaParts = [];
1301
+ if (idempotent)
1302
+ metaParts.push('idempotent=true');
1303
+ if (reversible)
1304
+ metaParts.push('reversible=true');
1305
+ if (metaParts.length > 0) {
1306
+ lines.push(`/** @action ${metaParts.join(' ')} */`);
1307
+ }
1308
+ const paramList = params ? parseParamList(params) : '';
1309
+ const retClause = returns ? `: Promise<${returns}>` : ': Promise<void>';
1310
+ lines.push(`${exp}async function ${name}(${paramList})${retClause} {`);
1311
+ if (code) {
1312
+ for (const line of code.split('\n')) {
1313
+ lines.push(` ${line}`);
1314
+ }
1315
+ }
1316
+ lines.push('}');
1317
+ return lines;
1318
+ }
1319
+ // ── Ground Layer: guard (extended — already in NODE_TYPES) ───────────────
1320
+ // guard name=published expr={{track.status == "published"}} else=403
1321
+ export function generateGuard(node) {
1322
+ const annotations = emitReasonAnnotations(node);
1323
+ const conf = p(node).confidence;
1324
+ const todo = emitLowConfidenceTodo(node, conf);
1325
+ const props = p(node);
1326
+ const name = props.name || 'guard';
1327
+ const expr = props.expr;
1328
+ const elseCode = props.else;
1329
+ const lines = [...todo, ...annotations];
1330
+ if (elseCode && /^\d+$/.test(elseCode)) {
1331
+ lines.push(`if (!(${expr})) { throw new HttpError(${elseCode}, 'Guard: ${name}'); }`);
1332
+ }
1333
+ else if (elseCode) {
1334
+ lines.push(`if (!(${expr})) { ${elseCode}; }`);
1335
+ }
1336
+ else {
1337
+ lines.push(`if (!(${expr})) { throw new Error('Guard failed: ${name}'); }`);
1338
+ }
1339
+ return lines;
1340
+ }
1341
+ // ── Ground Layer: assume ─────────────────────────────────────────────────
1342
+ // assume expr={{track.owner == $auth.user}} scope=request evidence="route-signing" fallback="throw AuthError()"
1343
+ export function generateAssume(node) {
1344
+ const annotations = emitReasonAnnotations(node);
1345
+ const conf = p(node).confidence;
1346
+ const todo = emitLowConfidenceTodo(node, conf);
1347
+ const props = p(node);
1348
+ const expr = props.expr;
1349
+ const scope = props.scope || 'request';
1350
+ const evidence = props.evidence;
1351
+ const fallback = props.fallback;
1352
+ if (!evidence)
1353
+ throw new KernCodegenError('assume requires evidence prop', node);
1354
+ if (!fallback)
1355
+ throw new KernCodegenError('assume requires fallback prop', node);
1356
+ const lines = [...todo, ...annotations];
1357
+ lines.push(`/** @assume ${expr} @scope ${scope} @evidence ${evidence} */`);
1358
+ lines.push(`if (process.env.NODE_ENV !== 'production') {`);
1359
+ lines.push(` if (!(${expr})) { ${fallback}; }`);
1360
+ lines.push(`}`);
1361
+ return lines;
1362
+ }
1363
+ // ── Ground Layer: invariant ──────────────────────────────────────────────
1364
+ // invariant name=stemLimit expr={{visible_stems <= policy.max_stems($auth.tier)}}
1365
+ export function generateInvariant(node) {
1366
+ const annotations = emitReasonAnnotations(node);
1367
+ const conf = p(node).confidence;
1368
+ const todo = emitLowConfidenceTodo(node, conf);
1369
+ const props = p(node);
1370
+ const name = props.name || 'invariant';
1371
+ const expr = props.expr;
1372
+ const lines = [...todo, ...annotations];
1373
+ lines.push(`console.assert(${expr}, 'Invariant: ${name}');`);
1374
+ return lines;
1375
+ }
1376
+ // ── Ground Layer: each (ground-layer, not inside parallel) ───────────────
1377
+ // each name=stem in="track.stems"
1378
+ // derive name=normalized expr={{normalize(stem.amplitude)}}
1379
+ export function generateEach(node) {
1380
+ const annotations = emitReasonAnnotations(node);
1381
+ const conf = p(node).confidence;
1382
+ const todo = emitLowConfidenceTodo(node, conf);
1383
+ const props = p(node);
1384
+ const name = props.name || 'item';
1385
+ const collection = props.in;
1386
+ const index = props.index;
1387
+ const lines = [...todo, ...annotations];
1388
+ if (index) {
1389
+ lines.push(`for (const [${index}, ${name}] of (${collection}).entries()) {`);
1390
+ }
1391
+ else {
1392
+ lines.push(`for (const ${name} of ${collection}) {`);
1393
+ }
1394
+ for (const child of kids(node)) {
1395
+ const childLines = generateCoreNode(child);
1396
+ for (const line of childLines) {
1397
+ lines.push(` ${line}`);
1398
+ }
1399
+ }
1400
+ lines.push('}');
1401
+ return lines;
1402
+ }
1403
+ // ── Ground Layer: collect ────────────────────────────────────────────────
1404
+ // collect name=overThreshold from="track.stems" where={{measure(stem.loudness) > threshold}} limit=10
1405
+ export function generateCollect(node) {
1406
+ const annotations = emitReasonAnnotations(node);
1407
+ const conf = p(node).confidence;
1408
+ const todo = emitLowConfidenceTodo(node, conf);
1409
+ const props = p(node);
1410
+ const name = props.name;
1411
+ const from = props.from;
1412
+ const where = props.where;
1413
+ const limit = props.limit;
1414
+ const order = props.order;
1415
+ const exp = exportPrefix(node);
1416
+ let chain = from;
1417
+ if (where)
1418
+ chain += `.filter(item => ${where})`;
1419
+ if (order)
1420
+ chain += `.sort((a, b) => ${order})`;
1421
+ if (limit)
1422
+ chain += `.slice(0, ${limit})`;
1423
+ return [...todo, ...annotations, `${exp}const ${name} = ${chain};`];
1424
+ }
1425
+ // ── Ground Layer: branch / path ──────────────────────────────────────────
1426
+ // branch name=tierRoute on="user.tier"
1427
+ // path value="free"
1428
+ // derive name=maxStems expr={{4}} type=number
1429
+ export function generateBranch(node) {
1430
+ const annotations = emitReasonAnnotations(node);
1431
+ const conf = p(node).confidence;
1432
+ const todo = emitLowConfidenceTodo(node, conf);
1433
+ const props = p(node);
1434
+ const name = props.name || 'branch';
1435
+ const on = props.on;
1436
+ const paths = kids(node, 'path');
1437
+ const lines = [...todo, ...annotations];
1438
+ lines.push(`/** branch: ${name} */`);
1439
+ lines.push(`switch (${on}) {`);
1440
+ for (const pathNode of paths) {
1441
+ const pp = p(pathNode);
1442
+ const value = pp.value;
1443
+ lines.push(` case '${value}': {`);
1444
+ for (const child of kids(pathNode)) {
1445
+ const childLines = generateCoreNode(child);
1446
+ for (const line of childLines) {
1447
+ lines.push(` ${line}`);
1448
+ }
1449
+ }
1450
+ lines.push(` break;`);
1451
+ lines.push(` }`);
1452
+ }
1453
+ lines.push('}');
1454
+ return lines;
1455
+ }
1456
+ // ── Ground Layer: resolve / candidate / discriminator ────────────────────
1457
+ // resolve name=normStrategy
1458
+ // candidate name=aggressive
1459
+ // handler <<<return aggressiveNormalize(signal);>>>
1460
+ // discriminator method=benchmark metric="snr"
1461
+ // handler <<<...>>>
1462
+ export function generateResolve(node) {
1463
+ const annotations = emitReasonAnnotations(node);
1464
+ const conf = p(node).confidence;
1465
+ const todo = emitLowConfidenceTodo(node, conf);
1466
+ const props = p(node);
1467
+ const name = props.name;
1468
+ const candidates = kids(node, 'candidate');
1469
+ const discriminator = firstChild(node, 'discriminator');
1470
+ if (!discriminator)
1471
+ throw new KernCodegenError('resolve requires discriminator', node);
1472
+ const lines = [...todo, ...annotations];
1473
+ const dp = p(discriminator);
1474
+ const method = dp.method || 'select';
1475
+ const metric = dp.metric || '';
1476
+ // Candidate array
1477
+ lines.push(`/** resolve: ${name} */`);
1478
+ lines.push(`const _${name}_candidates = [`);
1479
+ for (const c of candidates) {
1480
+ const cp = p(c);
1481
+ const cname = cp.name;
1482
+ const code = handlerCode(c);
1483
+ lines.push(` { name: '${cname}', fn: (signal: unknown) => { ${code.trim()} } },`);
1484
+ }
1485
+ lines.push(`];`);
1486
+ lines.push('');
1487
+ // Resolver function
1488
+ const discCode = handlerCode(discriminator);
1489
+ lines.push(`async function resolve${capitalize(name)}(signal: unknown): Promise<unknown> {`);
1490
+ lines.push(` const candidates = _${name}_candidates;`);
1491
+ lines.push(` // discriminator: ${method}(${metric})`);
1492
+ if (discCode) {
1493
+ for (const line of discCode.split('\n')) {
1494
+ lines.push(` ${line}`);
1495
+ }
1496
+ }
1497
+ lines.push(` return candidates[winnerIdx].fn(signal);`);
1498
+ lines.push('}');
1499
+ return lines;
1500
+ }
1501
+ // ── Ground Layer: expect ─────────────────────────────────────────────────
1502
+ // expect name=clipRate expr={{clip_flags_rate}} within="0.02..0.08"
1503
+ export function generateExpect(node) {
1504
+ const annotations = emitReasonAnnotations(node);
1505
+ const conf = p(node).confidence;
1506
+ const todo = emitLowConfidenceTodo(node, conf);
1507
+ const props = p(node);
1508
+ const name = props.name || 'expected';
1509
+ const expr = props.expr;
1510
+ const within = props.within;
1511
+ const max = props.max;
1512
+ const min = props.min;
1513
+ const lines = [...todo, ...annotations];
1514
+ lines.push(`if (process.env.NODE_ENV !== 'production') {`);
1515
+ lines.push(` const _${name} = ${expr};`);
1516
+ if (within) {
1517
+ const [lo, hi] = within.split('..');
1518
+ lines.push(` console.assert(_${name} >= ${lo} && _${name} <= ${hi}, 'Expected ${name} in [${lo}, ${hi}], got ' + _${name});`);
1519
+ }
1520
+ else if (min && max) {
1521
+ lines.push(` console.assert(_${name} >= ${min} && _${name} <= ${max}, 'Expected ${name} in [${min}, ${max}], got ' + _${name});`);
1522
+ }
1523
+ else if (max) {
1524
+ lines.push(` console.assert(_${name} <= ${max}, 'Expected ${name} <= ${max}, got ' + _${name});`);
1525
+ }
1526
+ else if (min) {
1527
+ lines.push(` console.assert(_${name} >= ${min}, 'Expected ${name} >= ${min}, got ' + _${name});`);
1528
+ }
1529
+ else {
1530
+ lines.push(` console.assert(_${name} != null, 'Expected ${name} to be defined');`);
1531
+ }
1532
+ lines.push('}');
1533
+ return lines;
1534
+ }
1535
+ // ── Ground Layer: recover / strategy ─────────────────────────────────────
1536
+ // recover name=paymentFlow
1537
+ // strategy name=retry max=3 delay=1000
1538
+ // strategy name=fallback
1539
+ // handler <<<throw new PaymentError('All recovery exhausted');>>>
1540
+ export function generateRecover(node) {
1541
+ const annotations = emitReasonAnnotations(node);
1542
+ const conf = p(node).confidence;
1543
+ const todo = emitLowConfidenceTodo(node, conf);
1544
+ const props = p(node);
1545
+ const name = props.name;
1546
+ const strategies = kids(node, 'strategy');
1547
+ const hasFallback = strategies.some(s => p(s).name === 'fallback');
1548
+ if (!hasFallback)
1549
+ throw new KernCodegenError('recover requires a fallback strategy', node);
1550
+ const lines = [...todo, ...annotations];
1551
+ lines.push(`/** recover: ${name} */`);
1552
+ lines.push(`async function ${name}WithRecovery<T>(fn: () => Promise<T>): Promise<T> {`);
1553
+ for (const strategy of strategies) {
1554
+ const sp = p(strategy);
1555
+ const sname = sp.name;
1556
+ const code = handlerCode(strategy);
1557
+ if (sname === 'retry') {
1558
+ const max = Number(sp.max) || 3;
1559
+ const delay = Number(sp.delay) || 1000;
1560
+ lines.push(` // strategy: retry (max=${max}, delay=${delay}ms)`);
1561
+ lines.push(` for (let _attempt = 0; _attempt < ${max}; _attempt++) {`);
1562
+ lines.push(` try { return await fn(); }`);
1563
+ lines.push(` catch { if (_attempt < ${max - 1}) await new Promise(r => setTimeout(r, ${delay})); }`);
1564
+ lines.push(` }`);
1565
+ }
1566
+ else if (sname === 'fallback') {
1567
+ lines.push(` // strategy: fallback (terminal)`);
1568
+ if (code) {
1569
+ for (const line of code.split('\n')) {
1570
+ lines.push(` ${line}`);
1571
+ }
1572
+ }
1573
+ else {
1574
+ lines.push(` throw new Error('All recovery strategies exhausted for ${name}');`);
1575
+ }
1576
+ }
1577
+ else {
1578
+ // compensate, degrade, or custom
1579
+ lines.push(` // strategy: ${sname}`);
1580
+ lines.push(` try {`);
1581
+ if (code) {
1582
+ for (const line of code.split('\n')) {
1583
+ lines.push(` ${line}`);
1584
+ }
1585
+ }
1586
+ lines.push(` } catch {}`);
1587
+ }
1588
+ }
1589
+ lines.push('}');
1590
+ return lines;
1591
+ }
1592
+ // ── Ground Layer: pattern / apply ────────────────────────────────────────
1593
+ // pattern → registerTemplate() alias (handled by template engine)
1594
+ // apply → expandTemplateNode() alias
1595
+ export function generatePattern(node) {
1596
+ // pattern nodes are registered as templates — no direct output
1597
+ return [];
1598
+ }
1599
+ export function generateApply(node) {
1600
+ // apply nodes expand the referenced pattern
1601
+ const props = p(node);
1602
+ const patternName = props.pattern;
1603
+ if (!patternName)
1604
+ return [];
1605
+ // Delegate to template expansion with the pattern name as node type
1606
+ const syntheticNode = { ...node, type: patternName };
1607
+ if (isTemplateNode(patternName)) {
1608
+ return expandTemplateNode(syntheticNode);
1609
+ }
1610
+ return [`// apply: pattern '${patternName}' not found`];
1611
+ }
1612
+ // ── Conditional ──────────────────────────────────────────────────────────
1613
+ // conditional if=isPro
1614
+ // text value="Pro features unlocked"
1615
+ // → {isPro && (<>..children..</>)}
1616
+ export function generateConditional(node) {
1617
+ const props = p(node);
1618
+ const rawCondition = props.if;
1619
+ // Handle expression objects: { __expr: true, code: 'loading' }
1620
+ const condition = rawCondition && typeof rawCondition === 'object' && rawCondition.__expr
1621
+ ? rawCondition.code
1622
+ : rawCondition;
1623
+ if (!condition)
1624
+ return [`// conditional: missing 'if' prop`];
1625
+ const childLines = [];
1626
+ for (const child of kids(node)) {
1627
+ childLines.push(...generateCoreNode(child));
1628
+ }
1629
+ if (childLines.length === 0) {
1630
+ return [`{${condition} && null}`];
1631
+ }
1632
+ if (childLines.length === 1) {
1633
+ return [`{${condition} && (${childLines[0].trim()})}`];
1634
+ }
1635
+ return [
1636
+ `{${condition} && (`,
1637
+ ` <>`,
1638
+ ...childLines.map(l => ` ${l}`),
1639
+ ` </>`,
1640
+ `)}`,
1641
+ ];
1642
+ }
1643
+ // ── Select ───────────────────────────────────────────────────────────────
1644
+ // select name=status value=current placeholder="Choose"
1645
+ // option value=active label="Active"
1646
+ // option value=pending label="Pending"
1647
+ export function generateSelect(node) {
1648
+ const props = p(node);
1649
+ const name = props.name || 'select';
1650
+ const value = props.value;
1651
+ const placeholder = props.placeholder;
1652
+ const onChange = props.onChange;
1653
+ const attrs = [`name="${name}"`];
1654
+ if (value)
1655
+ attrs.push(`value={${value}}`);
1656
+ if (onChange)
1657
+ attrs.push(`onChange={${onChange}}`);
1658
+ // Event handlers like onChange require 'use client' in React/Next.js
1659
+ const lines = onChange ? [`{/* kern:use-client */}`] : [];
1660
+ lines.push(`<select ${attrs.join(' ')}>`);
1661
+ if (placeholder) {
1662
+ lines.push(` <option value="" disabled>${placeholder}</option>`);
1663
+ }
1664
+ for (const opt of kids(node, 'option')) {
1665
+ const op = p(opt);
1666
+ const optValue = op.value || '';
1667
+ const optLabel = op.label || optValue;
1668
+ lines.push(` <option value="${optValue}">${optLabel}</option>`);
1669
+ }
1670
+ lines.push(`</select>`);
1671
+ return lines;
1672
+ }
1673
+ // ── Model ────────────────────────────────────────────────────────────────
1674
+ // model name=User table=users
1675
+ // column name=id type=uuid primary=true
1676
+ // column name=email type=string unique=true index=true
1677
+ // relation name=posts target=Post kind=one-to-many cascade=delete
1678
+ export function generateModel(node) {
1679
+ const props = p(node);
1680
+ const name = props.name;
1681
+ const table = props.table;
1682
+ const exp = exportPrefix(node);
1683
+ const lines = [];
1684
+ // Generate TypeScript interface
1685
+ lines.push(`${exp}interface ${name} {`);
1686
+ for (const col of kids(node, 'column')) {
1687
+ const cp = p(col);
1688
+ const colName = cp.name;
1689
+ const colType = mapColumnType(cp.type);
1690
+ const opt = cp.optional === 'true' || cp.optional === true ? '?' : '';
1691
+ lines.push(` ${colName}${opt}: ${colType};`);
1692
+ }
1693
+ for (const rel of kids(node, 'relation')) {
1694
+ const rp = p(rel);
1695
+ const relName = rp.name;
1696
+ const target = rp.target;
1697
+ const kind = rp.kind || 'one-to-many';
1698
+ const relType = kind.includes('many') ? `${target}[]` : target;
1699
+ lines.push(` ${relName}?: ${relType};`);
1700
+ }
1701
+ lines.push('}');
1702
+ // Prisma-hint comment
1703
+ if (table) {
1704
+ lines.push('');
1705
+ lines.push(`// Prisma: @@map("${table}")`);
1706
+ }
1707
+ return lines;
1708
+ }
1709
+ function mapColumnType(kernType) {
1710
+ const typeMap = {
1711
+ uuid: 'string', string: 'string', text: 'string',
1712
+ int: 'number', integer: 'number', float: 'number', decimal: 'number',
1713
+ boolean: 'boolean', bool: 'boolean',
1714
+ date: 'Date', datetime: 'Date', timestamp: 'Date',
1715
+ json: 'Record<string, unknown>',
1716
+ };
1717
+ return typeMap[kernType] || kernType;
1718
+ }
1719
+ // ── Repository ───────────────────────────────────────────────────────────
1720
+ // repository name=UserRepo model=User
1721
+ // method name=findByEmail params="email:string" returns="User|null"
1722
+ // handler <<<return this.findOne({ email });>>>
1723
+ export function generateRepository(node) {
1724
+ const props = p(node);
1725
+ const name = props.name;
1726
+ const model = props.model;
1727
+ const exp = exportPrefix(node);
1728
+ const lines = [];
1729
+ lines.push(`${exp}class ${name} {`);
1730
+ if (model) {
1731
+ lines.push(` constructor(private readonly model: typeof ${model}) {}`);
1732
+ lines.push('');
1733
+ }
1734
+ for (const method of kids(node, 'method')) {
1735
+ const mp = p(method);
1736
+ const mname = mp.name;
1737
+ const mparams = mp.params ? parseParamList(mp.params) : '';
1738
+ const isAsync = mp.async === 'true' || mp.async === true;
1739
+ const asyncKw = isAsync ? 'async ' : '';
1740
+ const mreturns = mp.returns ? `: ${mp.returns}` : '';
1741
+ const mcode = handlerCode(method);
1742
+ lines.push(` ${asyncKw}${mname}(${mparams})${mreturns} {`);
1743
+ if (mcode) {
1744
+ for (const line of mcode.split('\n')) {
1745
+ lines.push(` ${line}`);
1746
+ }
1747
+ }
1748
+ lines.push(' }');
1749
+ lines.push('');
1750
+ }
1751
+ lines.push('}');
1752
+ return lines;
1753
+ }
1754
+ // ── Dependency ───────────────────────────────────────────────────────────
1755
+ // dependency name=authService scope=singleton
1756
+ // inject name=db from=database
1757
+ // inject name=repo type=UserRepository with=db
1758
+ // returns AuthService with=repo
1759
+ export function generateDependency(node) {
1760
+ const props = p(node);
1761
+ const name = props.name;
1762
+ const scope = props.scope || 'transient';
1763
+ const exp = exportPrefix(node);
1764
+ const lines = [];
1765
+ const injects = kids(node, 'inject');
1766
+ const returnsNode = firstChild(node, 'returns');
1767
+ const returnsType = returnsNode ? (p(returnsNode).name || p(returnsNode).type || 'unknown') : 'unknown';
1768
+ if (scope === 'singleton') {
1769
+ lines.push(`let _${name}Instance: ${returnsType} | null = null;`);
1770
+ lines.push('');
1771
+ }
1772
+ lines.push(`${exp}function create${name[0].toUpperCase()}${name.slice(1)}(): ${returnsType} {`);
1773
+ if (scope === 'singleton') {
1774
+ lines.push(` if (_${name}Instance) return _${name}Instance;`);
1775
+ }
1776
+ for (const inj of injects) {
1777
+ const ip = p(inj);
1778
+ const injName = ip.name;
1779
+ const injType = ip.type;
1780
+ const injFrom = ip.from;
1781
+ const injWith = ip.with;
1782
+ if (injFrom) {
1783
+ lines.push(` const ${injName} = ${injFrom};`);
1784
+ }
1785
+ else if (injType && injWith) {
1786
+ lines.push(` const ${injName} = new ${injType}(${injWith});`);
1787
+ }
1788
+ else if (injType) {
1789
+ lines.push(` const ${injName} = new ${injType}();`);
1790
+ }
1791
+ }
1792
+ const returnsWith = returnsNode ? p(returnsNode).with : undefined;
1793
+ if (returnsWith) {
1794
+ lines.push(` const instance = new ${returnsType}(${returnsWith});`);
1795
+ }
1796
+ else {
1797
+ lines.push(` const instance = new ${returnsType}();`);
1798
+ }
1799
+ if (scope === 'singleton') {
1800
+ lines.push(` _${name}Instance = instance;`);
1801
+ }
1802
+ lines.push(` return instance;`);
1803
+ lines.push('}');
1804
+ return lines;
1805
+ }
1806
+ // ── Cache ────────────────────────────────────────────────────────────────
1807
+ // cache name=userCache backend=redis prefix="user:" ttl=3600
1808
+ // entry name=profile key="user:{id}"
1809
+ // strategy read-through
1810
+ // invalidate on=userUpdate tags="user:{id}"
1811
+ export function generateCache(node) {
1812
+ const props = p(node);
1813
+ const name = props.name;
1814
+ const backend = props.backend || 'memory';
1815
+ const prefix = props.prefix || '';
1816
+ const ttl = props.ttl;
1817
+ const exp = exportPrefix(node);
1818
+ const lines = [];
1819
+ lines.push(`${exp}const ${name} = {`);
1820
+ lines.push(` prefix: '${prefix}',`);
1821
+ if (ttl)
1822
+ lines.push(` ttl: ${ttl},`);
1823
+ lines.push(` backend: '${backend}',`);
1824
+ lines.push('');
1825
+ // Entry methods
1826
+ for (const entry of kids(node, 'entry')) {
1827
+ const ep = p(entry);
1828
+ const entryName = ep.name;
1829
+ const key = ep.key || entryName;
1830
+ const strategyNode = firstChild(entry, 'strategy');
1831
+ const strategy = strategyNode ? (p(strategyNode).name || 'cache-aside') : 'cache-aside';
1832
+ lines.push(` async get${entryName[0].toUpperCase()}${entryName.slice(1)}(id: string) {`);
1833
+ lines.push(` const key = \`${prefix}${key.replace(/\{id\}/g, '${id}')}\`;`);
1834
+ if (strategy === 'read-through') {
1835
+ lines.push(` // read-through: check cache, fetch if miss, populate cache`);
1836
+ }
1837
+ lines.push(` return ${backend === 'redis' ? `await redis.get(key)` : `cache.get(key)`};`);
1838
+ lines.push(` },`);
1839
+ lines.push('');
1840
+ }
1841
+ // Invalidation methods
1842
+ for (const inv of kids(node, 'invalidate')) {
1843
+ const ip = p(inv);
1844
+ const on = ip.on || 'update';
1845
+ const tags = ip.tags || '';
1846
+ lines.push(` async invalidateOn${on[0].toUpperCase()}${on.slice(1)}(id: string) {`);
1847
+ const invalidateKey = tags
1848
+ ? `\`${prefix}${tags.replace(/\{id\}/g, '${id}')}\``
1849
+ : `\`${prefix}\${id}\``;
1850
+ lines.push(` const key = ${invalidateKey};`);
1851
+ lines.push(` ${backend === 'redis' ? `await redis.del(key)` : `cache.delete(key)`};`);
1852
+ lines.push(` },`);
1853
+ lines.push('');
1854
+ }
1855
+ lines.push('} as const;');
1856
+ return lines;
1857
+ }
709
1858
  // ── Dispatcher ───────────────────────────────────────────────────────────
710
1859
  export const CORE_NODE_TYPES = new Set([
711
1860
  'type', 'interface', 'field', 'fn',
1861
+ 'union', 'variant',
1862
+ 'service', 'method', 'singleton', 'constructor',
712
1863
  'machine', 'transition',
713
1864
  'error', 'module', 'export',
714
1865
  'config', 'store',
715
1866
  'test', 'describe', 'it',
716
1867
  'event', 'import', 'const',
717
1868
  'hook',
1869
+ 'on', 'websocket',
718
1870
  'template', 'slot', 'body',
1871
+ // Async extensions
1872
+ 'signal', 'cleanup',
1873
+ // Ground layer
1874
+ 'derive', 'transform', 'action', 'guard', 'assume', 'invariant',
1875
+ 'each', 'collect', 'branch', 'path',
1876
+ 'resolve', 'candidate', 'discriminator',
1877
+ 'expect', 'recover', 'strategy',
1878
+ 'pattern', 'apply',
1879
+ // Reason layer
1880
+ 'reason', 'evidence',
1881
+ // Confidence layer
1882
+ 'needs',
1883
+ // Backend data layer (graduated nodes)
1884
+ 'model', 'column', 'relation',
1885
+ 'repository',
1886
+ 'dependency', 'inject',
1887
+ 'cache', 'entry', 'invalidate',
1888
+ // UI controls (graduated nodes)
1889
+ 'conditional',
1890
+ 'select', 'option',
719
1891
  ]);
720
1892
  /** Check if a node type is a core language construct. */
721
1893
  export function isCoreNode(type) {
722
1894
  return CORE_NODE_TYPES.has(type);
723
1895
  }
724
1896
  /** Generate TypeScript for any core language node. */
725
- export function generateCoreNode(node) {
1897
+ export function generateCoreNode(node, target) {
726
1898
  switch (node.type) {
727
1899
  case 'type': return generateType(node);
728
1900
  case 'interface': return generateInterface(node);
1901
+ case 'union': return generateUnion(node);
1902
+ case 'service': return generateService(node);
729
1903
  case 'fn': return generateFunction(node);
730
1904
  case 'machine': return generateMachine(node);
731
1905
  case 'error': return generateError(node);
@@ -737,15 +1911,67 @@ export function generateCoreNode(node) {
737
1911
  case 'import': return generateImport(node);
738
1912
  case 'const': return generateConst(node);
739
1913
  case 'hook': return generateHook(node);
740
- // Template definitions produce no output
1914
+ case 'on': return generateOn(node);
1915
+ case 'websocket': return generateWebSocket(node);
1916
+ // Ground layer
1917
+ case 'derive': return generateDerive(node);
1918
+ case 'transform': return generateTransform(node);
1919
+ case 'action': return generateAction(node);
1920
+ case 'guard': return generateGuard(node);
1921
+ case 'assume': return generateAssume(node);
1922
+ case 'invariant': return generateInvariant(node);
1923
+ case 'each': return generateEach(node);
1924
+ case 'collect': return generateCollect(node);
1925
+ case 'branch': return generateBranch(node);
1926
+ case 'resolve': return generateResolve(node);
1927
+ case 'expect': return generateExpect(node);
1928
+ case 'recover': return generateRecover(node);
1929
+ case 'pattern': return generatePattern(node);
1930
+ case 'apply': return generateApply(node);
1931
+ // Template / structural definitions produce no output
741
1932
  case 'template': return [];
742
1933
  case 'slot': return [];
743
1934
  case 'body': return [];
744
- default:
1935
+ case 'path': return [];
1936
+ case 'candidate': return [];
1937
+ case 'discriminator': return [];
1938
+ case 'strategy': return [];
1939
+ case 'reason': return [];
1940
+ case 'evidence': return [];
1941
+ case 'needs': return [];
1942
+ // Graduated nodes — backend data layer
1943
+ case 'model': return generateModel(node);
1944
+ case 'repository': return generateRepository(node);
1945
+ case 'dependency': return generateDependency(node);
1946
+ case 'cache': return generateCache(node);
1947
+ // Graduated nodes — UI controls
1948
+ case 'conditional': return generateConditional(node);
1949
+ case 'select': return generateSelect(node);
1950
+ // Structural children consumed by parents
1951
+ case 'variant': return [];
1952
+ case 'method': return [];
1953
+ case 'singleton': return [];
1954
+ case 'constructor': return [];
1955
+ case 'signal': return [];
1956
+ case 'cleanup': return [];
1957
+ case 'column': return [];
1958
+ case 'relation': return [];
1959
+ case 'inject': return [];
1960
+ case 'entry': return [];
1961
+ case 'invalidate': return [];
1962
+ case 'option': return [];
1963
+ default: {
1964
+ // Check evolved generators (v4) — target-specific first, then default
1965
+ const targetMap = target ? _evolvedTargetGenerators.get(node.type) : undefined;
1966
+ const targetGen = targetMap && target ? targetMap.get(target) : undefined;
1967
+ const evolvedGen = targetGen || _evolvedGenerators.get(node.type);
1968
+ if (evolvedGen)
1969
+ return evolvedGen(node);
745
1970
  // Check if this is a template instance
746
1971
  if (isTemplateNode(node.type))
747
1972
  return expandTemplateNode(node);
748
1973
  return [];
1974
+ }
749
1975
  }
750
1976
  }
751
1977
  //# sourceMappingURL=codegen-core.js.map