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