bonescript-compiler 0.5.2 → 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 (187) 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 +128 -292
  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 +162 -162
  12. package/dist/emit_events.d.ts +0 -1
  13. package/dist/emit_events.js +275 -342
  14. package/dist/emit_events.js.map +1 -1
  15. package/dist/emit_full.js +106 -268
  16. package/dist/emit_full.js.map +1 -1
  17. package/dist/emit_maintenance.js +249 -249
  18. package/dist/emit_runtime.d.ts +11 -17
  19. package/dist/emit_runtime.js +688 -29
  20. package/dist/emit_runtime.js.map +1 -1
  21. package/dist/emit_sourcemap.js +66 -66
  22. package/dist/emit_tests.js +0 -37
  23. package/dist/emit_tests.js.map +1 -1
  24. package/dist/emitter.js +16 -82
  25. package/dist/emitter.js.map +1 -1
  26. package/dist/extension_manager.d.ts +2 -2
  27. package/dist/extension_manager.js +3 -6
  28. package/dist/extension_manager.js.map +1 -1
  29. package/dist/ir.d.ts +0 -4
  30. package/dist/lowering.d.ts +14 -5
  31. package/dist/lowering.js +417 -66
  32. package/dist/lowering.js.map +1 -1
  33. package/dist/module_loader.d.ts +2 -2
  34. package/dist/module_loader.js +23 -20
  35. package/dist/module_loader.js.map +1 -1
  36. package/dist/optimizer.js +1 -1
  37. package/dist/optimizer.js.map +1 -1
  38. package/dist/scaffold.d.ts +2 -2
  39. package/dist/scaffold.js +319 -315
  40. package/dist/scaffold.js.map +1 -1
  41. package/dist/source_map.js.map +1 -0
  42. package/dist/test.js.map +1 -0
  43. package/dist/test_typechecker.d.ts +5 -0
  44. package/dist/test_typechecker.js +126 -0
  45. package/dist/test_typechecker.js.map +1 -0
  46. package/dist/typechecker.d.ts +0 -5
  47. package/dist/typechecker.js +13 -68
  48. package/dist/typechecker.js.map +1 -1
  49. package/dist/verifier.d.ts +1 -5
  50. package/dist/verifier.js +35 -140
  51. package/dist/verifier.js.map +1 -1
  52. package/package.json +52 -62
  53. package/src/algorithm_catalog.ts +345 -345
  54. package/src/ast.d.ts +244 -0
  55. package/src/ast.ts +334 -334
  56. package/src/cli.ts +624 -98
  57. package/src/emit_batch.ts +140 -140
  58. package/src/emit_capability.ts +436 -617
  59. package/src/emit_composition.ts +196 -229
  60. package/src/emit_deploy.ts +190 -190
  61. package/src/emit_events.ts +307 -377
  62. package/src/emit_extras.ts +240 -240
  63. package/src/emit_full.ts +309 -475
  64. package/src/emit_maintenance.ts +459 -459
  65. package/src/emit_runtime.ts +730 -17
  66. package/src/emit_sourcemap.ts +140 -140
  67. package/src/emit_tests.ts +205 -246
  68. package/src/emit_websocket.ts +229 -229
  69. package/src/emitter.ts +578 -642
  70. package/src/extension_manager.ts +187 -189
  71. package/src/formatter.ts +297 -297
  72. package/src/index.ts +88 -88
  73. package/src/ir.ts +215 -216
  74. package/src/lexer.d.ts +195 -0
  75. package/src/lexer.ts +630 -630
  76. package/src/lowering.ts +556 -168
  77. package/src/module_loader.ts +114 -112
  78. package/src/optimizer.ts +196 -196
  79. package/src/parse_decls.d.ts +13 -0
  80. package/src/parse_decls.ts +409 -409
  81. package/src/parse_decls2.d.ts +13 -0
  82. package/src/parse_decls2.ts +244 -244
  83. package/src/parse_expr.d.ts +7 -0
  84. package/src/parse_expr.ts +197 -197
  85. package/src/parse_types.d.ts +6 -0
  86. package/src/parse_types.ts +54 -54
  87. package/src/parser.d.ts +10 -0
  88. package/src/parser.ts +1 -1
  89. package/src/parser_base.d.ts +19 -0
  90. package/src/parser_base.ts +57 -57
  91. package/src/parser_recovery.ts +153 -153
  92. package/src/scaffold.ts +375 -371
  93. package/src/solver.ts +330 -330
  94. package/src/typechecker.d.ts +52 -0
  95. package/src/typechecker.ts +591 -657
  96. package/src/types.d.ts +38 -0
  97. package/src/types.ts +122 -122
  98. package/src/verifier.ts +46 -152
  99. package/README.md +0 -382
  100. package/dist/commands/check.d.ts +0 -5
  101. package/dist/commands/check.js +0 -34
  102. package/dist/commands/check.js.map +0 -1
  103. package/dist/commands/compile.d.ts +0 -5
  104. package/dist/commands/compile.js +0 -215
  105. package/dist/commands/compile.js.map +0 -1
  106. package/dist/commands/debug.d.ts +0 -5
  107. package/dist/commands/debug.js +0 -59
  108. package/dist/commands/debug.js.map +0 -1
  109. package/dist/commands/diff.d.ts +0 -5
  110. package/dist/commands/diff.js +0 -125
  111. package/dist/commands/diff.js.map +0 -1
  112. package/dist/commands/fmt.d.ts +0 -5
  113. package/dist/commands/fmt.js +0 -49
  114. package/dist/commands/fmt.js.map +0 -1
  115. package/dist/commands/init.d.ts +0 -5
  116. package/dist/commands/init.js +0 -96
  117. package/dist/commands/init.js.map +0 -1
  118. package/dist/commands/ir.d.ts +0 -5
  119. package/dist/commands/ir.js +0 -27
  120. package/dist/commands/ir.js.map +0 -1
  121. package/dist/commands/lex.d.ts +0 -5
  122. package/dist/commands/lex.js +0 -21
  123. package/dist/commands/lex.js.map +0 -1
  124. package/dist/commands/parse.d.ts +0 -5
  125. package/dist/commands/parse.js +0 -30
  126. package/dist/commands/parse.js.map +0 -1
  127. package/dist/commands/test.d.ts +0 -5
  128. package/dist/commands/test.js +0 -61
  129. package/dist/commands/test.js.map +0 -1
  130. package/dist/commands/verify_determinism.d.ts +0 -5
  131. package/dist/commands/verify_determinism.js +0 -64
  132. package/dist/commands/verify_determinism.js.map +0 -1
  133. package/dist/commands/watch.d.ts +0 -5
  134. package/dist/commands/watch.js +0 -50
  135. package/dist/commands/watch.js.map +0 -1
  136. package/dist/emit_auth.d.ts +0 -18
  137. package/dist/emit_auth.js +0 -507
  138. package/dist/emit_auth.js.map +0 -1
  139. package/dist/emit_database.d.ts +0 -7
  140. package/dist/emit_database.js +0 -74
  141. package/dist/emit_database.js.map +0 -1
  142. package/dist/emit_index.d.ts +0 -6
  143. package/dist/emit_index.js +0 -202
  144. package/dist/emit_index.js.map +0 -1
  145. package/dist/emit_models.d.ts +0 -12
  146. package/dist/emit_models.js +0 -171
  147. package/dist/emit_models.js.map +0 -1
  148. package/dist/emit_openapi.d.ts +0 -9
  149. package/dist/emit_openapi.js +0 -308
  150. package/dist/emit_openapi.js.map +0 -1
  151. package/dist/emit_package.d.ts +0 -7
  152. package/dist/emit_package.js +0 -70
  153. package/dist/emit_package.js.map +0 -1
  154. package/dist/emit_router.d.ts +0 -12
  155. package/dist/emit_router.js +0 -390
  156. package/dist/emit_router.js.map +0 -1
  157. package/dist/lowering_channels.d.ts +0 -11
  158. package/dist/lowering_channels.js +0 -103
  159. package/dist/lowering_channels.js.map +0 -1
  160. package/dist/lowering_entities.d.ts +0 -11
  161. package/dist/lowering_entities.js +0 -232
  162. package/dist/lowering_entities.js.map +0 -1
  163. package/dist/lowering_helpers.d.ts +0 -13
  164. package/dist/lowering_helpers.js +0 -76
  165. package/dist/lowering_helpers.js.map +0 -1
  166. package/src/commands/check.ts +0 -33
  167. package/src/commands/compile.ts +0 -191
  168. package/src/commands/debug.ts +0 -33
  169. package/src/commands/diff.ts +0 -108
  170. package/src/commands/fmt.ts +0 -22
  171. package/src/commands/init.ts +0 -72
  172. package/src/commands/ir.ts +0 -23
  173. package/src/commands/lex.ts +0 -17
  174. package/src/commands/parse.ts +0 -24
  175. package/src/commands/test.ts +0 -36
  176. package/src/commands/verify_determinism.ts +0 -66
  177. package/src/commands/watch.ts +0 -25
  178. package/src/emit_auth.ts +0 -513
  179. package/src/emit_database.ts +0 -75
  180. package/src/emit_index.ts +0 -210
  181. package/src/emit_models.ts +0 -176
  182. package/src/emit_openapi.ts +0 -318
  183. package/src/emit_package.ts +0 -69
  184. package/src/emit_router.ts +0 -409
  185. package/src/lowering_channels.ts +0 -108
  186. package/src/lowering_entities.ts +0 -258
  187. package/src/lowering_helpers.ts +0 -75
package/src/emit_full.ts CHANGED
@@ -1,475 +1,309 @@
1
- /**
2
- * BoneScript Full Emitter — Produces a complete, runnable project.
3
- * Combines schema generation with runtime service code.
4
- */
5
-
6
- import * as IR from "./ir";
7
- import { Emitter, EmittedFile } from "./emitter";
8
- import {
9
- emitPackageJson,
10
- emitTsConfig,
11
- emitDbClient,
12
- emitAuthMiddleware,
13
- emitEntityRouter,
14
- emitStateMachineRuntime,
15
- emitIndex,
16
- emitMigration,
17
- } from "./emit_runtime";
18
- import { emitWebSocketServer } from "./emit_websocket";
19
- import {
20
- emitLogger,
21
- emitMetrics,
22
- emitHealthChecks,
23
- emitFailureRules,
24
- emitMigrationDiff,
25
- } from "./emit_maintenance";
26
- import { emitFlowRuntime, emitChannelFilters } from "./emit_extras";
27
- import { emitAlgorithmsFile, collectUsedAlgorithms } from "./emit_composition";
28
- import { emitExtensionPointStub } from "./extension_manager";
29
- import * as AST from "./ast";
30
- import { emitDurableEventBus, emitOutboxSchema, emitTypedEventPublishers } from "./emit_events";
31
- import { emitBatchExecutor } from "./emit_batch";
32
- import { emitSourceMapFile, emitDebugHandler } from "./emit_sourcemap";
33
- import { emitTestSuite } from "./emit_tests";
34
- import { emitDockerfile, emitDockerignore, emitK8sDeployment, emitGithubActions } from "./emit_deploy";
35
- import { emitModelFile, emitModelsIndex } from "./emit_models";
36
- import { emitOpenApiSchema } from "./emit_openapi";
37
-
38
- function toSnakeCase(s: string): string {
39
- return s.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase();
40
- }
41
-
42
- /** Resolve the auth method for the system from the resolution map or module configs. */
43
- function resolveSystemAuthMethod(system: IR.IRSystem): "jwt" | "oauth2" | "apikey" {
44
- const direct = system.resolution["implied.auth_method"] || system.resolution["system.auth_method"];
45
- if (direct === "oauth2" || direct === "apikey" || direct === "jwt") return direct as "jwt" | "oauth2" | "apikey";
46
- for (const [key, val] of Object.entries(system.resolution)) {
47
- if (key.endsWith(".auth_method") && (val === "oauth2" || val === "apikey" || val === "jwt")) {
48
- return val as "jwt" | "oauth2" | "apikey";
49
- }
50
- }
51
- for (const mod of system.modules) {
52
- const m = mod.config["auth_method"] as string | undefined;
53
- if (m === "oauth2" || m === "apikey" || m === "jwt") return m;
54
- }
55
- return "jwt";
56
- }
57
-
58
- export class FullEmitter {
59
- private schemaEmitter = new Emitter();
60
-
61
- emit(system: IR.IRSystem): EmittedFile[] {
62
- const files: EmittedFile[] = [];
63
-
64
- // 1. Package files
65
- files.push({ path: "package.json", content: emitPackageJson(system), language: "json", source_module: "root" });
66
- files.push({ path: "tsconfig.json", content: emitTsConfig(), language: "json", source_module: "root" });
67
- files.push({ path: ".env.example", content: this.emitEnvExample(system), language: "yaml", source_module: "root" });
68
-
69
- // 2. Source: infrastructure
70
- files.push({ path: "src/db.ts", content: emitDbClient(system), language: "typescript", source_module: "infra" });
71
- // Durable event bus replaces the old in-process stub
72
- files.push({ path: "src/events.ts", content: emitDurableEventBus(system), language: "typescript", source_module: "infra" });
73
- // Outbox SQL schema
74
- files.push({ path: "migrations/event_outbox.sql", content: emitOutboxSchema(), language: "sql", source_module: "infra" });
75
-
76
- // API key table migration (only when auth_method = apikey)
77
- const authMethod = resolveSystemAuthMethod(system);
78
- if (authMethod === "apikey") {
79
- files.push({
80
- path: "migrations/api_keys.sql",
81
- content: [
82
- "-- Generated by BoneScript compiler. DO NOT EDIT.",
83
- "-- API key table for apikey auth strategy.",
84
- "",
85
- "CREATE TABLE IF NOT EXISTS api_keys (",
86
- " id UUID PRIMARY KEY DEFAULT gen_random_uuid(),",
87
- " actor_id UUID NOT NULL,",
88
- " key_hash VARCHAR(64) NOT NULL UNIQUE,",
89
- " key_prefix VARCHAR(16) NOT NULL,",
90
- " name VARCHAR(255) NOT NULL DEFAULT 'default',",
91
- " created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),",
92
- " expires_at TIMESTAMPTZ NOT NULL,",
93
- " revoked BOOLEAN NOT NULL DEFAULT false",
94
- ");",
95
- "CREATE INDEX IF NOT EXISTS idx_api_keys_actor ON api_keys (actor_id);",
96
- "CREATE INDEX IF NOT EXISTS idx_api_keys_hash ON api_keys (key_hash);",
97
- ].join("\n"),
98
- language: "sql",
99
- source_module: "infra",
100
- });
101
- }
102
- // Typed event publisher functions (one per declared event)
103
- if (system.events.length > 0) {
104
- files.push({ path: "src/publishers.ts", content: emitTypedEventPublishers(system), language: "typescript", source_module: "infra" });
105
- }
106
- files.push({ path: "src/auth.ts", content: emitAuthMiddleware(system), language: "typescript", source_module: "infra" });
107
- files.push({ path: "src/logger.ts", content: emitLogger(system), language: "typescript", source_module: "infra" });
108
- files.push({ path: "src/metrics.ts", content: emitMetrics(), language: "typescript", source_module: "infra" });
109
- files.push({ path: "src/health.ts", content: emitHealthChecks(system), language: "typescript", source_module: "infra" });
110
- files.push({ path: "src/failure_rules.ts", content: emitFailureRules(system), language: "typescript", source_module: "infra" });
111
-
112
- // 2a. WebSocket server (only if there are realtime channels)
113
- const wsContent = emitWebSocketServer(system);
114
- if (wsContent) {
115
- files.push({ path: "src/websocket.ts", content: wsContent, language: "typescript", source_module: "infra" });
116
- }
117
-
118
- // 2b. Flow saga runtime (only if there are flows)
119
- const flowContent = emitFlowRuntime(system);
120
- if (flowContent) {
121
- files.push({ path: "src/flows.ts", content: flowContent, language: "typescript", source_module: "infra" });
122
- }
123
-
124
- // 2b2. Batch executor (only if there are batch capabilities)
125
- const batchContent = emitBatchExecutor(system);
126
- if (batchContent) {
127
- files.push({ path: "src/batch.ts", content: batchContent, language: "typescript", source_module: "infra" });
128
- }
129
-
130
- // 2c. Migration diff utility (always emitted)
131
- files.push({ path: "src/migration_diff.ts", content: emitMigrationDiff(), language: "typescript", source_module: "infra" });
132
-
133
- // 2d. Algorithm implementations (only what's used)
134
- const usedAlgorithms = collectUsedAlgorithms(system);
135
- if (usedAlgorithms.size > 0) {
136
- const algoContent = emitAlgorithmsFile(usedAlgorithms);
137
- files.push({ path: "src/algorithms.ts", content: algoContent, language: "typescript", source_module: "algorithms" });
138
- } else {
139
- files.push({
140
- path: "src/algorithms.ts",
141
- content: "// No algorithms used in this system.\nexport {};\n",
142
- language: "typescript",
143
- source_module: "algorithms",
144
- });
145
- }
146
-
147
- // 2e. Extension points (escape hatches — preserved across recompilation)
148
- if (system.extension_points && system.extension_points.length > 0) {
149
- const extLines: string[] = [
150
- "// Generated by BoneScript compiler.",
151
- "// Extension points: implement the functions below.",
152
- "// Code between sentinel comments is preserved on recompile.",
153
- "// DO NOT remove the sentinel comments.",
154
- "",
155
- ];
156
- for (const ep of system.extension_points) {
157
- // Use the shared emitExtensionPointStub so the format is consistent
158
- // with what extension_manager.ts expects when merging on recompile.
159
- const stub = emitExtensionPointStub({
160
- kind: "ExtensionPointDecl",
161
- loc: { line: 0, column: 0, offset: 0 },
162
- name: ep.name,
163
- params: ep.params.map(p => ({
164
- kind: "Param" as const,
165
- loc: { line: 0, column: 0, offset: 0 },
166
- name: p.name,
167
- type: { kind: "PrimitiveType" as const, loc: { line: 0, column: 0, offset: 0 }, name: p.type },
168
- })),
169
- returns: ep.returns
170
- ? { kind: "PrimitiveType" as const, loc: { line: 0, column: 0, offset: 0 }, name: ep.returns }
171
- : null,
172
- stable: ep.stable,
173
- });
174
- extLines.push(stub);
175
- extLines.push("");
176
- }
177
- files.push({
178
- path: "src/extensions.ts",
179
- content: extLines.join("\n"),
180
- language: "typescript",
181
- source_module: "extensions",
182
- });
183
- }
184
-
185
- // 3. Source: state machines
186
- for (const mod of system.modules) {
187
- for (const sm of mod.state_machines) {
188
- files.push({
189
- path: `src/state_machines/${toSnakeCase(sm.entity)}.ts`,
190
- content: emitStateMachineRuntime(sm),
191
- language: "typescript",
192
- source_module: mod.id,
193
- });
194
- }
195
- }
196
-
197
- // 3a. Derived field helpers (one file per entity that has derived fields)
198
- for (const mod of system.modules) {
199
- for (const model of mod.models) {
200
- const derivedFields = model.fields.filter(f =>
201
- f.default_value && f.default_value.startsWith("GENERATED ALWAYS AS")
202
- );
203
- if (derivedFields.length === 0) continue;
204
- const lines: string[] = [
205
- `// Generated by BoneScript compiler. DO NOT EDIT.`,
206
- `// Derived field helpers for ${model.name}.`,
207
- `// These mirror the GENERATED ALWAYS AS columns in the SQL migration.`,
208
- `// Use them when you need the computed value in application code before a DB round-trip.`,
209
- ``,
210
- `export type ${model.name}Derived = {`,
211
- ];
212
- for (const f of derivedFields) {
213
- lines.push(` ${f.name}: unknown;`);
214
- }
215
- lines.push(`};`);
216
- lines.push(``);
217
- lines.push(`export const ${model.name.toUpperCase()}_DERIVED_FIELDS = [${derivedFields.map(f => `"${f.name}"`).join(", ")}] as const;`);
218
- lines.push(``);
219
- lines.push(`/** Returns true if the field name is a derived (computed) field on ${model.name}. */`);
220
- lines.push(`export function is${model.name}DerivedField(field: string): boolean {`);
221
- lines.push(` return (${model.name.toUpperCase()}_DERIVED_FIELDS as readonly string[]).includes(field);`);
222
- lines.push(`}`);
223
- files.push({
224
- path: `src/derived/${toSnakeCase(model.name)}.ts`,
225
- content: lines.join("\n"),
226
- language: "typescript",
227
- source_module: mod.id,
228
- });
229
- }
230
- }
231
-
232
- // 3b. Channel filter predicates (only if any channel has a filter expression)
233
- const realtimeMods = system.modules.filter(m => m.kind === "realtime_service" && m.config["filter"]);
234
- if (realtimeMods.length > 0) {
235
- const filterLines: string[] = [
236
- `// Generated by BoneScript compiler. DO NOT EDIT.`,
237
- `// Channel filter predicates — applied before delivering messages to participants.`,
238
- ``,
239
- `export const CHANNEL_FILTERS: Record<string, (event: any, participant: any) => boolean> = {`,
240
- ];
241
- for (const mod of realtimeMods) {
242
- const filterExpr = String(mod.config["filter"] || "true");
243
- // Translate bone field refs to JS: event.field and participant.field pass through,
244
- // bare identifiers are assumed to be event properties.
245
- const jsFilter = filterExpr
246
- .replace(/\band\b/g, "&&")
247
- .replace(/\bor\b/g, "||")
248
- .replace(/\bnot\b/g, "!")
249
- .replace(/\b==\b/g, "===")
250
- .replace(/\b!=\b/g, "!==")
251
- .replace(/\bcontains\b/g, "?.includes");
252
- filterLines.push(` "${mod.name}": (event, participant) => {`);
253
- filterLines.push(` try { return Boolean(${jsFilter}); } catch { return true; }`);
254
- filterLines.push(` },`);
255
- }
256
- filterLines.push(`};`);
257
- filterLines.push(``);
258
- filterLines.push(`export function shouldDeliver(channel: string, event: any, participant: any): boolean {`);
259
- filterLines.push(` const filter = CHANNEL_FILTERS[channel];`);
260
- filterLines.push(` return filter ? filter(event, participant) : true;`);
261
- filterLines.push(`}`);
262
- files.push({
263
- path: "src/channel_filters.ts",
264
- content: filterLines.join("\n"),
265
- language: "typescript",
266
- source_module: "infra",
267
- });
268
- }
269
-
270
- // 4. Source: route files (CRUD + capabilities)
271
- for (const mod of system.modules) {
272
- if (mod.kind === "api_service" && mod.models.length > 0) {
273
- const content = emitEntityRouter(mod, system);
274
- if (content) {
275
- files.push({
276
- path: `src/routes/${toSnakeCase(mod.models[0].name)}.ts`,
277
- content,
278
- language: "typescript",
279
- source_module: mod.id,
280
- });
281
- }
282
- }
283
- }
284
-
285
- // 5. Source: main entry point
286
- files.push({ path: "src/index.ts", content: emitIndex(system), language: "typescript", source_module: "root" });
287
-
288
- // 5a. Model interfaces, schemas, and validators (one file per model)
289
- const modelFiles: string[] = [];
290
- for (const mod of system.modules) {
291
- for (const model of mod.models) {
292
- const modelPath = `src/models/${toSnakeCase(model.name)}.ts`;
293
- files.push({
294
- path: modelPath,
295
- content: emitModelFile(model, mod, system),
296
- language: "typescript",
297
- source_module: mod.id,
298
- });
299
- modelFiles.push(modelPath);
300
- }
301
- }
302
- if (modelFiles.length > 0) {
303
- files.push({
304
- path: "src/models/index.ts",
305
- content: emitModelsIndex(system),
306
- language: "typescript",
307
- source_module: "shared",
308
- });
309
- }
310
-
311
- // 6. SQL migrations — run schema emitter ONCE, then match by model name
312
- // Includes both api_service entities AND data_store schemas.
313
- const schemas: string[] = [];
314
- const allSchemaFiles = this.schemaEmitter.emit(system);
315
- for (const mod of system.modules) {
316
- if (mod.kind === "data_store" || mod.kind === "api_service") {
317
- for (const model of mod.models) {
318
- const schemaFile = allSchemaFiles.find(f => f.path.includes(toSnakeCase(model.name)) && f.language === "sql");
319
- if (schemaFile) {
320
- files.push({ ...schemaFile, path: `migrations/${schemaFile.path.replace("schema/", "")}` });
321
- schemas.push(schemaFile.content);
322
- }
323
- }
324
- }
325
- }
326
-
327
- // 7. Migration runner
328
- files.push({ path: "src/migrate.ts", content: emitMigration(system, schemas), language: "typescript", source_module: "infra" });
329
-
330
- // 8. Docker compose for local dev
331
- files.push({ path: "docker-compose.yaml", content: this.emitDockerCompose(system), language: "yaml", source_module: "infra" });
332
-
333
- // 9. README
334
- files.push({ path: "README.md", content: this.emitReadme(system), language: "yaml", source_module: "root" });
335
-
336
- // 9a. OpenAPI schema (spec/09_CODEGEN.md §2 — ApiService secondary target)
337
- const openApiContent = emitOpenApiSchema(system);
338
- if (openApiContent) {
339
- files.push({ path: "openapi.json", content: openApiContent, language: "json", source_module: "root" });
340
- }
341
-
342
- // 10. Source map + debug handler
343
- files.push({ path: `${system.name}.bone.map`, content: emitSourceMapFile(system, `${system.name}.bone`), language: "json", source_module: "root" });
344
- files.push({ path: "src/debug.ts", content: emitDebugHandler(system), language: "typescript", source_module: "infra" });
345
- files.push({ path: "src/tests.ts", content: emitTestSuite(system), language: "typescript", source_module: "tests" });
346
-
347
- // 11. Deploy targets
348
- files.push({ path: "Dockerfile", content: emitDockerfile(system), language: "yaml", source_module: "deploy" });
349
- files.push({ path: ".dockerignore", content: emitDockerignore(), language: "yaml", source_module: "deploy" });
350
- files.push({ path: "k8s/deployment.yaml", content: emitK8sDeployment(system), language: "yaml", source_module: "deploy" });
351
- files.push({ path: ".github/workflows/ci.yaml", content: emitGithubActions(system), language: "yaml", source_module: "deploy" });
352
-
353
- return files;
354
- }
355
-
356
- private emitEnvExample(system: IR.IRSystem): string {
357
- return `# ${system.name} Environment Variables
358
- # Copy this file to .env and fill in real values. Never commit .env to source control.
359
-
360
- # --- Required in production ---
361
- # Generate with: node -e "console.log(require('crypto').randomBytes(48).toString('hex'))"
362
- JWT_SECRET=
363
-
364
- # --- Database ---
365
- DATABASE_URL=postgresql://postgres:postgres@localhost:5432/${toSnakeCase(system.name)}
366
-
367
- # --- Redis (optional, used by some domain templates) ---
368
- REDIS_URL=redis://localhost:6379
369
-
370
- # --- Server ---
371
- PORT=3000
372
- NODE_ENV=development
373
-
374
- # --- CORS ---
375
- # Comma-separated list of allowed origins. Leave empty to disallow all cross-origin requests.
376
- # Example: ALLOWED_ORIGINS=https://app.example.com,https://admin.example.com
377
- ALLOWED_ORIGINS=
378
-
379
- # --- Rate limiting ---
380
- # Global limit: requests per window per IP (default 300/min)
381
- RATE_LIMIT_MAX=300
382
- RATE_LIMIT_WINDOW_MS=60000
383
- # Auth endpoints (login/register) limit per IP (default 20/min — brute-force protection)
384
- AUTH_RATE_LIMIT_MAX=20
385
-
386
- # --- Event delivery mode ---
387
- # in_process: in-memory, fast, no durability guarantees (default for development)
388
- # durable: Postgres-backed transactional outbox (recommended for production)
389
- EVENT_MODE=in_process
390
- EVENT_WORKER_INTERVAL_MS=1000
391
-
392
- # --- Request timeout ---
393
- REQUEST_TIMEOUT_MS=30000
394
- `;
395
- }
396
-
397
- private emitDockerCompose(system: IR.IRSystem): string {
398
- return `# Generated by BoneScript compiler.
399
- version: "3.8"
400
-
401
- services:
402
- postgres:
403
- image: postgres:16-alpine
404
- environment:
405
- POSTGRES_DB: ${toSnakeCase(system.name)}
406
- POSTGRES_USER: postgres
407
- POSTGRES_PASSWORD: postgres
408
- ports:
409
- - "5432:5432"
410
- volumes:
411
- - pgdata:/var/lib/postgresql/data
412
-
413
- redis:
414
- image: redis:7-alpine
415
- ports:
416
- - "6379:6379"
417
-
418
- volumes:
419
- pgdata:
420
- `;
421
- }
422
-
423
- private emitReadme(system: IR.IRSystem): string {
424
- const apiModules = system.modules.filter(m => m.kind === "api_service");
425
- const routes = apiModules
426
- .filter(m => m.models.length > 0)
427
- .map(m => `- \`/${toSnakeCase(m.models[0].name)}s\` — ${m.name}`)
428
- .join("\n");
429
-
430
- return `# ${system.name}
431
-
432
- Generated by BoneScript compiler. Source hash: ${system.source_hash}
433
-
434
- ## Quick Start
435
-
436
- \`\`\`bash
437
- # Start dependencies
438
- docker compose up -d
439
-
440
- # Install
441
- npm install
442
-
443
- # Run migrations
444
- npm run migrate
445
-
446
- # Start server
447
- npm run dev
448
- \`\`\`
449
-
450
- ## API Routes
451
-
452
- ${routes}
453
-
454
- Each route supports:
455
- - \`GET /\` — List (paginated)
456
- - \`GET /:id\` — Read
457
- - \`POST /\` — Create
458
- - \`PUT /:id\` — Update
459
- - \`DELETE /:id\` — Delete
460
-
461
- Plus capability-specific endpoints.
462
-
463
- ## Auth
464
-
465
- Send a Bearer token in the Authorization header:
466
- \`\`\`
467
- Authorization: Bearer <jwt-token>
468
- \`\`\`
469
-
470
- ## Environment
471
-
472
- Copy \`.env.example\` to \`.env\` and configure.
473
- `;
474
- }
475
- }
1
+ /**
2
+ * BoneScript Full Emitter — Produces a complete, runnable project.
3
+ * Combines schema generation with runtime service code.
4
+ */
5
+
6
+ import * as IR from "./ir";
7
+ import { Emitter, EmittedFile } from "./emitter";
8
+ import {
9
+ emitPackageJson,
10
+ emitTsConfig,
11
+ emitDbClient,
12
+ emitAuthMiddleware,
13
+ emitEntityRouter,
14
+ emitStateMachineRuntime,
15
+ emitIndex,
16
+ emitMigration,
17
+ } from "./emit_runtime";
18
+ import { emitWebSocketServer } from "./emit_websocket";
19
+ import {
20
+ emitLogger,
21
+ emitMetrics,
22
+ emitHealthChecks,
23
+ emitFailureRules,
24
+ emitMigrationDiff,
25
+ } from "./emit_maintenance";
26
+ import { emitFlowRuntime } from "./emit_extras";
27
+ import { emitAlgorithmsFile, collectUsedAlgorithms } from "./emit_composition";
28
+ import { emitExtensionPointStub } from "./extension_manager";
29
+ import * as AST from "./ast";
30
+ import { emitDurableEventBus, emitOutboxSchema } from "./emit_events";
31
+ import { emitBatchExecutor } from "./emit_batch";
32
+ import { emitSourceMapFile, emitDebugHandler } from "./emit_sourcemap";
33
+ import { emitTestSuite } from "./emit_tests";
34
+ import { emitDockerfile, emitDockerignore, emitK8sDeployment, emitGithubActions } from "./emit_deploy";
35
+
36
+ function toSnakeCase(s: string): string {
37
+ return s.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase();
38
+ }
39
+
40
+ export class FullEmitter {
41
+ private schemaEmitter = new Emitter();
42
+
43
+ emit(system: IR.IRSystem): EmittedFile[] {
44
+ const files: EmittedFile[] = [];
45
+
46
+ // 1. Package files
47
+ files.push({ path: "package.json", content: emitPackageJson(system), language: "json", source_module: "root" });
48
+ files.push({ path: "tsconfig.json", content: emitTsConfig(), language: "json", source_module: "root" });
49
+ files.push({ path: ".env.example", content: this.emitEnvExample(system), language: "yaml", source_module: "root" });
50
+
51
+ // 2. Source: infrastructure
52
+ files.push({ path: "src/db.ts", content: emitDbClient(system), language: "typescript", source_module: "infra" });
53
+ // Durable event bus replaces the old in-process stub
54
+ files.push({ path: "src/events.ts", content: emitDurableEventBus(system), language: "typescript", source_module: "infra" });
55
+ // Outbox SQL schema
56
+ files.push({ path: "migrations/event_outbox.sql", content: emitOutboxSchema(), language: "sql", source_module: "infra" });
57
+ files.push({ path: "src/auth.ts", content: emitAuthMiddleware(system), language: "typescript", source_module: "infra" });
58
+ files.push({ path: "src/logger.ts", content: emitLogger(system), language: "typescript", source_module: "infra" });
59
+ files.push({ path: "src/metrics.ts", content: emitMetrics(), language: "typescript", source_module: "infra" });
60
+ files.push({ path: "src/health.ts", content: emitHealthChecks(system), language: "typescript", source_module: "infra" });
61
+ files.push({ path: "src/failure_rules.ts", content: emitFailureRules(system), language: "typescript", source_module: "infra" });
62
+
63
+ // 2a. WebSocket server (only if there are realtime channels)
64
+ const wsContent = emitWebSocketServer(system);
65
+ if (wsContent) {
66
+ files.push({ path: "src/websocket.ts", content: wsContent, language: "typescript", source_module: "infra" });
67
+ }
68
+
69
+ // 2b. Flow saga runtime (only if there are flows)
70
+ const flowContent = emitFlowRuntime(system);
71
+ if (flowContent) {
72
+ files.push({ path: "src/flows.ts", content: flowContent, language: "typescript", source_module: "infra" });
73
+ }
74
+
75
+ // 2b2. Batch executor (only if there are batch capabilities)
76
+ const batchContent = emitBatchExecutor(system);
77
+ if (batchContent) {
78
+ files.push({ path: "src/batch.ts", content: batchContent, language: "typescript", source_module: "infra" });
79
+ }
80
+
81
+ // 2c. Migration diff utility (always emitted)
82
+ files.push({ path: "src/migration_diff.ts", content: emitMigrationDiff(), language: "typescript", source_module: "infra" });
83
+
84
+ // 2d. Algorithm implementations (only what's used)
85
+ const usedAlgorithms = collectUsedAlgorithms(system);
86
+ if (usedAlgorithms.size > 0) {
87
+ const algoContent = emitAlgorithmsFile(usedAlgorithms);
88
+ files.push({ path: "src/algorithms.ts", content: algoContent, language: "typescript", source_module: "algorithms" });
89
+ } else {
90
+ files.push({
91
+ path: "src/algorithms.ts",
92
+ content: "// No algorithms used in this system.\nexport {};\n",
93
+ language: "typescript",
94
+ source_module: "algorithms",
95
+ });
96
+ }
97
+
98
+ // 2e. Extension points (escape hatches — preserved across recompilation)
99
+ if (system.extension_points && system.extension_points.length > 0) {
100
+ const extLines: string[] = [
101
+ "// Generated by BoneScript compiler.",
102
+ "// Extension points: implement the functions below.",
103
+ "// Code between sentinel comments is preserved on recompile.",
104
+ "// DO NOT remove the sentinel comments.",
105
+ "",
106
+ ];
107
+ for (const ep of system.extension_points) {
108
+ const params = ep.params.map((p: { name: string; type: string }) => `${p.name}: ${p.type}`).join(", ");
109
+ const returnType = ep.returns || "void";
110
+ extLines.push(`/**`);
111
+ extLines.push(` * Extension point: ${ep.name}`);
112
+ extLines.push(` * ${ep.stable ? "STABLE: implementation required." : "Optional."}`);
113
+ extLines.push(` */`);
114
+ extLines.push(`export function ${ep.name}(${params}): ${returnType} {`);
115
+ extLines.push(` // <bonescript:ext:${ep.name}:begin>`);
116
+ extLines.push(` throw new Error("Not implemented: ${ep.name}");`);
117
+ extLines.push(` // <bonescript:ext:${ep.name}:end>`);
118
+ extLines.push(`}`);
119
+ extLines.push("");
120
+ }
121
+ files.push({
122
+ path: "src/extensions.ts",
123
+ content: extLines.join("\n"),
124
+ language: "typescript",
125
+ source_module: "extensions",
126
+ });
127
+ }
128
+
129
+ // 3. Source: state machines
130
+ for (const mod of system.modules) {
131
+ for (const sm of mod.state_machines) {
132
+ files.push({
133
+ path: `src/state_machines/${toSnakeCase(sm.entity)}.ts`,
134
+ content: emitStateMachineRuntime(sm),
135
+ language: "typescript",
136
+ source_module: mod.id,
137
+ });
138
+ }
139
+ }
140
+
141
+ // 4. Source: route files (CRUD + capabilities)
142
+ for (const mod of system.modules) {
143
+ if (mod.kind === "api_service" && mod.models.length > 0) {
144
+ const content = emitEntityRouter(mod, system);
145
+ if (content) {
146
+ files.push({
147
+ path: `src/routes/${toSnakeCase(mod.models[0].name)}.ts`,
148
+ content,
149
+ language: "typescript",
150
+ source_module: mod.id,
151
+ });
152
+ }
153
+ }
154
+ }
155
+
156
+ // 5. Source: main entry point
157
+ files.push({ path: "src/index.ts", content: emitIndex(system), language: "typescript", source_module: "root" });
158
+
159
+ // 6. SQL migrations — run schema emitter ONCE, then match by model name
160
+ const schemas: string[] = [];
161
+ const allSchemaFiles = this.schemaEmitter.emit(system);
162
+ for (const mod of system.modules) {
163
+ if (mod.kind === "data_store" || mod.kind === "api_service") {
164
+ for (const model of mod.models) {
165
+ const schemaFile = allSchemaFiles.find(f => f.path.includes(toSnakeCase(model.name)) && f.language === "sql");
166
+ if (schemaFile) {
167
+ files.push({ ...schemaFile, path: `migrations/${schemaFile.path.replace("schema/", "")}` });
168
+ schemas.push(schemaFile.content);
169
+ }
170
+ }
171
+ }
172
+ }
173
+
174
+ // 7. Migration runner
175
+ files.push({ path: "src/migrate.ts", content: emitMigration(system, schemas), language: "typescript", source_module: "infra" });
176
+
177
+ // 8. Docker compose for local dev
178
+ files.push({ path: "docker-compose.yaml", content: this.emitDockerCompose(system), language: "yaml", source_module: "infra" });
179
+
180
+ // 9. README
181
+ files.push({ path: "README.md", content: this.emitReadme(system), language: "yaml", source_module: "root" });
182
+
183
+ // 10. Source map + debug handler
184
+ files.push({ path: `${system.name}.bone.map`, content: emitSourceMapFile(system, `${system.name}.bone`), language: "json", source_module: "root" });
185
+ files.push({ path: "src/debug.ts", content: emitDebugHandler(system), language: "typescript", source_module: "infra" });
186
+ files.push({ path: "src/tests.ts", content: emitTestSuite(system), language: "typescript", source_module: "tests" });
187
+
188
+ // 11. Deploy targets
189
+ files.push({ path: "Dockerfile", content: emitDockerfile(system), language: "yaml", source_module: "deploy" });
190
+ files.push({ path: ".dockerignore", content: emitDockerignore(), language: "yaml", source_module: "deploy" });
191
+ files.push({ path: "k8s/deployment.yaml", content: emitK8sDeployment(system), language: "yaml", source_module: "deploy" });
192
+ files.push({ path: ".github/workflows/ci.yaml", content: emitGithubActions(system), language: "yaml", source_module: "deploy" });
193
+
194
+ return files;
195
+ }
196
+
197
+ private emitEnvExample(system: IR.IRSystem): string {
198
+ return `# ${system.name} Environment Variables
199
+ # Copy this file to .env and fill in real values. Never commit .env to source control.
200
+
201
+ # --- Required in production ---
202
+ # Generate with: node -e "console.log(require('crypto').randomBytes(48).toString('hex'))"
203
+ JWT_SECRET=
204
+
205
+ # --- Database ---
206
+ DATABASE_URL=postgresql://postgres:postgres@localhost:5432/${toSnakeCase(system.name)}
207
+
208
+ # --- Redis (optional, used by some domain templates) ---
209
+ REDIS_URL=redis://localhost:6379
210
+
211
+ # --- Server ---
212
+ PORT=3000
213
+ NODE_ENV=development
214
+
215
+ # --- CORS ---
216
+ # Comma-separated list of allowed origins. Leave empty to disallow all cross-origin requests.
217
+ # Example: ALLOWED_ORIGINS=https://app.example.com,https://admin.example.com
218
+ ALLOWED_ORIGINS=
219
+
220
+ # --- Event delivery mode ---
221
+ # in_process: in-memory, fast, no durability guarantees (default for development)
222
+ # durable: Postgres-backed transactional outbox (recommended for production)
223
+ EVENT_MODE=in_process
224
+ EVENT_WORKER_INTERVAL_MS=1000
225
+
226
+ # --- Request timeout ---
227
+ REQUEST_TIMEOUT_MS=30000
228
+ `;
229
+ }
230
+
231
+ private emitDockerCompose(system: IR.IRSystem): string {
232
+ return `# Generated by BoneScript compiler.
233
+ version: "3.8"
234
+
235
+ services:
236
+ postgres:
237
+ image: postgres:16-alpine
238
+ environment:
239
+ POSTGRES_DB: ${toSnakeCase(system.name)}
240
+ POSTGRES_USER: postgres
241
+ POSTGRES_PASSWORD: postgres
242
+ ports:
243
+ - "5432:5432"
244
+ volumes:
245
+ - pgdata:/var/lib/postgresql/data
246
+
247
+ redis:
248
+ image: redis:7-alpine
249
+ ports:
250
+ - "6379:6379"
251
+
252
+ volumes:
253
+ pgdata:
254
+ `;
255
+ }
256
+
257
+ private emitReadme(system: IR.IRSystem): string {
258
+ const apiModules = system.modules.filter(m => m.kind === "api_service");
259
+ const routes = apiModules
260
+ .filter(m => m.models.length > 0)
261
+ .map(m => `- \`/${toSnakeCase(m.models[0].name)}s\` — ${m.name}`)
262
+ .join("\n");
263
+
264
+ return `# ${system.name}
265
+
266
+ Generated by BoneScript compiler. Source hash: ${system.source_hash}
267
+
268
+ ## Quick Start
269
+
270
+ \`\`\`bash
271
+ # Start dependencies
272
+ docker compose up -d
273
+
274
+ # Install
275
+ npm install
276
+
277
+ # Run migrations
278
+ npm run migrate
279
+
280
+ # Start server
281
+ npm run dev
282
+ \`\`\`
283
+
284
+ ## API Routes
285
+
286
+ ${routes}
287
+
288
+ Each route supports:
289
+ - \`GET /\` — List (paginated)
290
+ - \`GET /:id\` — Read
291
+ - \`POST /\` — Create
292
+ - \`PUT /:id\` — Update
293
+ - \`DELETE /:id\` — Delete
294
+
295
+ Plus capability-specific endpoints.
296
+
297
+ ## Auth
298
+
299
+ Send a Bearer token in the Authorization header:
300
+ \`\`\`
301
+ Authorization: Bearer <jwt-token>
302
+ \`\`\`
303
+
304
+ ## Environment
305
+
306
+ Copy \`.env.example\` to \`.env\` and configure.
307
+ `;
308
+ }
309
+ }