bonescript-compiler 0.4.0 → 0.5.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/emit_auth.d.ts +14 -2
- package/dist/emit_auth.js +498 -60
- package/dist/emit_auth.js.map +1 -1
- package/dist/emit_full.js +133 -11
- package/dist/emit_full.js.map +1 -1
- package/dist/emit_index.js +46 -1
- package/dist/emit_index.js.map +1 -1
- package/dist/emitter.js +47 -0
- package/dist/emitter.js.map +1 -1
- package/dist/ir.d.ts +4 -0
- package/dist/lowering_channels.js +1 -0
- package/dist/lowering_channels.js.map +1 -1
- package/dist/lowering_entities.js +11 -1
- package/dist/lowering_entities.js.map +1 -1
- package/dist/optimizer.js +1 -1
- package/dist/optimizer.js.map +1 -1
- package/dist/scaffold.js +0 -1
- package/dist/scaffold.js.map +1 -1
- package/dist/typechecker.d.ts +5 -0
- package/dist/typechecker.js +36 -0
- package/dist/typechecker.js.map +1 -1
- package/package.json +1 -1
- package/src/emit_auth.ts +513 -67
- package/src/emit_full.ts +136 -12
- package/src/emit_index.ts +210 -161
- package/src/emitter.ts +642 -592
- package/src/ir.ts +1 -0
- package/src/lowering_channels.ts +1 -0
- package/src/lowering_entities.ts +258 -248
- package/src/optimizer.ts +1 -1
- package/src/scaffold.ts +0 -1
- package/src/typechecker.ts +657 -606
package/src/emitter.ts
CHANGED
|
@@ -1,592 +1,642 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* BoneScript Code Emitter — Stage 6 of the compilation pipeline.
|
|
3
|
-
* Implements spec/09_CODEGEN.md.
|
|
4
|
-
*
|
|
5
|
-
* Generates target code from the IR. Every IR node maps to code.
|
|
6
|
-
* No orphan logic. No hidden behavior. Deterministic formatting.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import * as IR from "./ir";
|
|
10
|
-
|
|
11
|
-
export interface EmittedFile {
|
|
12
|
-
path: string;
|
|
13
|
-
content: string;
|
|
14
|
-
language: "typescript" | "sql" | "yaml" | "json";
|
|
15
|
-
source_module: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// ─── Type Mapping ────────────────────────────────────────────────────────────
|
|
19
|
-
|
|
20
|
-
const TS_TYPE_MAP: Record<string, string> = {
|
|
21
|
-
string: "string",
|
|
22
|
-
uint: "number",
|
|
23
|
-
int: "number",
|
|
24
|
-
float: "number",
|
|
25
|
-
bool: "boolean",
|
|
26
|
-
timestamp: "Date",
|
|
27
|
-
uuid: "string",
|
|
28
|
-
bytes: "Buffer",
|
|
29
|
-
json: "unknown",
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const SQL_TYPE_MAP: Record<string, string> = {
|
|
33
|
-
string: "VARCHAR",
|
|
34
|
-
uint: "BIGINT",
|
|
35
|
-
int: "BIGINT",
|
|
36
|
-
float: "DOUBLE PRECISION",
|
|
37
|
-
bool: "BOOLEAN",
|
|
38
|
-
timestamp: "TIMESTAMPTZ",
|
|
39
|
-
uuid: "UUID",
|
|
40
|
-
bytes: "BYTEA",
|
|
41
|
-
json: "JSONB",
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
function toTsType(irType: string): string {
|
|
45
|
-
if (TS_TYPE_MAP[irType]) return TS_TYPE_MAP[irType];
|
|
46
|
-
const listMatch = irType.match(/^list<(.+)>$/);
|
|
47
|
-
if (listMatch) return `${toTsType(listMatch[1])}[]`;
|
|
48
|
-
const setMatch = irType.match(/^set<(.+)>$/);
|
|
49
|
-
if (setMatch) return `Set<${toTsType(setMatch[1])}>`;
|
|
50
|
-
const mapMatch = irType.match(/^map<(.+),\s*(.+)>$/);
|
|
51
|
-
if (mapMatch) return `Map<${toTsType(mapMatch[1])}, ${toTsType(mapMatch[2])}>`;
|
|
52
|
-
const optMatch = irType.match(/^optional<(.+)>$/);
|
|
53
|
-
if (optMatch) return `${toTsType(optMatch[1])} | null`;
|
|
54
|
-
// Entity reference or unknown
|
|
55
|
-
return irType;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function toSqlType(irType: string): string {
|
|
59
|
-
if (SQL_TYPE_MAP[irType]) return SQL_TYPE_MAP[irType];
|
|
60
|
-
if (irType.startsWith("list<") || irType.startsWith("set<") || irType.startsWith("map<")) return "JSONB";
|
|
61
|
-
if (irType.startsWith("optional<")) return toSqlType(irType.slice(9, -1));
|
|
62
|
-
return "JSONB";
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** Returns an inline SQL CHECK constraint for types that need one, or empty string. */
|
|
66
|
-
function sqlCheckConstraint(irType: string): string {
|
|
67
|
-
if (irType === "uint") return " CHECK (VALUE >= 0)";
|
|
68
|
-
return "";
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function toSnakeCase(s: string): string {
|
|
72
|
-
return s.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase();
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// ─── Emitter ─────────────────────────────────────────────────────────────────
|
|
76
|
-
|
|
77
|
-
export class Emitter {
|
|
78
|
-
emit(system: IR.IRSystem): EmittedFile[] {
|
|
79
|
-
const files: EmittedFile[] = [];
|
|
80
|
-
|
|
81
|
-
// 1. Schema files (SQL)
|
|
82
|
-
for (const mod of system.modules) {
|
|
83
|
-
if (mod.kind === "data_store" || mod.kind === "api_service") {
|
|
84
|
-
for (const model of mod.models) {
|
|
85
|
-
files.push(this.emitSchema(model, mod, system));
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// 2. Type definition files (TypeScript)
|
|
91
|
-
files.push(this.emitSharedTypes(system));
|
|
92
|
-
|
|
93
|
-
// 3. Event types (TypeScript)
|
|
94
|
-
if (system.events.length > 0) {
|
|
95
|
-
files.push(this.emitEventTypes(system));
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// 4. Service files (TypeScript)
|
|
99
|
-
for (const mod of system.modules) {
|
|
100
|
-
if (mod.kind === "api_service") {
|
|
101
|
-
files.push(this.emitService(mod, system));
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// 5. State machine files (TypeScript)
|
|
106
|
-
for (const mod of system.modules) {
|
|
107
|
-
for (const sm of mod.state_machines) {
|
|
108
|
-
files.push(this.emitStateMachine(sm, mod, system));
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// 6. Config files (YAML)
|
|
113
|
-
files.push(this.emitServiceConfig(system));
|
|
114
|
-
|
|
115
|
-
// 7. Infrastructure config (YAML)
|
|
116
|
-
files.push(this.emitInfraConfig(system));
|
|
117
|
-
|
|
118
|
-
return files;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// ─── SQL Schema ────────────────────────────────────────────────────────────
|
|
122
|
-
|
|
123
|
-
private emitSchema(model: IR.IRModel, mod: IR.IRModule, system: IR.IRSystem): EmittedFile {
|
|
124
|
-
const tableName = toSnakeCase(model.name) + "s";
|
|
125
|
-
const lines: string[] = [];
|
|
126
|
-
|
|
127
|
-
lines.push(`-- Generated by BoneScript compiler. DO NOT EDIT.`);
|
|
128
|
-
lines.push(`-- Source: ${system.source_hash}`);
|
|
129
|
-
lines.push(`-- Module: ${mod.name}`);
|
|
130
|
-
lines.push(``);
|
|
131
|
-
lines.push(`CREATE TABLE IF NOT EXISTS ${tableName} (`);
|
|
132
|
-
|
|
133
|
-
const fieldLines: string[] = [];
|
|
134
|
-
for (const field of model.fields) {
|
|
135
|
-
let line = ` ${field.name} ${toSqlType(field.type)}${sqlCheckConstraint(field.type)}`;
|
|
136
|
-
if (!field.nullable) line += " NOT NULL";
|
|
137
|
-
if (field.default_value) {
|
|
138
|
-
if (field.default_value === "gen_random_uuid()") line += " DEFAULT gen_random_uuid()";
|
|
139
|
-
else if (field.default_value === "now()") line += " DEFAULT NOW()";
|
|
140
|
-
else line += ` DEFAULT ${field.default_value}`;
|
|
141
|
-
} else if (field.name === "created_at" || field.name === "updated_at") {
|
|
142
|
-
// Always add DEFAULT NOW() for timestamp audit fields
|
|
143
|
-
line += " DEFAULT NOW()";
|
|
144
|
-
} else if (field.name === "id" && field.type === "uuid") {
|
|
145
|
-
// Always add DEFAULT gen_random_uuid() for uuid primary keys
|
|
146
|
-
line += " DEFAULT gen_random_uuid()";
|
|
147
|
-
}
|
|
148
|
-
if (field.name === model.primary_key) line += " PRIMARY KEY";
|
|
149
|
-
fieldLines.push(line);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Add unique constraints
|
|
153
|
-
for (const c of model.constraints) {
|
|
154
|
-
if (c.kind === "unique") {
|
|
155
|
-
fieldLines.push(` CONSTRAINT ${tableName}_${c.target}_unique UNIQUE (${c.target})`);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// Add foreign key constraints from relations
|
|
160
|
-
for (const rel of mod.relations || []) {
|
|
161
|
-
if (rel.kind === "belongs_to") {
|
|
162
|
-
// belongs_to: FK is on this table
|
|
163
|
-
fieldLines.push(` CONSTRAINT fk_${tableName}_${rel.foreign_key} FOREIGN KEY (${rel.foreign_key}) REFERENCES ${rel.to_table}(id) ON DELETE CASCADE`);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
lines.push(`
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
}
|
|
242
|
-
lines.push(`
|
|
243
|
-
lines.push(
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
lines.push(
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
lines.push(`
|
|
311
|
-
lines.push(
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
lines.push(
|
|
315
|
-
lines.push(`
|
|
316
|
-
lines.push(`
|
|
317
|
-
lines.push(`
|
|
318
|
-
lines.push(
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
lines
|
|
335
|
-
lines.push(
|
|
336
|
-
lines.push(
|
|
337
|
-
lines.push(``);
|
|
338
|
-
|
|
339
|
-
for (const
|
|
340
|
-
|
|
341
|
-
lines.push(`
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
}
|
|
348
|
-
lines.push(`
|
|
349
|
-
lines.push(
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
lines.push(`
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
lines.push(
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
lines.push(`
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
lines.push(
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
lines.push(`
|
|
486
|
-
lines.push(
|
|
487
|
-
|
|
488
|
-
lines.
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
const
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
lines.push(`
|
|
513
|
-
lines.push(
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
};
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
lines.push(
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
lines.push(
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
1
|
+
/**
|
|
2
|
+
* BoneScript Code Emitter — Stage 6 of the compilation pipeline.
|
|
3
|
+
* Implements spec/09_CODEGEN.md.
|
|
4
|
+
*
|
|
5
|
+
* Generates target code from the IR. Every IR node maps to code.
|
|
6
|
+
* No orphan logic. No hidden behavior. Deterministic formatting.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as IR from "./ir";
|
|
10
|
+
|
|
11
|
+
export interface EmittedFile {
|
|
12
|
+
path: string;
|
|
13
|
+
content: string;
|
|
14
|
+
language: "typescript" | "sql" | "yaml" | "json";
|
|
15
|
+
source_module: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// ─── Type Mapping ────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
const TS_TYPE_MAP: Record<string, string> = {
|
|
21
|
+
string: "string",
|
|
22
|
+
uint: "number",
|
|
23
|
+
int: "number",
|
|
24
|
+
float: "number",
|
|
25
|
+
bool: "boolean",
|
|
26
|
+
timestamp: "Date",
|
|
27
|
+
uuid: "string",
|
|
28
|
+
bytes: "Buffer",
|
|
29
|
+
json: "unknown",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const SQL_TYPE_MAP: Record<string, string> = {
|
|
33
|
+
string: "VARCHAR",
|
|
34
|
+
uint: "BIGINT",
|
|
35
|
+
int: "BIGINT",
|
|
36
|
+
float: "DOUBLE PRECISION",
|
|
37
|
+
bool: "BOOLEAN",
|
|
38
|
+
timestamp: "TIMESTAMPTZ",
|
|
39
|
+
uuid: "UUID",
|
|
40
|
+
bytes: "BYTEA",
|
|
41
|
+
json: "JSONB",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
function toTsType(irType: string): string {
|
|
45
|
+
if (TS_TYPE_MAP[irType]) return TS_TYPE_MAP[irType];
|
|
46
|
+
const listMatch = irType.match(/^list<(.+)>$/);
|
|
47
|
+
if (listMatch) return `${toTsType(listMatch[1])}[]`;
|
|
48
|
+
const setMatch = irType.match(/^set<(.+)>$/);
|
|
49
|
+
if (setMatch) return `Set<${toTsType(setMatch[1])}>`;
|
|
50
|
+
const mapMatch = irType.match(/^map<(.+),\s*(.+)>$/);
|
|
51
|
+
if (mapMatch) return `Map<${toTsType(mapMatch[1])}, ${toTsType(mapMatch[2])}>`;
|
|
52
|
+
const optMatch = irType.match(/^optional<(.+)>$/);
|
|
53
|
+
if (optMatch) return `${toTsType(optMatch[1])} | null`;
|
|
54
|
+
// Entity reference or unknown
|
|
55
|
+
return irType;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function toSqlType(irType: string): string {
|
|
59
|
+
if (SQL_TYPE_MAP[irType]) return SQL_TYPE_MAP[irType];
|
|
60
|
+
if (irType.startsWith("list<") || irType.startsWith("set<") || irType.startsWith("map<")) return "JSONB";
|
|
61
|
+
if (irType.startsWith("optional<")) return toSqlType(irType.slice(9, -1));
|
|
62
|
+
return "JSONB";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Returns an inline SQL CHECK constraint for types that need one, or empty string. */
|
|
66
|
+
function sqlCheckConstraint(irType: string): string {
|
|
67
|
+
if (irType === "uint") return " CHECK (VALUE >= 0)";
|
|
68
|
+
return "";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function toSnakeCase(s: string): string {
|
|
72
|
+
return s.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ─── Emitter ─────────────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
export class Emitter {
|
|
78
|
+
emit(system: IR.IRSystem): EmittedFile[] {
|
|
79
|
+
const files: EmittedFile[] = [];
|
|
80
|
+
|
|
81
|
+
// 1. Schema files (SQL)
|
|
82
|
+
for (const mod of system.modules) {
|
|
83
|
+
if (mod.kind === "data_store" || mod.kind === "api_service") {
|
|
84
|
+
for (const model of mod.models) {
|
|
85
|
+
files.push(this.emitSchema(model, mod, system));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 2. Type definition files (TypeScript)
|
|
91
|
+
files.push(this.emitSharedTypes(system));
|
|
92
|
+
|
|
93
|
+
// 3. Event types (TypeScript)
|
|
94
|
+
if (system.events.length > 0) {
|
|
95
|
+
files.push(this.emitEventTypes(system));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 4. Service files (TypeScript)
|
|
99
|
+
for (const mod of system.modules) {
|
|
100
|
+
if (mod.kind === "api_service") {
|
|
101
|
+
files.push(this.emitService(mod, system));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 5. State machine files (TypeScript)
|
|
106
|
+
for (const mod of system.modules) {
|
|
107
|
+
for (const sm of mod.state_machines) {
|
|
108
|
+
files.push(this.emitStateMachine(sm, mod, system));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 6. Config files (YAML)
|
|
113
|
+
files.push(this.emitServiceConfig(system));
|
|
114
|
+
|
|
115
|
+
// 7. Infrastructure config (YAML)
|
|
116
|
+
files.push(this.emitInfraConfig(system));
|
|
117
|
+
|
|
118
|
+
return files;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ─── SQL Schema ────────────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
private emitSchema(model: IR.IRModel, mod: IR.IRModule, system: IR.IRSystem): EmittedFile {
|
|
124
|
+
const tableName = toSnakeCase(model.name) + "s";
|
|
125
|
+
const lines: string[] = [];
|
|
126
|
+
|
|
127
|
+
lines.push(`-- Generated by BoneScript compiler. DO NOT EDIT.`);
|
|
128
|
+
lines.push(`-- Source: ${system.source_hash}`);
|
|
129
|
+
lines.push(`-- Module: ${mod.name}`);
|
|
130
|
+
lines.push(``);
|
|
131
|
+
lines.push(`CREATE TABLE IF NOT EXISTS ${tableName} (`);
|
|
132
|
+
|
|
133
|
+
const fieldLines: string[] = [];
|
|
134
|
+
for (const field of model.fields) {
|
|
135
|
+
let line = ` ${field.name} ${toSqlType(field.type)}${sqlCheckConstraint(field.type)}`;
|
|
136
|
+
if (!field.nullable) line += " NOT NULL";
|
|
137
|
+
if (field.default_value) {
|
|
138
|
+
if (field.default_value === "gen_random_uuid()") line += " DEFAULT gen_random_uuid()";
|
|
139
|
+
else if (field.default_value === "now()") line += " DEFAULT NOW()";
|
|
140
|
+
else line += ` DEFAULT ${field.default_value}`;
|
|
141
|
+
} else if (field.name === "created_at" || field.name === "updated_at") {
|
|
142
|
+
// Always add DEFAULT NOW() for timestamp audit fields
|
|
143
|
+
line += " DEFAULT NOW()";
|
|
144
|
+
} else if (field.name === "id" && field.type === "uuid") {
|
|
145
|
+
// Always add DEFAULT gen_random_uuid() for uuid primary keys
|
|
146
|
+
line += " DEFAULT gen_random_uuid()";
|
|
147
|
+
}
|
|
148
|
+
if (field.name === model.primary_key) line += " PRIMARY KEY";
|
|
149
|
+
fieldLines.push(line);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Add unique constraints
|
|
153
|
+
for (const c of model.constraints) {
|
|
154
|
+
if (c.kind === "unique") {
|
|
155
|
+
fieldLines.push(` CONSTRAINT ${tableName}_${c.target}_unique UNIQUE (${c.target})`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Add foreign key constraints from relations
|
|
160
|
+
for (const rel of mod.relations || []) {
|
|
161
|
+
if (rel.kind === "belongs_to") {
|
|
162
|
+
// belongs_to: FK is on this table
|
|
163
|
+
fieldLines.push(` CONSTRAINT fk_${tableName}_${rel.foreign_key} FOREIGN KEY (${rel.foreign_key}) REFERENCES ${rel.to_table}(id) ON DELETE CASCADE`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Add cardinality CHECK constraints from relations
|
|
168
|
+
// has_one: enforce at most 1 child row via a partial unique index (emitted below)
|
|
169
|
+
// has_many with explicit max: enforce via CHECK on count (done via trigger — see below)
|
|
170
|
+
for (const rel of (mod as any).relations_with_cardinality || []) {
|
|
171
|
+
if (rel.cardinality && rel.cardinality.max !== "*" && typeof rel.cardinality.max === "number") {
|
|
172
|
+
// Will be enforced via trigger — placeholder comment
|
|
173
|
+
fieldLines.push(` -- cardinality: ${rel.name} max ${rel.cardinality.max} (enforced by trigger)`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
lines.push(fieldLines.join(",\n"));
|
|
178
|
+
lines.push(`);`);
|
|
179
|
+
lines.push(``);
|
|
180
|
+
|
|
181
|
+
// Indexes
|
|
182
|
+
for (const idx of model.indexes) {
|
|
183
|
+
const idxName = `idx_${tableName}_${idx.fields.join("_")}`;
|
|
184
|
+
const unique = idx.unique ? "UNIQUE " : "";
|
|
185
|
+
lines.push(`CREATE ${unique}INDEX IF NOT EXISTS ${idxName} ON ${tableName} (${idx.fields.join(", ")});`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// FK indexes for belongs_to relations
|
|
189
|
+
for (const rel of mod.relations || []) {
|
|
190
|
+
if (rel.kind === "belongs_to") {
|
|
191
|
+
lines.push(`CREATE INDEX IF NOT EXISTS idx_${tableName}_${rel.foreign_key} ON ${tableName} (${rel.foreign_key});`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Cardinality enforcement
|
|
196
|
+
for (const rel of mod.relations || []) {
|
|
197
|
+
// has_one: enforce via unique index on the FK column in the child table
|
|
198
|
+
if (rel.kind === "has_one") {
|
|
199
|
+
const childTable = rel.to_table;
|
|
200
|
+
const fk = rel.foreign_key;
|
|
201
|
+
lines.push(``);
|
|
202
|
+
lines.push(`-- has_one cardinality: each ${tableName.slice(0, -1)} may have at most one ${childTable.slice(0, -1)}`);
|
|
203
|
+
lines.push(`CREATE UNIQUE INDEX IF NOT EXISTS idx_${childTable}_${fk}_unique ON ${childTable} (${fk});`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// has_many with explicit numeric max: enforce via a BEFORE INSERT trigger
|
|
207
|
+
if (rel.kind === "has_many" && rel.cardinality && rel.cardinality.max !== "*") {
|
|
208
|
+
const maxCount = rel.cardinality.max as number;
|
|
209
|
+
const childTable = rel.to_table;
|
|
210
|
+
const fk = rel.foreign_key;
|
|
211
|
+
const fnName = `check_${tableName}_${rel.name}_max`;
|
|
212
|
+
lines.push(``);
|
|
213
|
+
lines.push(`-- has_many cardinality: max ${maxCount} ${childTable} per ${tableName.slice(0, -1)}`);
|
|
214
|
+
lines.push(`CREATE OR REPLACE FUNCTION ${fnName}()`);
|
|
215
|
+
lines.push(`RETURNS TRIGGER AS $$`);
|
|
216
|
+
lines.push(`DECLARE`);
|
|
217
|
+
lines.push(` current_count INTEGER;`);
|
|
218
|
+
lines.push(`BEGIN`);
|
|
219
|
+
lines.push(` SELECT COUNT(*) INTO current_count FROM ${childTable} WHERE ${fk} = NEW.${fk};`);
|
|
220
|
+
lines.push(` IF current_count >= ${maxCount} THEN`);
|
|
221
|
+
lines.push(` RAISE EXCEPTION 'Cardinality violation: ${tableName.slice(0, -1)} already has ${maxCount} ${childTable} (max ${maxCount})';`);
|
|
222
|
+
lines.push(` END IF;`);
|
|
223
|
+
lines.push(` RETURN NEW;`);
|
|
224
|
+
lines.push(`END;`);
|
|
225
|
+
lines.push(`$$ LANGUAGE plpgsql;`);
|
|
226
|
+
lines.push(``);
|
|
227
|
+
lines.push(`DROP TRIGGER IF EXISTS trg_${fnName} ON ${childTable};`);
|
|
228
|
+
lines.push(`CREATE TRIGGER trg_${fnName}`);
|
|
229
|
+
lines.push(` BEFORE INSERT ON ${childTable}`);
|
|
230
|
+
lines.push(` FOR EACH ROW`);
|
|
231
|
+
lines.push(` EXECUTE FUNCTION ${fnName}();`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Junction tables for many_to_many
|
|
236
|
+
for (const rel of mod.relations || []) {
|
|
237
|
+
if (rel.kind === "many_to_many" && rel.junction_table) {
|
|
238
|
+
lines.push(``);
|
|
239
|
+
lines.push(`CREATE TABLE IF NOT EXISTS ${rel.junction_table} (`);
|
|
240
|
+
lines.push(` ${rel.from_table.slice(0, -1)}_id UUID NOT NULL REFERENCES ${rel.from_table}(id) ON DELETE CASCADE,`);
|
|
241
|
+
lines.push(` ${rel.to_table.slice(0, -1)}_id UUID NOT NULL REFERENCES ${rel.to_table}(id) ON DELETE CASCADE,`);
|
|
242
|
+
lines.push(` created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),`);
|
|
243
|
+
lines.push(` PRIMARY KEY (${rel.from_table.slice(0, -1)}_id, ${rel.to_table.slice(0, -1)}_id)`);
|
|
244
|
+
lines.push(`);`);
|
|
245
|
+
lines.push(`CREATE INDEX IF NOT EXISTS idx_${rel.junction_table}_${rel.from_table.slice(0, -1)} ON ${rel.junction_table} (${rel.from_table.slice(0, -1)}_id);`);
|
|
246
|
+
lines.push(`CREATE INDEX IF NOT EXISTS idx_${rel.junction_table}_${rel.to_table.slice(0, -1)} ON ${rel.junction_table} (${rel.to_table.slice(0, -1)}_id);`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Updated_at trigger
|
|
251
|
+
if (model.fields.some(f => f.name === "updated_at")) {
|
|
252
|
+
lines.push(``);
|
|
253
|
+
lines.push(`CREATE OR REPLACE FUNCTION update_${tableName}_updated_at()`);
|
|
254
|
+
lines.push(`RETURNS TRIGGER AS $$`);
|
|
255
|
+
lines.push(`BEGIN`);
|
|
256
|
+
lines.push(` NEW.updated_at = NOW();`);
|
|
257
|
+
lines.push(` RETURN NEW;`);
|
|
258
|
+
lines.push(`END;`);
|
|
259
|
+
lines.push(`$$ LANGUAGE plpgsql;`);
|
|
260
|
+
lines.push(``);
|
|
261
|
+
lines.push(`CREATE TRIGGER trg_${tableName}_updated_at`);
|
|
262
|
+
lines.push(` BEFORE UPDATE ON ${tableName}`);
|
|
263
|
+
lines.push(` FOR EACH ROW`);
|
|
264
|
+
lines.push(` EXECUTE FUNCTION update_${tableName}_updated_at();`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
lines.push(``);
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
path: `schema/${toSnakeCase(model.name)}.sql`,
|
|
271
|
+
content: lines.join("\n"),
|
|
272
|
+
language: "sql",
|
|
273
|
+
source_module: mod.id,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ─── Shared Types ──────────────────────────────────────────────────────────
|
|
278
|
+
|
|
279
|
+
private emitSharedTypes(system: IR.IRSystem): EmittedFile {
|
|
280
|
+
const lines: string[] = [];
|
|
281
|
+
lines.push(`// Generated by BoneScript compiler. DO NOT EDIT.`);
|
|
282
|
+
lines.push(`// Source: ${system.source_hash}`);
|
|
283
|
+
lines.push(``);
|
|
284
|
+
|
|
285
|
+
for (const mod of system.modules) {
|
|
286
|
+
for (const model of mod.models) {
|
|
287
|
+
lines.push(`export interface ${model.name} {`);
|
|
288
|
+
for (const field of model.fields) {
|
|
289
|
+
const nullable = field.nullable ? " | null" : "";
|
|
290
|
+
lines.push(` ${field.name}: ${toTsType(field.type)}${nullable};`);
|
|
291
|
+
}
|
|
292
|
+
lines.push(`}`);
|
|
293
|
+
lines.push(``);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Common types
|
|
298
|
+
lines.push(`export interface ServiceError {`);
|
|
299
|
+
lines.push(` code: string;`);
|
|
300
|
+
lines.push(` message: string;`);
|
|
301
|
+
lines.push(` details?: unknown;`);
|
|
302
|
+
lines.push(`}`);
|
|
303
|
+
lines.push(``);
|
|
304
|
+
lines.push(`export type Result<T, E = ServiceError> =`);
|
|
305
|
+
lines.push(` | { ok: true; value: T }`);
|
|
306
|
+
lines.push(` | { ok: false; error: E };`);
|
|
307
|
+
lines.push(``);
|
|
308
|
+
lines.push(`export interface RequestContext {`);
|
|
309
|
+
lines.push(` authenticated: boolean;`);
|
|
310
|
+
lines.push(` actor_id: string | null;`);
|
|
311
|
+
lines.push(` trace_id: string;`);
|
|
312
|
+
lines.push(` correlation_id: string;`);
|
|
313
|
+
lines.push(`}`);
|
|
314
|
+
lines.push(``);
|
|
315
|
+
lines.push(`export interface PaginatedResult<T> {`);
|
|
316
|
+
lines.push(` items: T[];`);
|
|
317
|
+
lines.push(` total: number;`);
|
|
318
|
+
lines.push(` page: number;`);
|
|
319
|
+
lines.push(` page_size: number;`);
|
|
320
|
+
lines.push(`}`);
|
|
321
|
+
lines.push(``);
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
path: `types/models.ts`,
|
|
325
|
+
content: lines.join("\n"),
|
|
326
|
+
language: "typescript",
|
|
327
|
+
source_module: "shared",
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// ─── Event Types ───────────────────────────────────────────────────────────
|
|
332
|
+
|
|
333
|
+
private emitEventTypes(system: IR.IRSystem): EmittedFile {
|
|
334
|
+
const lines: string[] = [];
|
|
335
|
+
lines.push(`// Generated by BoneScript compiler. DO NOT EDIT.`);
|
|
336
|
+
lines.push(`// Source: ${system.source_hash}`);
|
|
337
|
+
lines.push(``);
|
|
338
|
+
|
|
339
|
+
for (const ev of system.events) {
|
|
340
|
+
lines.push(`export interface ${ev.name}Event {`);
|
|
341
|
+
lines.push(` type: "${ev.name}";`);
|
|
342
|
+
lines.push(` payload: {`);
|
|
343
|
+
for (const field of ev.payload) {
|
|
344
|
+
const nullable = field.nullable ? " | null" : "";
|
|
345
|
+
lines.push(` ${field.name}: ${toTsType(field.type)}${nullable};`);
|
|
346
|
+
}
|
|
347
|
+
lines.push(` };`);
|
|
348
|
+
lines.push(` metadata: {`);
|
|
349
|
+
lines.push(` source: string;`);
|
|
350
|
+
lines.push(` timestamp: Date;`);
|
|
351
|
+
lines.push(` correlation_id: string;`);
|
|
352
|
+
lines.push(` causation_id: string;`);
|
|
353
|
+
lines.push(` };`);
|
|
354
|
+
lines.push(`}`);
|
|
355
|
+
lines.push(``);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Union type of all events
|
|
359
|
+
const eventNames = system.events.map(e => `${e.name}Event`);
|
|
360
|
+
lines.push(`export type SystemEvent = ${eventNames.join(" | ")};`);
|
|
361
|
+
lines.push(``);
|
|
362
|
+
|
|
363
|
+
// Event bus interface
|
|
364
|
+
lines.push(`export interface EventBus {`);
|
|
365
|
+
lines.push(` publish(event: SystemEvent): Promise<void>;`);
|
|
366
|
+
lines.push(` subscribe(type: string, handler: (event: SystemEvent) => Promise<void>): void;`);
|
|
367
|
+
lines.push(`}`);
|
|
368
|
+
lines.push(``);
|
|
369
|
+
|
|
370
|
+
return {
|
|
371
|
+
path: `types/events.ts`,
|
|
372
|
+
content: lines.join("\n"),
|
|
373
|
+
language: "typescript",
|
|
374
|
+
source_module: "shared",
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// ─── Service Implementation ────────────────────────────────────────────────
|
|
379
|
+
|
|
380
|
+
private emitService(mod: IR.IRModule, system: IR.IRSystem): EmittedFile {
|
|
381
|
+
const lines: string[] = [];
|
|
382
|
+
lines.push(`// Generated by BoneScript compiler. DO NOT EDIT.`);
|
|
383
|
+
lines.push(`// Source: ${system.source_hash}`);
|
|
384
|
+
lines.push(`// Module: ${mod.name} (${mod.kind})`);
|
|
385
|
+
lines.push(``);
|
|
386
|
+
lines.push(`import { Result, RequestContext, ServiceError, PaginatedResult } from "../types/models";`);
|
|
387
|
+
lines.push(``);
|
|
388
|
+
|
|
389
|
+
for (const iface of mod.interfaces) {
|
|
390
|
+
// Interface definition
|
|
391
|
+
lines.push(`export interface ${iface.name} {`);
|
|
392
|
+
for (const method of iface.methods) {
|
|
393
|
+
const params = method.input.map(f => `${f.name}: ${toTsType(f.type)}`).join(", ");
|
|
394
|
+
const ctxParam = method.authenticated ? "ctx: RequestContext" : "";
|
|
395
|
+
const allParams = [ctxParam, params].filter(Boolean).join(", ");
|
|
396
|
+
lines.push(` ${method.name}(${allParams}): Promise<Result<${toTsType(method.output)}>>;`);
|
|
397
|
+
}
|
|
398
|
+
lines.push(`}`);
|
|
399
|
+
lines.push(``);
|
|
400
|
+
|
|
401
|
+
// Implementation class
|
|
402
|
+
lines.push(`export class ${mod.name} implements ${iface.name} {`);
|
|
403
|
+
for (const method of iface.methods) {
|
|
404
|
+
lines.push(this.emitMethod(method, mod, system));
|
|
405
|
+
}
|
|
406
|
+
lines.push(`}`);
|
|
407
|
+
lines.push(``);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return {
|
|
411
|
+
path: `services/${toSnakeCase(mod.name)}.ts`,
|
|
412
|
+
content: lines.join("\n"),
|
|
413
|
+
language: "typescript",
|
|
414
|
+
source_module: mod.id,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
private emitMethod(method: IR.IRMethod, mod: IR.IRModule, system: IR.IRSystem): string {
|
|
419
|
+
const lines: string[] = [];
|
|
420
|
+
const params = method.input.map(f => `${f.name}: ${toTsType(f.type)}`).join(", ");
|
|
421
|
+
const ctxParam = method.authenticated ? "ctx: RequestContext" : "";
|
|
422
|
+
const allParams = [ctxParam, params].filter(Boolean).join(", ");
|
|
423
|
+
|
|
424
|
+
lines.push(` async ${method.name}(${allParams}): Promise<Result<${toTsType(method.output)}>> {`);
|
|
425
|
+
|
|
426
|
+
// Auth check
|
|
427
|
+
if (method.authenticated) {
|
|
428
|
+
lines.push(` // [Guard] Authentication required`);
|
|
429
|
+
lines.push(` if (!ctx.authenticated) {`);
|
|
430
|
+
lines.push(` return { ok: false, error: { code: "UNAUTHORIZED", message: "Authentication required" } };`);
|
|
431
|
+
lines.push(` }`);
|
|
432
|
+
lines.push(``);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Preconditions
|
|
436
|
+
if (method.preconditions.length > 0) {
|
|
437
|
+
lines.push(` // [Preconditions]`);
|
|
438
|
+
for (const pre of method.preconditions) {
|
|
439
|
+
lines.push(` // CHECK: ${pre.description}`);
|
|
440
|
+
}
|
|
441
|
+
lines.push(``);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Effects
|
|
445
|
+
if (method.effects.length > 0) {
|
|
446
|
+
lines.push(` // [Effects] Applied in declaration order (deterministic)`);
|
|
447
|
+
for (const eff of method.effects) {
|
|
448
|
+
const opSymbol = eff.op === "assign" ? "=" : eff.op === "add" ? "+=" : "-=";
|
|
449
|
+
lines.push(` // EFFECT: ${eff.target} ${opSymbol} ${eff.value}`);
|
|
450
|
+
}
|
|
451
|
+
lines.push(``);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Emissions
|
|
455
|
+
if (method.emissions.length > 0) {
|
|
456
|
+
lines.push(` // [Events]`);
|
|
457
|
+
for (const ev of method.emissions) {
|
|
458
|
+
lines.push(` // EMIT: ${ev}`);
|
|
459
|
+
}
|
|
460
|
+
lines.push(``);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Real implementation — delegate to emitCapabilityBody for capabilities,
|
|
464
|
+
// or generate CRUD SQL for standard methods
|
|
465
|
+
const { emitCapabilityBody } = require("./emit_capability");
|
|
466
|
+
const { emitPipelineBody, emitAlgorithmBody } = require("./emit_composition");
|
|
467
|
+
|
|
468
|
+
if (method.pipeline) {
|
|
469
|
+
lines.push(emitPipelineBody(method, " "));
|
|
470
|
+
} else if (method.algorithm) {
|
|
471
|
+
lines.push(emitAlgorithmBody(method, " "));
|
|
472
|
+
} else if (method.effects.length > 0 || method.preconditions.length > 0) {
|
|
473
|
+
// Capability with effects/preconditions — use the full capability body emitter
|
|
474
|
+
try {
|
|
475
|
+
lines.push(emitCapabilityBody(method, mod, system, " "));
|
|
476
|
+
} catch {
|
|
477
|
+
// Fallback: emit a descriptive stub if body generation fails
|
|
478
|
+
lines.push(` // Effects: ${method.effects.map((e: any) => e.target + " " + e.op + " " + e.value).join("; ")}`);
|
|
479
|
+
lines.push(` return { ok: false, error: { code: "NOT_IMPLEMENTED", message: "${method.name} not yet implemented" } };`);
|
|
480
|
+
}
|
|
481
|
+
} else {
|
|
482
|
+
// CRUD or simple method — emit a typed not-implemented stub
|
|
483
|
+
lines.push(` return { ok: false, error: { code: "NOT_IMPLEMENTED", message: "${method.name} not yet implemented" } };`);
|
|
484
|
+
}
|
|
485
|
+
lines.push(` }`);
|
|
486
|
+
lines.push(``);
|
|
487
|
+
|
|
488
|
+
return lines.join("\n");
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// ─── State Machine ─────────────────────────────────────────────────────────
|
|
492
|
+
|
|
493
|
+
private emitStateMachine(sm: IR.IRStateMachine, mod: IR.IRModule, system: IR.IRSystem): EmittedFile {
|
|
494
|
+
const lines: string[] = [];
|
|
495
|
+
lines.push(`// Generated by BoneScript compiler. DO NOT EDIT.`);
|
|
496
|
+
lines.push(`// Source: ${system.source_hash}`);
|
|
497
|
+
lines.push(`// Entity: ${sm.entity}`);
|
|
498
|
+
lines.push(``);
|
|
499
|
+
|
|
500
|
+
// State type
|
|
501
|
+
const stateUnion = sm.states.map(s => `"${s}"`).join(" | ");
|
|
502
|
+
lines.push(`export type ${sm.entity}State = ${stateUnion};`);
|
|
503
|
+
lines.push(``);
|
|
504
|
+
|
|
505
|
+
// Transition table
|
|
506
|
+
lines.push(`export const ${sm.entity.toUpperCase()}_TRANSITIONS: Record<${sm.entity}State, Record<string, ${sm.entity}State>> = {`);
|
|
507
|
+
for (const state of sm.states) {
|
|
508
|
+
const transitions = sm.transitions.filter(t => t.from === state);
|
|
509
|
+
const entries = transitions.map(t => `"${t.trigger}": "${t.to}"`).join(", ");
|
|
510
|
+
lines.push(` "${state}": { ${entries} },`);
|
|
511
|
+
}
|
|
512
|
+
lines.push(`};`);
|
|
513
|
+
lines.push(``);
|
|
514
|
+
|
|
515
|
+
// Transition function
|
|
516
|
+
lines.push(`export interface TransitionError {`);
|
|
517
|
+
lines.push(` current: ${sm.entity}State;`);
|
|
518
|
+
lines.push(` trigger: string;`);
|
|
519
|
+
lines.push(` message: string;`);
|
|
520
|
+
lines.push(`}`);
|
|
521
|
+
lines.push(``);
|
|
522
|
+
lines.push(`export function transition${sm.entity}(`);
|
|
523
|
+
lines.push(` current: ${sm.entity}State,`);
|
|
524
|
+
lines.push(` trigger: string`);
|
|
525
|
+
lines.push(`): { ok: true; state: ${sm.entity}State } | { ok: false; error: TransitionError } {`);
|
|
526
|
+
lines.push(` const next = ${sm.entity.toUpperCase()}_TRANSITIONS[current]?.[trigger];`);
|
|
527
|
+
lines.push(` if (!next) {`);
|
|
528
|
+
lines.push(` return {`);
|
|
529
|
+
lines.push(` ok: false,`);
|
|
530
|
+
lines.push(` error: {`);
|
|
531
|
+
lines.push(` current,`);
|
|
532
|
+
lines.push(` trigger,`);
|
|
533
|
+
lines.push(` message: \`Invalid transition: \${current} --[\${trigger}]--> ?\``);
|
|
534
|
+
lines.push(` }`);
|
|
535
|
+
lines.push(` };`);
|
|
536
|
+
lines.push(` }`);
|
|
537
|
+
lines.push(` return { ok: true, state: next };`);
|
|
538
|
+
lines.push(`}`);
|
|
539
|
+
lines.push(``);
|
|
540
|
+
|
|
541
|
+
// Initial state
|
|
542
|
+
lines.push(`export const ${sm.entity.toUpperCase()}_INITIAL: ${sm.entity}State = "${sm.initial}";`);
|
|
543
|
+
lines.push(``);
|
|
544
|
+
|
|
545
|
+
return {
|
|
546
|
+
path: `services/state_machines/${toSnakeCase(sm.entity)}_states.ts`,
|
|
547
|
+
content: lines.join("\n"),
|
|
548
|
+
language: "typescript",
|
|
549
|
+
source_module: mod.id,
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// ─── Service Config (YAML) ─────────────────────────────────────────────────
|
|
554
|
+
|
|
555
|
+
private emitServiceConfig(system: IR.IRSystem): EmittedFile {
|
|
556
|
+
const lines: string[] = [];
|
|
557
|
+
lines.push(`# Generated by BoneScript compiler. DO NOT EDIT.`);
|
|
558
|
+
lines.push(`# Source: ${system.source_hash}`);
|
|
559
|
+
lines.push(``);
|
|
560
|
+
lines.push(`system:`);
|
|
561
|
+
lines.push(` name: ${system.name}`);
|
|
562
|
+
lines.push(` version: ${system.version}`);
|
|
563
|
+
lines.push(` domain: ${system.domain || "generic"}`);
|
|
564
|
+
lines.push(``);
|
|
565
|
+
lines.push(`services:`);
|
|
566
|
+
|
|
567
|
+
for (const mod of system.modules) {
|
|
568
|
+
if (mod.kind === "api_service" || mod.kind === "realtime_service") {
|
|
569
|
+
lines.push(` - name: ${toSnakeCase(mod.name)}`);
|
|
570
|
+
lines.push(` kind: ${mod.kind}`);
|
|
571
|
+
lines.push(` dependencies:`);
|
|
572
|
+
for (const dep of mod.dependencies) {
|
|
573
|
+
const depMod = system.modules.find(m => m.id === dep);
|
|
574
|
+
if (depMod) lines.push(` - ${toSnakeCase(depMod.name)}`);
|
|
575
|
+
}
|
|
576
|
+
for (const [key, val] of Object.entries(mod.config)) {
|
|
577
|
+
lines.push(` ${key}: ${val}`);
|
|
578
|
+
}
|
|
579
|
+
lines.push(``);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
return {
|
|
584
|
+
path: `config/services.yaml`,
|
|
585
|
+
content: lines.join("\n"),
|
|
586
|
+
language: "yaml",
|
|
587
|
+
source_module: "config",
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// ─── Infrastructure Config (YAML) ──────────────────────────────────────────
|
|
592
|
+
|
|
593
|
+
private emitInfraConfig(system: IR.IRSystem): EmittedFile {
|
|
594
|
+
const lines: string[] = [];
|
|
595
|
+
lines.push(`# Generated by BoneScript compiler. DO NOT EDIT.`);
|
|
596
|
+
lines.push(`# Source: ${system.source_hash}`);
|
|
597
|
+
lines.push(``);
|
|
598
|
+
lines.push(`infrastructure:`);
|
|
599
|
+
|
|
600
|
+
// Data stores
|
|
601
|
+
lines.push(` datastores:`);
|
|
602
|
+
for (const mod of system.modules) {
|
|
603
|
+
if (mod.kind === "data_store") {
|
|
604
|
+
lines.push(` - name: ${toSnakeCase(mod.name)}`);
|
|
605
|
+
lines.push(` engine: ${mod.config["engine"] || "postgresql"}`);
|
|
606
|
+
lines.push(` replicas: ${mod.config["replicas"] || 1}`);
|
|
607
|
+
if (mod.config["retention_ms"]) lines.push(` retention_ms: ${mod.config["retention_ms"]}`);
|
|
608
|
+
if (mod.config["partition_key"]) lines.push(` partition_key: ${mod.config["partition_key"]}`);
|
|
609
|
+
lines.push(``);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Gateway
|
|
614
|
+
lines.push(` gateway:`);
|
|
615
|
+
const gw = system.modules.find(m => m.kind === "gateway");
|
|
616
|
+
if (gw) {
|
|
617
|
+
for (const [key, val] of Object.entries(gw.config)) {
|
|
618
|
+
lines.push(` ${key}: ${val}`);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
lines.push(``);
|
|
622
|
+
|
|
623
|
+
// Events
|
|
624
|
+
if (system.events.length > 0) {
|
|
625
|
+
lines.push(` events:`);
|
|
626
|
+
for (const ev of system.events) {
|
|
627
|
+
lines.push(` - name: ${ev.name}`);
|
|
628
|
+
lines.push(` delivery: ${ev.delivery}`);
|
|
629
|
+
lines.push(` ordering: ${ev.ordering}`);
|
|
630
|
+
if (ev.ttl_ms) lines.push(` ttl_ms: ${ev.ttl_ms}`);
|
|
631
|
+
lines.push(``);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
return {
|
|
636
|
+
path: `config/infrastructure.yaml`,
|
|
637
|
+
content: lines.join("\n"),
|
|
638
|
+
language: "yaml",
|
|
639
|
+
source_module: "config",
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
}
|