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