bonescript-compiler 0.5.3 → 0.5.4

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