bonescript-compiler 0.2.1 → 0.3.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.
Files changed (146) hide show
  1. package/LICENSE +21 -21
  2. package/dist/algorithm_catalog.js +166 -166
  3. package/dist/cli.d.ts +2 -1
  4. package/dist/cli.js +75 -543
  5. package/dist/cli.js.map +1 -1
  6. package/dist/commands/check.d.ts +5 -0
  7. package/dist/commands/check.js +34 -0
  8. package/dist/commands/check.js.map +1 -0
  9. package/dist/commands/compile.d.ts +5 -0
  10. package/dist/commands/compile.js +183 -0
  11. package/dist/commands/compile.js.map +1 -0
  12. package/dist/commands/debug.d.ts +5 -0
  13. package/dist/commands/debug.js +59 -0
  14. package/dist/commands/debug.js.map +1 -0
  15. package/dist/commands/diff.d.ts +5 -0
  16. package/dist/commands/diff.js +125 -0
  17. package/dist/commands/diff.js.map +1 -0
  18. package/dist/commands/fmt.d.ts +5 -0
  19. package/dist/commands/fmt.js +49 -0
  20. package/dist/commands/fmt.js.map +1 -0
  21. package/dist/commands/init.d.ts +5 -0
  22. package/dist/commands/init.js +69 -0
  23. package/dist/commands/init.js.map +1 -0
  24. package/dist/commands/ir.d.ts +5 -0
  25. package/dist/commands/ir.js +27 -0
  26. package/dist/commands/ir.js.map +1 -0
  27. package/dist/commands/lex.d.ts +5 -0
  28. package/dist/commands/lex.js +21 -0
  29. package/dist/commands/lex.js.map +1 -0
  30. package/dist/commands/parse.d.ts +5 -0
  31. package/dist/commands/parse.js +30 -0
  32. package/dist/commands/parse.js.map +1 -0
  33. package/dist/commands/test.d.ts +5 -0
  34. package/dist/commands/test.js +61 -0
  35. package/dist/commands/test.js.map +1 -0
  36. package/dist/commands/verify_determinism.d.ts +5 -0
  37. package/dist/commands/verify_determinism.js +64 -0
  38. package/dist/commands/verify_determinism.js.map +1 -0
  39. package/dist/commands/watch.d.ts +5 -0
  40. package/dist/commands/watch.js +50 -0
  41. package/dist/commands/watch.js.map +1 -0
  42. package/dist/emit_auth.d.ts +6 -0
  43. package/dist/emit_auth.js +69 -0
  44. package/dist/emit_auth.js.map +1 -0
  45. package/dist/emit_capability.d.ts +13 -0
  46. package/dist/emit_capability.js +235 -125
  47. package/dist/emit_capability.js.map +1 -1
  48. package/dist/emit_database.d.ts +7 -0
  49. package/dist/emit_database.js +74 -0
  50. package/dist/emit_database.js.map +1 -0
  51. package/dist/emit_deploy.js +162 -162
  52. package/dist/emit_events.js +274 -274
  53. package/dist/emit_full.js +102 -95
  54. package/dist/emit_full.js.map +1 -1
  55. package/dist/emit_index.d.ts +6 -0
  56. package/dist/emit_index.js +157 -0
  57. package/dist/emit_index.js.map +1 -0
  58. package/dist/emit_maintenance.js +249 -249
  59. package/dist/emit_package.d.ts +7 -0
  60. package/dist/emit_package.js +70 -0
  61. package/dist/emit_package.js.map +1 -0
  62. package/dist/emit_router.d.ts +12 -0
  63. package/dist/emit_router.js +375 -0
  64. package/dist/emit_router.js.map +1 -0
  65. package/dist/emit_runtime.d.ts +17 -11
  66. package/dist/emit_runtime.js +29 -686
  67. package/dist/emit_runtime.js.map +1 -1
  68. package/dist/emit_sourcemap.js +66 -66
  69. package/dist/extension_manager.d.ts +2 -2
  70. package/dist/extension_manager.js +6 -3
  71. package/dist/extension_manager.js.map +1 -1
  72. package/dist/lowering.d.ts +5 -14
  73. package/dist/lowering.js +32 -417
  74. package/dist/lowering.js.map +1 -1
  75. package/dist/lowering_channels.d.ts +11 -0
  76. package/dist/lowering_channels.js +102 -0
  77. package/dist/lowering_channels.js.map +1 -0
  78. package/dist/lowering_entities.d.ts +11 -0
  79. package/dist/lowering_entities.js +222 -0
  80. package/dist/lowering_entities.js.map +1 -0
  81. package/dist/lowering_helpers.d.ts +13 -0
  82. package/dist/lowering_helpers.js +76 -0
  83. package/dist/lowering_helpers.js.map +1 -0
  84. package/dist/module_loader.d.ts +2 -2
  85. package/dist/module_loader.js +20 -23
  86. package/dist/module_loader.js.map +1 -1
  87. package/dist/scaffold.d.ts +2 -2
  88. package/dist/scaffold.js +316 -319
  89. package/dist/scaffold.js.map +1 -1
  90. package/package.json +62 -52
  91. package/src/algorithm_catalog.ts +345 -345
  92. package/src/ast.ts +334 -334
  93. package/src/cli.ts +98 -624
  94. package/src/commands/check.ts +33 -0
  95. package/src/commands/compile.ts +160 -0
  96. package/src/commands/debug.ts +33 -0
  97. package/src/commands/diff.ts +108 -0
  98. package/src/commands/fmt.ts +22 -0
  99. package/src/commands/init.ts +46 -0
  100. package/src/commands/ir.ts +23 -0
  101. package/src/commands/lex.ts +17 -0
  102. package/src/commands/parse.ts +24 -0
  103. package/src/commands/test.ts +36 -0
  104. package/src/commands/verify_determinism.ts +66 -0
  105. package/src/commands/watch.ts +25 -0
  106. package/src/emit_auth.ts +67 -0
  107. package/src/emit_batch.ts +140 -140
  108. package/src/emit_capability.ts +562 -436
  109. package/src/emit_composition.ts +196 -196
  110. package/src/emit_database.ts +75 -0
  111. package/src/emit_deploy.ts +190 -190
  112. package/src/emit_events.ts +307 -307
  113. package/src/emit_extras.ts +240 -240
  114. package/src/emit_full.ts +316 -309
  115. package/src/emit_index.ts +161 -0
  116. package/src/emit_maintenance.ts +459 -459
  117. package/src/emit_package.ts +69 -0
  118. package/src/emit_router.ts +395 -0
  119. package/src/emit_runtime.ts +17 -728
  120. package/src/emit_sourcemap.ts +140 -140
  121. package/src/emit_tests.ts +205 -205
  122. package/src/emit_websocket.ts +229 -229
  123. package/src/emitter.ts +566 -566
  124. package/src/extension_manager.ts +189 -187
  125. package/src/formatter.ts +297 -297
  126. package/src/index.ts +88 -88
  127. package/src/ir.ts +215 -215
  128. package/src/lexer.ts +630 -630
  129. package/src/lowering.ts +124 -556
  130. package/src/lowering_channels.ts +107 -0
  131. package/src/lowering_entities.ts +248 -0
  132. package/src/lowering_helpers.ts +75 -0
  133. package/src/module_loader.ts +112 -114
  134. package/src/optimizer.ts +196 -196
  135. package/src/parse_decls.ts +409 -409
  136. package/src/parse_decls2.ts +244 -244
  137. package/src/parse_expr.ts +197 -197
  138. package/src/parse_types.ts +54 -54
  139. package/src/parser.ts +1 -1
  140. package/src/parser_base.ts +57 -57
  141. package/src/parser_recovery.ts +153 -153
  142. package/src/scaffold.ts +372 -375
  143. package/src/solver.ts +330 -330
  144. package/src/typechecker.ts +591 -591
  145. package/src/types.ts +122 -122
  146. package/src/verifier.ts +348 -348
package/src/emitter.ts CHANGED
@@ -1,566 +1,566 @@
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
- function toSnakeCase(s: string): string {
66
- return s.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase();
67
- }
68
-
69
- // ─── Emitter ─────────────────────────────────────────────────────────────────
70
-
71
- export class Emitter {
72
- emit(system: IR.IRSystem): EmittedFile[] {
73
- const files: EmittedFile[] = [];
74
-
75
- // 1. Schema files (SQL)
76
- for (const mod of system.modules) {
77
- if (mod.kind === "data_store" || mod.kind === "api_service") {
78
- for (const model of mod.models) {
79
- files.push(this.emitSchema(model, mod, system));
80
- }
81
- }
82
- }
83
-
84
- // 2. Type definition files (TypeScript)
85
- files.push(this.emitSharedTypes(system));
86
-
87
- // 3. Event types (TypeScript)
88
- if (system.events.length > 0) {
89
- files.push(this.emitEventTypes(system));
90
- }
91
-
92
- // 4. Service files (TypeScript)
93
- for (const mod of system.modules) {
94
- if (mod.kind === "api_service") {
95
- files.push(this.emitService(mod, system));
96
- }
97
- }
98
-
99
- // 5. State machine files (TypeScript)
100
- for (const mod of system.modules) {
101
- for (const sm of mod.state_machines) {
102
- files.push(this.emitStateMachine(sm, mod, system));
103
- }
104
- }
105
-
106
- // 6. Config files (YAML)
107
- files.push(this.emitServiceConfig(system));
108
-
109
- // 7. Infrastructure config (YAML)
110
- files.push(this.emitInfraConfig(system));
111
-
112
- return files;
113
- }
114
-
115
- // ─── SQL Schema ────────────────────────────────────────────────────────────
116
-
117
- private emitSchema(model: IR.IRModel, mod: IR.IRModule, system: IR.IRSystem): EmittedFile {
118
- const tableName = toSnakeCase(model.name) + "s";
119
- const lines: string[] = [];
120
-
121
- lines.push(`-- Generated by BoneScript compiler. DO NOT EDIT.`);
122
- lines.push(`-- Source: ${system.source_hash}`);
123
- lines.push(`-- Module: ${mod.name}`);
124
- lines.push(``);
125
- lines.push(`CREATE TABLE IF NOT EXISTS ${tableName} (`);
126
-
127
- const fieldLines: string[] = [];
128
- for (const field of model.fields) {
129
- let line = ` ${field.name} ${toSqlType(field.type)}`;
130
- if (!field.nullable) line += " NOT NULL";
131
- if (field.default_value) {
132
- if (field.default_value === "gen_random_uuid()") line += " DEFAULT gen_random_uuid()";
133
- else if (field.default_value === "now()") line += " DEFAULT NOW()";
134
- else line += ` DEFAULT ${field.default_value}`;
135
- } else if (field.name === "created_at" || field.name === "updated_at") {
136
- // Always add DEFAULT NOW() for timestamp audit fields
137
- line += " DEFAULT NOW()";
138
- } else if (field.name === "id" && field.type === "uuid") {
139
- // Always add DEFAULT gen_random_uuid() for uuid primary keys
140
- line += " DEFAULT gen_random_uuid()";
141
- }
142
- if (field.name === model.primary_key) line += " PRIMARY KEY";
143
- fieldLines.push(line);
144
- }
145
-
146
- // Add unique constraints
147
- for (const c of model.constraints) {
148
- if (c.kind === "unique") {
149
- fieldLines.push(` CONSTRAINT ${tableName}_${c.target}_unique UNIQUE (${c.target})`);
150
- }
151
- }
152
-
153
- // Add foreign key constraints from relations
154
- for (const rel of mod.relations || []) {
155
- if (rel.kind === "belongs_to") {
156
- // belongs_to: FK is on this table
157
- fieldLines.push(` CONSTRAINT fk_${tableName}_${rel.foreign_key} FOREIGN KEY (${rel.foreign_key}) REFERENCES ${rel.to_table}(id) ON DELETE CASCADE`);
158
- }
159
- }
160
-
161
- lines.push(fieldLines.join(",\n"));
162
- lines.push(`);`);
163
- lines.push(``);
164
-
165
- // Indexes
166
- for (const idx of model.indexes) {
167
- const idxName = `idx_${tableName}_${idx.fields.join("_")}`;
168
- const unique = idx.unique ? "UNIQUE " : "";
169
- lines.push(`CREATE ${unique}INDEX IF NOT EXISTS ${idxName} ON ${tableName} (${idx.fields.join(", ")});`);
170
- }
171
-
172
- // FK indexes for belongs_to relations
173
- for (const rel of mod.relations || []) {
174
- if (rel.kind === "belongs_to") {
175
- lines.push(`CREATE INDEX IF NOT EXISTS idx_${tableName}_${rel.foreign_key} ON ${tableName} (${rel.foreign_key});`);
176
- }
177
- }
178
-
179
- // Junction tables for many_to_many
180
- for (const rel of mod.relations || []) {
181
- if (rel.kind === "many_to_many" && rel.junction_table) {
182
- lines.push(``);
183
- lines.push(`CREATE TABLE IF NOT EXISTS ${rel.junction_table} (`);
184
- lines.push(` ${rel.from_table.slice(0, -1)}_id UUID NOT NULL REFERENCES ${rel.from_table}(id) ON DELETE CASCADE,`);
185
- lines.push(` ${rel.to_table.slice(0, -1)}_id UUID NOT NULL REFERENCES ${rel.to_table}(id) ON DELETE CASCADE,`);
186
- lines.push(` created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),`);
187
- lines.push(` PRIMARY KEY (${rel.from_table.slice(0, -1)}_id, ${rel.to_table.slice(0, -1)}_id)`);
188
- lines.push(`);`);
189
- 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);`);
190
- 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);`);
191
- }
192
- }
193
-
194
- // Updated_at trigger
195
- if (model.fields.some(f => f.name === "updated_at")) {
196
- lines.push(``);
197
- lines.push(`CREATE OR REPLACE FUNCTION update_${tableName}_updated_at()`);
198
- lines.push(`RETURNS TRIGGER AS $$`);
199
- lines.push(`BEGIN`);
200
- lines.push(` NEW.updated_at = NOW();`);
201
- lines.push(` RETURN NEW;`);
202
- lines.push(`END;`);
203
- lines.push(`$$ LANGUAGE plpgsql;`);
204
- lines.push(``);
205
- lines.push(`CREATE TRIGGER trg_${tableName}_updated_at`);
206
- lines.push(` BEFORE UPDATE ON ${tableName}`);
207
- lines.push(` FOR EACH ROW`);
208
- lines.push(` EXECUTE FUNCTION update_${tableName}_updated_at();`);
209
- }
210
-
211
- lines.push(``);
212
-
213
- return {
214
- path: `schema/${toSnakeCase(model.name)}.sql`,
215
- content: lines.join("\n"),
216
- language: "sql",
217
- source_module: mod.id,
218
- };
219
- }
220
-
221
- // ─── Shared Types ──────────────────────────────────────────────────────────
222
-
223
- private emitSharedTypes(system: IR.IRSystem): EmittedFile {
224
- const lines: string[] = [];
225
- lines.push(`// Generated by BoneScript compiler. DO NOT EDIT.`);
226
- lines.push(`// Source: ${system.source_hash}`);
227
- lines.push(``);
228
-
229
- for (const mod of system.modules) {
230
- for (const model of mod.models) {
231
- lines.push(`export interface ${model.name} {`);
232
- for (const field of model.fields) {
233
- const nullable = field.nullable ? " | null" : "";
234
- lines.push(` ${field.name}: ${toTsType(field.type)}${nullable};`);
235
- }
236
- lines.push(`}`);
237
- lines.push(``);
238
- }
239
- }
240
-
241
- // Common types
242
- lines.push(`export interface ServiceError {`);
243
- lines.push(` code: string;`);
244
- lines.push(` message: string;`);
245
- lines.push(` details?: unknown;`);
246
- lines.push(`}`);
247
- lines.push(``);
248
- lines.push(`export type Result<T, E = ServiceError> =`);
249
- lines.push(` | { ok: true; value: T }`);
250
- lines.push(` | { ok: false; error: E };`);
251
- lines.push(``);
252
- lines.push(`export interface RequestContext {`);
253
- lines.push(` authenticated: boolean;`);
254
- lines.push(` actor_id: string | null;`);
255
- lines.push(` trace_id: string;`);
256
- lines.push(` correlation_id: string;`);
257
- lines.push(`}`);
258
- lines.push(``);
259
- lines.push(`export interface PaginatedResult<T> {`);
260
- lines.push(` items: T[];`);
261
- lines.push(` total: number;`);
262
- lines.push(` page: number;`);
263
- lines.push(` page_size: number;`);
264
- lines.push(`}`);
265
- lines.push(``);
266
-
267
- return {
268
- path: `types/models.ts`,
269
- content: lines.join("\n"),
270
- language: "typescript",
271
- source_module: "shared",
272
- };
273
- }
274
-
275
- // ─── Event Types ───────────────────────────────────────────────────────────
276
-
277
- private emitEventTypes(system: IR.IRSystem): EmittedFile {
278
- const lines: string[] = [];
279
- lines.push(`// Generated by BoneScript compiler. DO NOT EDIT.`);
280
- lines.push(`// Source: ${system.source_hash}`);
281
- lines.push(``);
282
-
283
- for (const ev of system.events) {
284
- lines.push(`export interface ${ev.name}Event {`);
285
- lines.push(` type: "${ev.name}";`);
286
- lines.push(` payload: {`);
287
- for (const field of ev.payload) {
288
- const nullable = field.nullable ? " | null" : "";
289
- lines.push(` ${field.name}: ${toTsType(field.type)}${nullable};`);
290
- }
291
- lines.push(` };`);
292
- lines.push(` metadata: {`);
293
- lines.push(` source: string;`);
294
- lines.push(` timestamp: Date;`);
295
- lines.push(` correlation_id: string;`);
296
- lines.push(` causation_id: string;`);
297
- lines.push(` };`);
298
- lines.push(`}`);
299
- lines.push(``);
300
- }
301
-
302
- // Union type of all events
303
- const eventNames = system.events.map(e => `${e.name}Event`);
304
- lines.push(`export type SystemEvent = ${eventNames.join(" | ")};`);
305
- lines.push(``);
306
-
307
- // Event bus interface
308
- lines.push(`export interface EventBus {`);
309
- lines.push(` publish(event: SystemEvent): Promise<void>;`);
310
- lines.push(` subscribe(type: string, handler: (event: SystemEvent) => Promise<void>): void;`);
311
- lines.push(`}`);
312
- lines.push(``);
313
-
314
- return {
315
- path: `types/events.ts`,
316
- content: lines.join("\n"),
317
- language: "typescript",
318
- source_module: "shared",
319
- };
320
- }
321
-
322
- // ─── Service Implementation ────────────────────────────────────────────────
323
-
324
- private emitService(mod: IR.IRModule, system: IR.IRSystem): EmittedFile {
325
- const lines: string[] = [];
326
- lines.push(`// Generated by BoneScript compiler. DO NOT EDIT.`);
327
- lines.push(`// Source: ${system.source_hash}`);
328
- lines.push(`// Module: ${mod.name} (${mod.kind})`);
329
- lines.push(``);
330
- lines.push(`import { Result, RequestContext, ServiceError, PaginatedResult } from "../types/models";`);
331
- lines.push(``);
332
-
333
- for (const iface of mod.interfaces) {
334
- // Interface definition
335
- lines.push(`export interface ${iface.name} {`);
336
- for (const method of iface.methods) {
337
- const params = method.input.map(f => `${f.name}: ${toTsType(f.type)}`).join(", ");
338
- const ctxParam = method.authenticated ? "ctx: RequestContext" : "";
339
- const allParams = [ctxParam, params].filter(Boolean).join(", ");
340
- lines.push(` ${method.name}(${allParams}): Promise<Result<${toTsType(method.output)}>>;`);
341
- }
342
- lines.push(`}`);
343
- lines.push(``);
344
-
345
- // Implementation class
346
- lines.push(`export class ${mod.name} implements ${iface.name} {`);
347
- for (const method of iface.methods) {
348
- lines.push(this.emitMethod(method));
349
- }
350
- lines.push(`}`);
351
- lines.push(``);
352
- }
353
-
354
- return {
355
- path: `services/${toSnakeCase(mod.name)}.ts`,
356
- content: lines.join("\n"),
357
- language: "typescript",
358
- source_module: mod.id,
359
- };
360
- }
361
-
362
- private emitMethod(method: IR.IRMethod): string {
363
- const lines: string[] = [];
364
- const params = method.input.map(f => `${f.name}: ${toTsType(f.type)}`).join(", ");
365
- const ctxParam = method.authenticated ? "ctx: RequestContext" : "";
366
- const allParams = [ctxParam, params].filter(Boolean).join(", ");
367
-
368
- lines.push(` async ${method.name}(${allParams}): Promise<Result<${toTsType(method.output)}>> {`);
369
-
370
- // Auth check
371
- if (method.authenticated) {
372
- lines.push(` // [Guard] Authentication required`);
373
- lines.push(` if (!ctx.authenticated) {`);
374
- lines.push(` return { ok: false, error: { code: "UNAUTHORIZED", message: "Authentication required" } };`);
375
- lines.push(` }`);
376
- lines.push(``);
377
- }
378
-
379
- // Preconditions
380
- if (method.preconditions.length > 0) {
381
- lines.push(` // [Preconditions]`);
382
- for (const pre of method.preconditions) {
383
- lines.push(` // CHECK: ${pre.description}`);
384
- }
385
- lines.push(``);
386
- }
387
-
388
- // Effects
389
- if (method.effects.length > 0) {
390
- lines.push(` // [Effects] Applied in declaration order (deterministic)`);
391
- for (const eff of method.effects) {
392
- const opSymbol = eff.op === "assign" ? "=" : eff.op === "add" ? "+=" : "-=";
393
- lines.push(` // EFFECT: ${eff.target} ${opSymbol} ${eff.value}`);
394
- }
395
- lines.push(``);
396
- }
397
-
398
- // Emissions
399
- if (method.emissions.length > 0) {
400
- lines.push(` // [Events]`);
401
- for (const ev of method.emissions) {
402
- lines.push(` // EMIT: ${ev}`);
403
- }
404
- lines.push(``);
405
- }
406
-
407
- lines.push(` // TODO: Implementation`);
408
- lines.push(` throw new Error("Not implemented: ${method.name}");`);
409
- lines.push(` }`);
410
- lines.push(``);
411
-
412
- return lines.join("\n");
413
- }
414
-
415
- // ─── State Machine ─────────────────────────────────────────────────────────
416
-
417
- private emitStateMachine(sm: IR.IRStateMachine, mod: IR.IRModule, system: IR.IRSystem): EmittedFile {
418
- const lines: string[] = [];
419
- lines.push(`// Generated by BoneScript compiler. DO NOT EDIT.`);
420
- lines.push(`// Source: ${system.source_hash}`);
421
- lines.push(`// Entity: ${sm.entity}`);
422
- lines.push(``);
423
-
424
- // State type
425
- const stateUnion = sm.states.map(s => `"${s}"`).join(" | ");
426
- lines.push(`export type ${sm.entity}State = ${stateUnion};`);
427
- lines.push(``);
428
-
429
- // Transition table
430
- lines.push(`export const ${sm.entity.toUpperCase()}_TRANSITIONS: Record<${sm.entity}State, Record<string, ${sm.entity}State>> = {`);
431
- for (const state of sm.states) {
432
- const transitions = sm.transitions.filter(t => t.from === state);
433
- const entries = transitions.map(t => `"${t.trigger}": "${t.to}"`).join(", ");
434
- lines.push(` "${state}": { ${entries} },`);
435
- }
436
- lines.push(`};`);
437
- lines.push(``);
438
-
439
- // Transition function
440
- lines.push(`export interface TransitionError {`);
441
- lines.push(` current: ${sm.entity}State;`);
442
- lines.push(` trigger: string;`);
443
- lines.push(` message: string;`);
444
- lines.push(`}`);
445
- lines.push(``);
446
- lines.push(`export function transition${sm.entity}(`);
447
- lines.push(` current: ${sm.entity}State,`);
448
- lines.push(` trigger: string`);
449
- lines.push(`): { ok: true; state: ${sm.entity}State } | { ok: false; error: TransitionError } {`);
450
- lines.push(` const next = ${sm.entity.toUpperCase()}_TRANSITIONS[current]?.[trigger];`);
451
- lines.push(` if (!next) {`);
452
- lines.push(` return {`);
453
- lines.push(` ok: false,`);
454
- lines.push(` error: {`);
455
- lines.push(` current,`);
456
- lines.push(` trigger,`);
457
- lines.push(` message: \`Invalid transition: \${current} --[\${trigger}]--> ?\``);
458
- lines.push(` }`);
459
- lines.push(` };`);
460
- lines.push(` }`);
461
- lines.push(` return { ok: true, state: next };`);
462
- lines.push(`}`);
463
- lines.push(``);
464
-
465
- // Initial state
466
- lines.push(`export const ${sm.entity.toUpperCase()}_INITIAL: ${sm.entity}State = "${sm.initial}";`);
467
- lines.push(``);
468
-
469
- return {
470
- path: `services/state_machines/${toSnakeCase(sm.entity)}_states.ts`,
471
- content: lines.join("\n"),
472
- language: "typescript",
473
- source_module: mod.id,
474
- };
475
- }
476
-
477
- // ─── Service Config (YAML) ─────────────────────────────────────────────────
478
-
479
- private emitServiceConfig(system: IR.IRSystem): EmittedFile {
480
- const lines: string[] = [];
481
- lines.push(`# Generated by BoneScript compiler. DO NOT EDIT.`);
482
- lines.push(`# Source: ${system.source_hash}`);
483
- lines.push(``);
484
- lines.push(`system:`);
485
- lines.push(` name: ${system.name}`);
486
- lines.push(` version: ${system.version}`);
487
- lines.push(` domain: ${system.domain || "generic"}`);
488
- lines.push(``);
489
- lines.push(`services:`);
490
-
491
- for (const mod of system.modules) {
492
- if (mod.kind === "api_service" || mod.kind === "realtime_service") {
493
- lines.push(` - name: ${toSnakeCase(mod.name)}`);
494
- lines.push(` kind: ${mod.kind}`);
495
- lines.push(` dependencies:`);
496
- for (const dep of mod.dependencies) {
497
- const depMod = system.modules.find(m => m.id === dep);
498
- if (depMod) lines.push(` - ${toSnakeCase(depMod.name)}`);
499
- }
500
- for (const [key, val] of Object.entries(mod.config)) {
501
- lines.push(` ${key}: ${val}`);
502
- }
503
- lines.push(``);
504
- }
505
- }
506
-
507
- return {
508
- path: `config/services.yaml`,
509
- content: lines.join("\n"),
510
- language: "yaml",
511
- source_module: "config",
512
- };
513
- }
514
-
515
- // ─── Infrastructure Config (YAML) ──────────────────────────────────────────
516
-
517
- private emitInfraConfig(system: IR.IRSystem): EmittedFile {
518
- const lines: string[] = [];
519
- lines.push(`# Generated by BoneScript compiler. DO NOT EDIT.`);
520
- lines.push(`# Source: ${system.source_hash}`);
521
- lines.push(``);
522
- lines.push(`infrastructure:`);
523
-
524
- // Data stores
525
- lines.push(` datastores:`);
526
- for (const mod of system.modules) {
527
- if (mod.kind === "data_store") {
528
- lines.push(` - name: ${toSnakeCase(mod.name)}`);
529
- lines.push(` engine: ${mod.config["engine"] || "postgresql"}`);
530
- lines.push(` replicas: ${mod.config["replicas"] || 1}`);
531
- if (mod.config["retention_ms"]) lines.push(` retention_ms: ${mod.config["retention_ms"]}`);
532
- if (mod.config["partition_key"]) lines.push(` partition_key: ${mod.config["partition_key"]}`);
533
- lines.push(``);
534
- }
535
- }
536
-
537
- // Gateway
538
- lines.push(` gateway:`);
539
- const gw = system.modules.find(m => m.kind === "gateway");
540
- if (gw) {
541
- for (const [key, val] of Object.entries(gw.config)) {
542
- lines.push(` ${key}: ${val}`);
543
- }
544
- }
545
- lines.push(``);
546
-
547
- // Events
548
- if (system.events.length > 0) {
549
- lines.push(` events:`);
550
- for (const ev of system.events) {
551
- lines.push(` - name: ${ev.name}`);
552
- lines.push(` delivery: ${ev.delivery}`);
553
- lines.push(` ordering: ${ev.ordering}`);
554
- if (ev.ttl_ms) lines.push(` ttl_ms: ${ev.ttl_ms}`);
555
- lines.push(``);
556
- }
557
- }
558
-
559
- return {
560
- path: `config/infrastructure.yaml`,
561
- content: lines.join("\n"),
562
- language: "yaml",
563
- source_module: "config",
564
- };
565
- }
566
- }
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
+ function toSnakeCase(s: string): string {
66
+ return s.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase();
67
+ }
68
+
69
+ // ─── Emitter ─────────────────────────────────────────────────────────────────
70
+
71
+ export class Emitter {
72
+ emit(system: IR.IRSystem): EmittedFile[] {
73
+ const files: EmittedFile[] = [];
74
+
75
+ // 1. Schema files (SQL)
76
+ for (const mod of system.modules) {
77
+ if (mod.kind === "data_store" || mod.kind === "api_service") {
78
+ for (const model of mod.models) {
79
+ files.push(this.emitSchema(model, mod, system));
80
+ }
81
+ }
82
+ }
83
+
84
+ // 2. Type definition files (TypeScript)
85
+ files.push(this.emitSharedTypes(system));
86
+
87
+ // 3. Event types (TypeScript)
88
+ if (system.events.length > 0) {
89
+ files.push(this.emitEventTypes(system));
90
+ }
91
+
92
+ // 4. Service files (TypeScript)
93
+ for (const mod of system.modules) {
94
+ if (mod.kind === "api_service") {
95
+ files.push(this.emitService(mod, system));
96
+ }
97
+ }
98
+
99
+ // 5. State machine files (TypeScript)
100
+ for (const mod of system.modules) {
101
+ for (const sm of mod.state_machines) {
102
+ files.push(this.emitStateMachine(sm, mod, system));
103
+ }
104
+ }
105
+
106
+ // 6. Config files (YAML)
107
+ files.push(this.emitServiceConfig(system));
108
+
109
+ // 7. Infrastructure config (YAML)
110
+ files.push(this.emitInfraConfig(system));
111
+
112
+ return files;
113
+ }
114
+
115
+ // ─── SQL Schema ────────────────────────────────────────────────────────────
116
+
117
+ private emitSchema(model: IR.IRModel, mod: IR.IRModule, system: IR.IRSystem): EmittedFile {
118
+ const tableName = toSnakeCase(model.name) + "s";
119
+ const lines: string[] = [];
120
+
121
+ lines.push(`-- Generated by BoneScript compiler. DO NOT EDIT.`);
122
+ lines.push(`-- Source: ${system.source_hash}`);
123
+ lines.push(`-- Module: ${mod.name}`);
124
+ lines.push(``);
125
+ lines.push(`CREATE TABLE IF NOT EXISTS ${tableName} (`);
126
+
127
+ const fieldLines: string[] = [];
128
+ for (const field of model.fields) {
129
+ let line = ` ${field.name} ${toSqlType(field.type)}`;
130
+ if (!field.nullable) line += " NOT NULL";
131
+ if (field.default_value) {
132
+ if (field.default_value === "gen_random_uuid()") line += " DEFAULT gen_random_uuid()";
133
+ else if (field.default_value === "now()") line += " DEFAULT NOW()";
134
+ else line += ` DEFAULT ${field.default_value}`;
135
+ } else if (field.name === "created_at" || field.name === "updated_at") {
136
+ // Always add DEFAULT NOW() for timestamp audit fields
137
+ line += " DEFAULT NOW()";
138
+ } else if (field.name === "id" && field.type === "uuid") {
139
+ // Always add DEFAULT gen_random_uuid() for uuid primary keys
140
+ line += " DEFAULT gen_random_uuid()";
141
+ }
142
+ if (field.name === model.primary_key) line += " PRIMARY KEY";
143
+ fieldLines.push(line);
144
+ }
145
+
146
+ // Add unique constraints
147
+ for (const c of model.constraints) {
148
+ if (c.kind === "unique") {
149
+ fieldLines.push(` CONSTRAINT ${tableName}_${c.target}_unique UNIQUE (${c.target})`);
150
+ }
151
+ }
152
+
153
+ // Add foreign key constraints from relations
154
+ for (const rel of mod.relations || []) {
155
+ if (rel.kind === "belongs_to") {
156
+ // belongs_to: FK is on this table
157
+ fieldLines.push(` CONSTRAINT fk_${tableName}_${rel.foreign_key} FOREIGN KEY (${rel.foreign_key}) REFERENCES ${rel.to_table}(id) ON DELETE CASCADE`);
158
+ }
159
+ }
160
+
161
+ lines.push(fieldLines.join(",\n"));
162
+ lines.push(`);`);
163
+ lines.push(``);
164
+
165
+ // Indexes
166
+ for (const idx of model.indexes) {
167
+ const idxName = `idx_${tableName}_${idx.fields.join("_")}`;
168
+ const unique = idx.unique ? "UNIQUE " : "";
169
+ lines.push(`CREATE ${unique}INDEX IF NOT EXISTS ${idxName} ON ${tableName} (${idx.fields.join(", ")});`);
170
+ }
171
+
172
+ // FK indexes for belongs_to relations
173
+ for (const rel of mod.relations || []) {
174
+ if (rel.kind === "belongs_to") {
175
+ lines.push(`CREATE INDEX IF NOT EXISTS idx_${tableName}_${rel.foreign_key} ON ${tableName} (${rel.foreign_key});`);
176
+ }
177
+ }
178
+
179
+ // Junction tables for many_to_many
180
+ for (const rel of mod.relations || []) {
181
+ if (rel.kind === "many_to_many" && rel.junction_table) {
182
+ lines.push(``);
183
+ lines.push(`CREATE TABLE IF NOT EXISTS ${rel.junction_table} (`);
184
+ lines.push(` ${rel.from_table.slice(0, -1)}_id UUID NOT NULL REFERENCES ${rel.from_table}(id) ON DELETE CASCADE,`);
185
+ lines.push(` ${rel.to_table.slice(0, -1)}_id UUID NOT NULL REFERENCES ${rel.to_table}(id) ON DELETE CASCADE,`);
186
+ lines.push(` created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),`);
187
+ lines.push(` PRIMARY KEY (${rel.from_table.slice(0, -1)}_id, ${rel.to_table.slice(0, -1)}_id)`);
188
+ lines.push(`);`);
189
+ 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);`);
190
+ 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);`);
191
+ }
192
+ }
193
+
194
+ // Updated_at trigger
195
+ if (model.fields.some(f => f.name === "updated_at")) {
196
+ lines.push(``);
197
+ lines.push(`CREATE OR REPLACE FUNCTION update_${tableName}_updated_at()`);
198
+ lines.push(`RETURNS TRIGGER AS $$`);
199
+ lines.push(`BEGIN`);
200
+ lines.push(` NEW.updated_at = NOW();`);
201
+ lines.push(` RETURN NEW;`);
202
+ lines.push(`END;`);
203
+ lines.push(`$$ LANGUAGE plpgsql;`);
204
+ lines.push(``);
205
+ lines.push(`CREATE TRIGGER trg_${tableName}_updated_at`);
206
+ lines.push(` BEFORE UPDATE ON ${tableName}`);
207
+ lines.push(` FOR EACH ROW`);
208
+ lines.push(` EXECUTE FUNCTION update_${tableName}_updated_at();`);
209
+ }
210
+
211
+ lines.push(``);
212
+
213
+ return {
214
+ path: `schema/${toSnakeCase(model.name)}.sql`,
215
+ content: lines.join("\n"),
216
+ language: "sql",
217
+ source_module: mod.id,
218
+ };
219
+ }
220
+
221
+ // ─── Shared Types ──────────────────────────────────────────────────────────
222
+
223
+ private emitSharedTypes(system: IR.IRSystem): EmittedFile {
224
+ const lines: string[] = [];
225
+ lines.push(`// Generated by BoneScript compiler. DO NOT EDIT.`);
226
+ lines.push(`// Source: ${system.source_hash}`);
227
+ lines.push(``);
228
+
229
+ for (const mod of system.modules) {
230
+ for (const model of mod.models) {
231
+ lines.push(`export interface ${model.name} {`);
232
+ for (const field of model.fields) {
233
+ const nullable = field.nullable ? " | null" : "";
234
+ lines.push(` ${field.name}: ${toTsType(field.type)}${nullable};`);
235
+ }
236
+ lines.push(`}`);
237
+ lines.push(``);
238
+ }
239
+ }
240
+
241
+ // Common types
242
+ lines.push(`export interface ServiceError {`);
243
+ lines.push(` code: string;`);
244
+ lines.push(` message: string;`);
245
+ lines.push(` details?: unknown;`);
246
+ lines.push(`}`);
247
+ lines.push(``);
248
+ lines.push(`export type Result<T, E = ServiceError> =`);
249
+ lines.push(` | { ok: true; value: T }`);
250
+ lines.push(` | { ok: false; error: E };`);
251
+ lines.push(``);
252
+ lines.push(`export interface RequestContext {`);
253
+ lines.push(` authenticated: boolean;`);
254
+ lines.push(` actor_id: string | null;`);
255
+ lines.push(` trace_id: string;`);
256
+ lines.push(` correlation_id: string;`);
257
+ lines.push(`}`);
258
+ lines.push(``);
259
+ lines.push(`export interface PaginatedResult<T> {`);
260
+ lines.push(` items: T[];`);
261
+ lines.push(` total: number;`);
262
+ lines.push(` page: number;`);
263
+ lines.push(` page_size: number;`);
264
+ lines.push(`}`);
265
+ lines.push(``);
266
+
267
+ return {
268
+ path: `types/models.ts`,
269
+ content: lines.join("\n"),
270
+ language: "typescript",
271
+ source_module: "shared",
272
+ };
273
+ }
274
+
275
+ // ─── Event Types ───────────────────────────────────────────────────────────
276
+
277
+ private emitEventTypes(system: IR.IRSystem): EmittedFile {
278
+ const lines: string[] = [];
279
+ lines.push(`// Generated by BoneScript compiler. DO NOT EDIT.`);
280
+ lines.push(`// Source: ${system.source_hash}`);
281
+ lines.push(``);
282
+
283
+ for (const ev of system.events) {
284
+ lines.push(`export interface ${ev.name}Event {`);
285
+ lines.push(` type: "${ev.name}";`);
286
+ lines.push(` payload: {`);
287
+ for (const field of ev.payload) {
288
+ const nullable = field.nullable ? " | null" : "";
289
+ lines.push(` ${field.name}: ${toTsType(field.type)}${nullable};`);
290
+ }
291
+ lines.push(` };`);
292
+ lines.push(` metadata: {`);
293
+ lines.push(` source: string;`);
294
+ lines.push(` timestamp: Date;`);
295
+ lines.push(` correlation_id: string;`);
296
+ lines.push(` causation_id: string;`);
297
+ lines.push(` };`);
298
+ lines.push(`}`);
299
+ lines.push(``);
300
+ }
301
+
302
+ // Union type of all events
303
+ const eventNames = system.events.map(e => `${e.name}Event`);
304
+ lines.push(`export type SystemEvent = ${eventNames.join(" | ")};`);
305
+ lines.push(``);
306
+
307
+ // Event bus interface
308
+ lines.push(`export interface EventBus {`);
309
+ lines.push(` publish(event: SystemEvent): Promise<void>;`);
310
+ lines.push(` subscribe(type: string, handler: (event: SystemEvent) => Promise<void>): void;`);
311
+ lines.push(`}`);
312
+ lines.push(``);
313
+
314
+ return {
315
+ path: `types/events.ts`,
316
+ content: lines.join("\n"),
317
+ language: "typescript",
318
+ source_module: "shared",
319
+ };
320
+ }
321
+
322
+ // ─── Service Implementation ────────────────────────────────────────────────
323
+
324
+ private emitService(mod: IR.IRModule, system: IR.IRSystem): EmittedFile {
325
+ const lines: string[] = [];
326
+ lines.push(`// Generated by BoneScript compiler. DO NOT EDIT.`);
327
+ lines.push(`// Source: ${system.source_hash}`);
328
+ lines.push(`// Module: ${mod.name} (${mod.kind})`);
329
+ lines.push(``);
330
+ lines.push(`import { Result, RequestContext, ServiceError, PaginatedResult } from "../types/models";`);
331
+ lines.push(``);
332
+
333
+ for (const iface of mod.interfaces) {
334
+ // Interface definition
335
+ lines.push(`export interface ${iface.name} {`);
336
+ for (const method of iface.methods) {
337
+ const params = method.input.map(f => `${f.name}: ${toTsType(f.type)}`).join(", ");
338
+ const ctxParam = method.authenticated ? "ctx: RequestContext" : "";
339
+ const allParams = [ctxParam, params].filter(Boolean).join(", ");
340
+ lines.push(` ${method.name}(${allParams}): Promise<Result<${toTsType(method.output)}>>;`);
341
+ }
342
+ lines.push(`}`);
343
+ lines.push(``);
344
+
345
+ // Implementation class
346
+ lines.push(`export class ${mod.name} implements ${iface.name} {`);
347
+ for (const method of iface.methods) {
348
+ lines.push(this.emitMethod(method));
349
+ }
350
+ lines.push(`}`);
351
+ lines.push(``);
352
+ }
353
+
354
+ return {
355
+ path: `services/${toSnakeCase(mod.name)}.ts`,
356
+ content: lines.join("\n"),
357
+ language: "typescript",
358
+ source_module: mod.id,
359
+ };
360
+ }
361
+
362
+ private emitMethod(method: IR.IRMethod): string {
363
+ const lines: string[] = [];
364
+ const params = method.input.map(f => `${f.name}: ${toTsType(f.type)}`).join(", ");
365
+ const ctxParam = method.authenticated ? "ctx: RequestContext" : "";
366
+ const allParams = [ctxParam, params].filter(Boolean).join(", ");
367
+
368
+ lines.push(` async ${method.name}(${allParams}): Promise<Result<${toTsType(method.output)}>> {`);
369
+
370
+ // Auth check
371
+ if (method.authenticated) {
372
+ lines.push(` // [Guard] Authentication required`);
373
+ lines.push(` if (!ctx.authenticated) {`);
374
+ lines.push(` return { ok: false, error: { code: "UNAUTHORIZED", message: "Authentication required" } };`);
375
+ lines.push(` }`);
376
+ lines.push(``);
377
+ }
378
+
379
+ // Preconditions
380
+ if (method.preconditions.length > 0) {
381
+ lines.push(` // [Preconditions]`);
382
+ for (const pre of method.preconditions) {
383
+ lines.push(` // CHECK: ${pre.description}`);
384
+ }
385
+ lines.push(``);
386
+ }
387
+
388
+ // Effects
389
+ if (method.effects.length > 0) {
390
+ lines.push(` // [Effects] Applied in declaration order (deterministic)`);
391
+ for (const eff of method.effects) {
392
+ const opSymbol = eff.op === "assign" ? "=" : eff.op === "add" ? "+=" : "-=";
393
+ lines.push(` // EFFECT: ${eff.target} ${opSymbol} ${eff.value}`);
394
+ }
395
+ lines.push(``);
396
+ }
397
+
398
+ // Emissions
399
+ if (method.emissions.length > 0) {
400
+ lines.push(` // [Events]`);
401
+ for (const ev of method.emissions) {
402
+ lines.push(` // EMIT: ${ev}`);
403
+ }
404
+ lines.push(``);
405
+ }
406
+
407
+ lines.push(` // TODO: Implementation`);
408
+ lines.push(` throw new Error("Not implemented: ${method.name}");`);
409
+ lines.push(` }`);
410
+ lines.push(``);
411
+
412
+ return lines.join("\n");
413
+ }
414
+
415
+ // ─── State Machine ─────────────────────────────────────────────────────────
416
+
417
+ private emitStateMachine(sm: IR.IRStateMachine, mod: IR.IRModule, system: IR.IRSystem): EmittedFile {
418
+ const lines: string[] = [];
419
+ lines.push(`// Generated by BoneScript compiler. DO NOT EDIT.`);
420
+ lines.push(`// Source: ${system.source_hash}`);
421
+ lines.push(`// Entity: ${sm.entity}`);
422
+ lines.push(``);
423
+
424
+ // State type
425
+ const stateUnion = sm.states.map(s => `"${s}"`).join(" | ");
426
+ lines.push(`export type ${sm.entity}State = ${stateUnion};`);
427
+ lines.push(``);
428
+
429
+ // Transition table
430
+ lines.push(`export const ${sm.entity.toUpperCase()}_TRANSITIONS: Record<${sm.entity}State, Record<string, ${sm.entity}State>> = {`);
431
+ for (const state of sm.states) {
432
+ const transitions = sm.transitions.filter(t => t.from === state);
433
+ const entries = transitions.map(t => `"${t.trigger}": "${t.to}"`).join(", ");
434
+ lines.push(` "${state}": { ${entries} },`);
435
+ }
436
+ lines.push(`};`);
437
+ lines.push(``);
438
+
439
+ // Transition function
440
+ lines.push(`export interface TransitionError {`);
441
+ lines.push(` current: ${sm.entity}State;`);
442
+ lines.push(` trigger: string;`);
443
+ lines.push(` message: string;`);
444
+ lines.push(`}`);
445
+ lines.push(``);
446
+ lines.push(`export function transition${sm.entity}(`);
447
+ lines.push(` current: ${sm.entity}State,`);
448
+ lines.push(` trigger: string`);
449
+ lines.push(`): { ok: true; state: ${sm.entity}State } | { ok: false; error: TransitionError } {`);
450
+ lines.push(` const next = ${sm.entity.toUpperCase()}_TRANSITIONS[current]?.[trigger];`);
451
+ lines.push(` if (!next) {`);
452
+ lines.push(` return {`);
453
+ lines.push(` ok: false,`);
454
+ lines.push(` error: {`);
455
+ lines.push(` current,`);
456
+ lines.push(` trigger,`);
457
+ lines.push(` message: \`Invalid transition: \${current} --[\${trigger}]--> ?\``);
458
+ lines.push(` }`);
459
+ lines.push(` };`);
460
+ lines.push(` }`);
461
+ lines.push(` return { ok: true, state: next };`);
462
+ lines.push(`}`);
463
+ lines.push(``);
464
+
465
+ // Initial state
466
+ lines.push(`export const ${sm.entity.toUpperCase()}_INITIAL: ${sm.entity}State = "${sm.initial}";`);
467
+ lines.push(``);
468
+
469
+ return {
470
+ path: `services/state_machines/${toSnakeCase(sm.entity)}_states.ts`,
471
+ content: lines.join("\n"),
472
+ language: "typescript",
473
+ source_module: mod.id,
474
+ };
475
+ }
476
+
477
+ // ─── Service Config (YAML) ─────────────────────────────────────────────────
478
+
479
+ private emitServiceConfig(system: IR.IRSystem): EmittedFile {
480
+ const lines: string[] = [];
481
+ lines.push(`# Generated by BoneScript compiler. DO NOT EDIT.`);
482
+ lines.push(`# Source: ${system.source_hash}`);
483
+ lines.push(``);
484
+ lines.push(`system:`);
485
+ lines.push(` name: ${system.name}`);
486
+ lines.push(` version: ${system.version}`);
487
+ lines.push(` domain: ${system.domain || "generic"}`);
488
+ lines.push(``);
489
+ lines.push(`services:`);
490
+
491
+ for (const mod of system.modules) {
492
+ if (mod.kind === "api_service" || mod.kind === "realtime_service") {
493
+ lines.push(` - name: ${toSnakeCase(mod.name)}`);
494
+ lines.push(` kind: ${mod.kind}`);
495
+ lines.push(` dependencies:`);
496
+ for (const dep of mod.dependencies) {
497
+ const depMod = system.modules.find(m => m.id === dep);
498
+ if (depMod) lines.push(` - ${toSnakeCase(depMod.name)}`);
499
+ }
500
+ for (const [key, val] of Object.entries(mod.config)) {
501
+ lines.push(` ${key}: ${val}`);
502
+ }
503
+ lines.push(``);
504
+ }
505
+ }
506
+
507
+ return {
508
+ path: `config/services.yaml`,
509
+ content: lines.join("\n"),
510
+ language: "yaml",
511
+ source_module: "config",
512
+ };
513
+ }
514
+
515
+ // ─── Infrastructure Config (YAML) ──────────────────────────────────────────
516
+
517
+ private emitInfraConfig(system: IR.IRSystem): EmittedFile {
518
+ const lines: string[] = [];
519
+ lines.push(`# Generated by BoneScript compiler. DO NOT EDIT.`);
520
+ lines.push(`# Source: ${system.source_hash}`);
521
+ lines.push(``);
522
+ lines.push(`infrastructure:`);
523
+
524
+ // Data stores
525
+ lines.push(` datastores:`);
526
+ for (const mod of system.modules) {
527
+ if (mod.kind === "data_store") {
528
+ lines.push(` - name: ${toSnakeCase(mod.name)}`);
529
+ lines.push(` engine: ${mod.config["engine"] || "postgresql"}`);
530
+ lines.push(` replicas: ${mod.config["replicas"] || 1}`);
531
+ if (mod.config["retention_ms"]) lines.push(` retention_ms: ${mod.config["retention_ms"]}`);
532
+ if (mod.config["partition_key"]) lines.push(` partition_key: ${mod.config["partition_key"]}`);
533
+ lines.push(``);
534
+ }
535
+ }
536
+
537
+ // Gateway
538
+ lines.push(` gateway:`);
539
+ const gw = system.modules.find(m => m.kind === "gateway");
540
+ if (gw) {
541
+ for (const [key, val] of Object.entries(gw.config)) {
542
+ lines.push(` ${key}: ${val}`);
543
+ }
544
+ }
545
+ lines.push(``);
546
+
547
+ // Events
548
+ if (system.events.length > 0) {
549
+ lines.push(` events:`);
550
+ for (const ev of system.events) {
551
+ lines.push(` - name: ${ev.name}`);
552
+ lines.push(` delivery: ${ev.delivery}`);
553
+ lines.push(` ordering: ${ev.ordering}`);
554
+ if (ev.ttl_ms) lines.push(` ttl_ms: ${ev.ttl_ms}`);
555
+ lines.push(``);
556
+ }
557
+ }
558
+
559
+ return {
560
+ path: `config/infrastructure.yaml`,
561
+ content: lines.join("\n"),
562
+ language: "yaml",
563
+ source_module: "config",
564
+ };
565
+ }
566
+ }