@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.
- package/dist/codegen-core.d.ts +41 -2
- package/dist/codegen-core.js +1217 -16
- package/dist/codegen-core.js.map +1 -1
- package/dist/concepts.d.ts +100 -0
- package/dist/concepts.js +17 -0
- package/dist/concepts.js.map +1 -0
- package/dist/config.d.ts +37 -1
- package/dist/config.js +29 -1
- package/dist/config.js.map +1 -1
- package/dist/coverage-gap.d.ts +27 -0
- package/dist/coverage-gap.js +73 -0
- package/dist/coverage-gap.js.map +1 -0
- package/dist/errors.d.ts +16 -0
- package/dist/errors.js +9 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +8 -3
- package/dist/index.js +17 -3
- package/dist/index.js.map +1 -1
- package/dist/parser.d.ts +12 -0
- package/dist/parser.js +191 -3
- package/dist/parser.js.map +1 -1
- package/dist/scanner.js +48 -3
- package/dist/scanner.js.map +1 -1
- package/dist/spec.d.ts +13 -1
- package/dist/spec.js +54 -3
- package/dist/spec.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/package.json +10 -2
package/dist/codegen-core.js
CHANGED
|
@@ -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
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
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
|
|
551
|
-
const
|
|
552
|
-
|
|
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="
|
|
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
|
-
|
|
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
|
-
|
|
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
|