bonescript-compiler 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/algorithm_catalog.d.ts +32 -0
- package/dist/algorithm_catalog.js +323 -0
- package/dist/algorithm_catalog.js.map +1 -0
- package/dist/ast.d.ts +244 -0
- package/dist/ast.js +8 -0
- package/dist/ast.js.map +1 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +605 -0
- package/dist/cli.js.map +1 -0
- package/dist/emit_batch.d.ts +7 -0
- package/dist/emit_batch.js +133 -0
- package/dist/emit_batch.js.map +1 -0
- package/dist/emit_capability.d.ts +7 -0
- package/dist/emit_capability.js +376 -0
- package/dist/emit_capability.js.map +1 -0
- package/dist/emit_composition.d.ts +22 -0
- package/dist/emit_composition.js +184 -0
- package/dist/emit_composition.js.map +1 -0
- package/dist/emit_deploy.d.ts +9 -0
- package/dist/emit_deploy.js +191 -0
- package/dist/emit_deploy.js.map +1 -0
- package/dist/emit_events.d.ts +14 -0
- package/dist/emit_events.js +305 -0
- package/dist/emit_events.js.map +1 -0
- package/dist/emit_extras.d.ts +12 -0
- package/dist/emit_extras.js +234 -0
- package/dist/emit_extras.js.map +1 -0
- package/dist/emit_full.d.ts +13 -0
- package/dist/emit_full.js +273 -0
- package/dist/emit_full.js.map +1 -0
- package/dist/emit_maintenance.d.ts +16 -0
- package/dist/emit_maintenance.js +442 -0
- package/dist/emit_maintenance.js.map +1 -0
- package/dist/emit_runtime.d.ts +13 -0
- package/dist/emit_runtime.js +691 -0
- package/dist/emit_runtime.js.map +1 -0
- package/dist/emit_sourcemap.d.ts +29 -0
- package/dist/emit_sourcemap.js +123 -0
- package/dist/emit_sourcemap.js.map +1 -0
- package/dist/emit_tests.d.ts +15 -0
- package/dist/emit_tests.js +185 -0
- package/dist/emit_tests.js.map +1 -0
- package/dist/emit_websocket.d.ts +6 -0
- package/dist/emit_websocket.js +223 -0
- package/dist/emit_websocket.js.map +1 -0
- package/dist/emitter.d.ts +25 -0
- package/dist/emitter.js +511 -0
- package/dist/emitter.js.map +1 -0
- package/dist/extension_manager.d.ts +38 -0
- package/dist/extension_manager.js +170 -0
- package/dist/extension_manager.js.map +1 -0
- package/dist/formatter.d.ts +34 -0
- package/dist/formatter.js +317 -0
- package/dist/formatter.js.map +1 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +113 -0
- package/dist/index.js.map +1 -0
- package/dist/ir.d.ts +168 -0
- package/dist/ir.js +10 -0
- package/dist/ir.js.map +1 -0
- package/dist/lexer.d.ts +195 -0
- package/dist/lexer.js +619 -0
- package/dist/lexer.js.map +1 -0
- package/dist/lowering.d.ts +25 -0
- package/dist/lowering.js +500 -0
- package/dist/lowering.js.map +1 -0
- package/dist/module_loader.d.ts +25 -0
- package/dist/module_loader.js +126 -0
- package/dist/module_loader.js.map +1 -0
- package/dist/optimizer.d.ts +26 -0
- package/dist/optimizer.js +158 -0
- package/dist/optimizer.js.map +1 -0
- package/dist/parse_decls.d.ts +13 -0
- package/dist/parse_decls.js +442 -0
- package/dist/parse_decls.js.map +1 -0
- package/dist/parse_decls2.d.ts +13 -0
- package/dist/parse_decls2.js +295 -0
- package/dist/parse_decls2.js.map +1 -0
- package/dist/parse_expr.d.ts +7 -0
- package/dist/parse_expr.js +197 -0
- package/dist/parse_expr.js.map +1 -0
- package/dist/parse_types.d.ts +6 -0
- package/dist/parse_types.js +51 -0
- package/dist/parse_types.js.map +1 -0
- package/dist/parser.d.ts +10 -0
- package/dist/parser.js +62 -0
- package/dist/parser.js.map +1 -0
- package/dist/parser_base.d.ts +19 -0
- package/dist/parser_base.js +50 -0
- package/dist/parser_base.js.map +1 -0
- package/dist/parser_recovery.d.ts +26 -0
- package/dist/parser_recovery.js +140 -0
- package/dist/parser_recovery.js.map +1 -0
- package/dist/scaffold.d.ts +13 -0
- package/dist/scaffold.js +376 -0
- package/dist/scaffold.js.map +1 -0
- package/dist/solver.d.ts +26 -0
- package/dist/solver.js +281 -0
- package/dist/solver.js.map +1 -0
- package/dist/typechecker.d.ts +52 -0
- package/dist/typechecker.js +534 -0
- package/dist/typechecker.js.map +1 -0
- package/dist/types.d.ts +38 -0
- package/dist/types.js +85 -0
- package/dist/types.js.map +1 -0
- package/dist/verifier.d.ts +46 -0
- package/dist/verifier.js +307 -0
- package/dist/verifier.js.map +1 -0
- package/package.json +52 -0
- package/src/algorithm_catalog.ts +345 -0
- package/src/ast.ts +334 -0
- package/src/cli.ts +624 -0
- package/src/emit_batch.ts +140 -0
- package/src/emit_capability.ts +436 -0
- package/src/emit_composition.ts +196 -0
- package/src/emit_deploy.ts +190 -0
- package/src/emit_events.ts +307 -0
- package/src/emit_extras.ts +240 -0
- package/src/emit_full.ts +309 -0
- package/src/emit_maintenance.ts +459 -0
- package/src/emit_runtime.ts +731 -0
- package/src/emit_sourcemap.ts +140 -0
- package/src/emit_tests.ts +205 -0
- package/src/emit_websocket.ts +229 -0
- package/src/emitter.ts +566 -0
- package/src/extension_manager.ts +187 -0
- package/src/formatter.ts +297 -0
- package/src/index.ts +88 -0
- package/src/ir.ts +215 -0
- package/src/lexer.ts +630 -0
- package/src/lowering.ts +556 -0
- package/src/module_loader.ts +114 -0
- package/src/optimizer.ts +196 -0
- package/src/parse_decls.ts +409 -0
- package/src/parse_decls2.ts +244 -0
- package/src/parse_expr.ts +197 -0
- package/src/parse_types.ts +54 -0
- package/src/parser.ts +64 -0
- package/src/parser_base.ts +57 -0
- package/src/parser_recovery.ts +153 -0
- package/src/scaffold.ts +375 -0
- package/src/solver.ts +330 -0
- package/src/typechecker.ts +591 -0
- package/src/types.ts +122 -0
- package/src/verifier.ts +348 -0
package/src/scaffold.ts
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BoneScript Project Scaffolder — `bone init`
|
|
3
|
+
* Creates a new BoneScript project with sensible defaults for a chosen domain.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as fs from "fs";
|
|
7
|
+
import * as path from "path";
|
|
8
|
+
|
|
9
|
+
export type ScaffoldDomain =
|
|
10
|
+
| "multiplayer_game"
|
|
11
|
+
| "saas_platform"
|
|
12
|
+
| "iot_system"
|
|
13
|
+
| "social_network"
|
|
14
|
+
| "marketplace"
|
|
15
|
+
| "realtime_collaboration";
|
|
16
|
+
|
|
17
|
+
const TEMPLATES: Record<ScaffoldDomain, string> = {
|
|
18
|
+
multiplayer_game: `system MyGame {
|
|
19
|
+
domain: multiplayer_game
|
|
20
|
+
|
|
21
|
+
entity Player {
|
|
22
|
+
owns: [
|
|
23
|
+
username: string,
|
|
24
|
+
score: uint
|
|
25
|
+
]
|
|
26
|
+
constraints: [
|
|
27
|
+
username.unique,
|
|
28
|
+
username.length in 3..32,
|
|
29
|
+
score >= 0
|
|
30
|
+
]
|
|
31
|
+
states: active -> suspended | deleted
|
|
32
|
+
auth: jwt
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
capability award_points(player: Player, points: uint) {
|
|
36
|
+
requires: [points > 0, player.state == "active"]
|
|
37
|
+
effects: [player.score += points]
|
|
38
|
+
sync: eventual
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
store PlayerStore {
|
|
42
|
+
engine: postgresql
|
|
43
|
+
schema: {
|
|
44
|
+
id: uuid,
|
|
45
|
+
username: string,
|
|
46
|
+
score: uint,
|
|
47
|
+
state: string
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
channel game_lobby {
|
|
52
|
+
transport: websocket
|
|
53
|
+
ordering: causal
|
|
54
|
+
participants: set<Player>
|
|
55
|
+
persistence: last_100
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
`,
|
|
59
|
+
|
|
60
|
+
saas_platform: `system MySaaS {
|
|
61
|
+
domain: saas_platform
|
|
62
|
+
|
|
63
|
+
entity Tenant {
|
|
64
|
+
owns: [
|
|
65
|
+
name: string,
|
|
66
|
+
plan: string,
|
|
67
|
+
active: bool
|
|
68
|
+
]
|
|
69
|
+
constraints: [
|
|
70
|
+
name.length in 1..100,
|
|
71
|
+
plan in ["free", "pro", "enterprise"]
|
|
72
|
+
]
|
|
73
|
+
states: trialing -> active -> suspended | cancelled
|
|
74
|
+
auth: oauth2
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
entity User {
|
|
78
|
+
owns: [
|
|
79
|
+
email: string,
|
|
80
|
+
tenant_id: uuid,
|
|
81
|
+
role: string
|
|
82
|
+
]
|
|
83
|
+
constraints: [
|
|
84
|
+
email.unique,
|
|
85
|
+
role in ["admin", "member", "viewer"]
|
|
86
|
+
]
|
|
87
|
+
auth: oauth2
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
capability invite_user(tenant: Tenant, email: string, role: string) {
|
|
91
|
+
requires: [tenant.state == "active", role in ["admin", "member", "viewer"]]
|
|
92
|
+
effects: []
|
|
93
|
+
emits: UserInvited
|
|
94
|
+
sync: transactional
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
event UserInvited {
|
|
98
|
+
payload: {
|
|
99
|
+
tenant_id: uuid,
|
|
100
|
+
email: string,
|
|
101
|
+
role: string,
|
|
102
|
+
invited_at: timestamp
|
|
103
|
+
}
|
|
104
|
+
delivery: at_least_once
|
|
105
|
+
ttl: 7d
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
store TenantStore {
|
|
109
|
+
engine: postgresql
|
|
110
|
+
schema: {
|
|
111
|
+
id: uuid,
|
|
112
|
+
name: string,
|
|
113
|
+
plan: string,
|
|
114
|
+
active: bool,
|
|
115
|
+
state: string
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
`,
|
|
120
|
+
|
|
121
|
+
iot_system: `system MyIoT {
|
|
122
|
+
domain: iot_system
|
|
123
|
+
|
|
124
|
+
entity Device {
|
|
125
|
+
owns: [
|
|
126
|
+
serial: string,
|
|
127
|
+
firmware_version: string,
|
|
128
|
+
last_seen: timestamp,
|
|
129
|
+
battery: uint
|
|
130
|
+
]
|
|
131
|
+
constraints: [
|
|
132
|
+
serial.unique,
|
|
133
|
+
battery <= 100
|
|
134
|
+
]
|
|
135
|
+
states: online -> offline -> retired
|
|
136
|
+
auth: apikey
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
entity Reading {
|
|
140
|
+
owns: [
|
|
141
|
+
device_id: uuid,
|
|
142
|
+
sensor: string,
|
|
143
|
+
value: float,
|
|
144
|
+
recorded_at: timestamp
|
|
145
|
+
]
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
capability ingest_reading(device: Device, sensor: string, value: float) {
|
|
149
|
+
requires: [device.state == "online"]
|
|
150
|
+
effects: [device.last_seen = now()]
|
|
151
|
+
emits: ReadingRecorded
|
|
152
|
+
sync: eventual
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
event ReadingRecorded {
|
|
156
|
+
payload: {
|
|
157
|
+
device_id: uuid,
|
|
158
|
+
sensor: string,
|
|
159
|
+
value: float
|
|
160
|
+
}
|
|
161
|
+
delivery: at_least_once
|
|
162
|
+
ttl: 1d
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
store DeviceStore {
|
|
166
|
+
engine: dynamodb
|
|
167
|
+
schema: {
|
|
168
|
+
id: uuid,
|
|
169
|
+
serial: string,
|
|
170
|
+
firmware_version: string,
|
|
171
|
+
battery: uint,
|
|
172
|
+
state: string
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
`,
|
|
177
|
+
|
|
178
|
+
social_network: `system MySocial {
|
|
179
|
+
domain: social_network
|
|
180
|
+
|
|
181
|
+
entity User {
|
|
182
|
+
owns: [
|
|
183
|
+
handle: string,
|
|
184
|
+
display_name: string,
|
|
185
|
+
followers: set<uuid>,
|
|
186
|
+
following: set<uuid>
|
|
187
|
+
]
|
|
188
|
+
constraints: [
|
|
189
|
+
handle.unique,
|
|
190
|
+
handle.length in 3..30
|
|
191
|
+
]
|
|
192
|
+
auth: oauth2
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
entity Post {
|
|
196
|
+
owns: [
|
|
197
|
+
author_id: uuid,
|
|
198
|
+
content: string,
|
|
199
|
+
likes: uint
|
|
200
|
+
]
|
|
201
|
+
constraints: [
|
|
202
|
+
content.length in 1..500
|
|
203
|
+
]
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
capability follow_user(follower: User, target: User) {
|
|
207
|
+
requires: [follower != target]
|
|
208
|
+
effects: [
|
|
209
|
+
follower.following += target.id,
|
|
210
|
+
target.followers += follower.id
|
|
211
|
+
]
|
|
212
|
+
sync: eventual
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
channel feed {
|
|
216
|
+
transport: websocket
|
|
217
|
+
ordering: causal
|
|
218
|
+
participants: set<User>
|
|
219
|
+
persistence: last_100
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
`,
|
|
223
|
+
|
|
224
|
+
marketplace: `system MyMarket {
|
|
225
|
+
domain: marketplace
|
|
226
|
+
|
|
227
|
+
entity Listing {
|
|
228
|
+
owns: [
|
|
229
|
+
seller_id: uuid,
|
|
230
|
+
title: string,
|
|
231
|
+
price: uint,
|
|
232
|
+
stock: uint
|
|
233
|
+
]
|
|
234
|
+
constraints: [
|
|
235
|
+
title.length in 1..200,
|
|
236
|
+
price > 0,
|
|
237
|
+
stock >= 0
|
|
238
|
+
]
|
|
239
|
+
states: draft -> active -> sold_out | archived
|
|
240
|
+
auth: oauth2
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
entity Order {
|
|
244
|
+
owns: [
|
|
245
|
+
buyer_id: uuid,
|
|
246
|
+
listing_id: uuid,
|
|
247
|
+
quantity: uint,
|
|
248
|
+
total: uint
|
|
249
|
+
]
|
|
250
|
+
states: pending -> paid -> shipped -> delivered | cancelled
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
capability purchase(buyer: User, listing: Listing, qty: uint) {
|
|
254
|
+
requires: [
|
|
255
|
+
listing.state == "active",
|
|
256
|
+
listing.stock >= qty
|
|
257
|
+
]
|
|
258
|
+
effects: [
|
|
259
|
+
listing.stock -= qty
|
|
260
|
+
]
|
|
261
|
+
emits: OrderCreated
|
|
262
|
+
sync: transactional
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
event OrderCreated {
|
|
266
|
+
payload: {
|
|
267
|
+
order_id: uuid,
|
|
268
|
+
buyer_id: uuid,
|
|
269
|
+
listing_id: uuid,
|
|
270
|
+
total: uint
|
|
271
|
+
}
|
|
272
|
+
delivery: exactly_once
|
|
273
|
+
ttl: 30d
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
`,
|
|
277
|
+
|
|
278
|
+
realtime_collaboration: `system MyCollab {
|
|
279
|
+
domain: realtime_collaboration
|
|
280
|
+
|
|
281
|
+
entity Document {
|
|
282
|
+
owns: [
|
|
283
|
+
title: string,
|
|
284
|
+
owner_id: uuid,
|
|
285
|
+
content: json,
|
|
286
|
+
version: uint
|
|
287
|
+
]
|
|
288
|
+
constraints: [
|
|
289
|
+
version >= 1
|
|
290
|
+
]
|
|
291
|
+
auth: jwt
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
entity Cursor {
|
|
295
|
+
owns: [
|
|
296
|
+
document_id: uuid,
|
|
297
|
+
user_id: uuid,
|
|
298
|
+
position: uint,
|
|
299
|
+
color: string
|
|
300
|
+
]
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
capability apply_change(doc: Document, user: User, change: json) {
|
|
304
|
+
requires: [doc.owner_id == user.id or doc.collaborators contains user.id]
|
|
305
|
+
effects: [
|
|
306
|
+
doc.version += 1,
|
|
307
|
+
doc.content = change
|
|
308
|
+
]
|
|
309
|
+
emits: DocumentChanged
|
|
310
|
+
sync: realtime
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
channel doc_session {
|
|
314
|
+
transport: websocket
|
|
315
|
+
ordering: causal
|
|
316
|
+
participants: set<User>
|
|
317
|
+
persistence: last_1000
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
event DocumentChanged {
|
|
321
|
+
payload: {
|
|
322
|
+
document_id: uuid,
|
|
323
|
+
user_id: uuid,
|
|
324
|
+
version: uint
|
|
325
|
+
}
|
|
326
|
+
delivery: at_least_once
|
|
327
|
+
ttl: 1d
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
`,
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
export interface ScaffoldOptions {
|
|
334
|
+
name: string;
|
|
335
|
+
domain: ScaffoldDomain;
|
|
336
|
+
outDir: string;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export function scaffold(opts: ScaffoldOptions): { created: string[] } {
|
|
340
|
+
const created: string[] = [];
|
|
341
|
+
|
|
342
|
+
if (!fs.existsSync(opts.outDir)) {
|
|
343
|
+
fs.mkdirSync(opts.outDir, { recursive: true });
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Main .bone file
|
|
347
|
+
const mainFile = path.join(opts.outDir, `${opts.name}.bone`);
|
|
348
|
+
let content = TEMPLATES[opts.domain];
|
|
349
|
+
// Replace placeholder system name with provided name
|
|
350
|
+
content = content.replace(/^system \w+ \{/, `system ${pascalCase(opts.name)} {`);
|
|
351
|
+
fs.writeFileSync(mainFile, content, "utf-8");
|
|
352
|
+
created.push(mainFile);
|
|
353
|
+
|
|
354
|
+
// README
|
|
355
|
+
const readmePath = path.join(opts.outDir, "README.md");
|
|
356
|
+
fs.writeFileSync(readmePath, `# ${opts.name}
|
|
357
|
+
|
|
358
|
+
BoneScript project (domain: ${opts.domain}).
|
|
359
|
+
|
|
360
|
+
## Compile
|
|
361
|
+
|
|
362
|
+
\`\`\`bash
|
|
363
|
+
bone compile ${opts.name}.bone
|
|
364
|
+
\`\`\`
|
|
365
|
+
|
|
366
|
+
The output will be written to \`./output/\` as a complete Node.js project.
|
|
367
|
+
`, "utf-8");
|
|
368
|
+
created.push(readmePath);
|
|
369
|
+
|
|
370
|
+
return { created };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function pascalCase(s: string): string {
|
|
374
|
+
return s.replace(/(^|[-_\s])(\w)/g, (_, __, c) => c.toUpperCase());
|
|
375
|
+
}
|
package/src/solver.ts
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BoneScript Constraint Solver — Stage 5 of the compilation pipeline.
|
|
3
|
+
* Implements spec/06_CONSTRAINT_SOLVER.md.
|
|
4
|
+
*
|
|
5
|
+
* Resolves all underspecified aspects of the IR into concrete decisions.
|
|
6
|
+
* Uses ONLY: ontology implication rules, domain defaults, structural necessity.
|
|
7
|
+
* NO heuristics. NO probabilistic matching.
|
|
8
|
+
*
|
|
9
|
+
* Phases:
|
|
10
|
+
* 1. Collect — gather all constraints
|
|
11
|
+
* 2. Normalize — canonical form
|
|
12
|
+
* 3. Propagate — unit propagation
|
|
13
|
+
* 4. Check — verify consistency
|
|
14
|
+
* 5. Complete — fill remaining with defaults
|
|
15
|
+
* 6. Verify — final pass
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import * as IR from "./ir";
|
|
19
|
+
|
|
20
|
+
// ─── Domain Defaults ─────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
interface DomainDefaults {
|
|
23
|
+
auth: string;
|
|
24
|
+
engine: string;
|
|
25
|
+
session_engine: string;
|
|
26
|
+
sync: string;
|
|
27
|
+
channel_transport: string;
|
|
28
|
+
channel_ordering: string;
|
|
29
|
+
channel_persistence: string;
|
|
30
|
+
session_ttl_ms: number;
|
|
31
|
+
max_connections: number;
|
|
32
|
+
rate_limit: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const DOMAIN_DEFAULTS: Record<string, DomainDefaults> = {
|
|
36
|
+
multiplayer_game: {
|
|
37
|
+
auth: "jwt",
|
|
38
|
+
engine: "postgresql",
|
|
39
|
+
session_engine: "redis",
|
|
40
|
+
sync: "realtime",
|
|
41
|
+
channel_transport: "websocket",
|
|
42
|
+
channel_ordering: "causal",
|
|
43
|
+
channel_persistence: "last_100",
|
|
44
|
+
session_ttl_ms: 3_600_000,
|
|
45
|
+
max_connections: 10000,
|
|
46
|
+
rate_limit: 1000,
|
|
47
|
+
},
|
|
48
|
+
saas_platform: {
|
|
49
|
+
auth: "oauth2",
|
|
50
|
+
engine: "postgresql",
|
|
51
|
+
session_engine: "redis",
|
|
52
|
+
sync: "eventual",
|
|
53
|
+
channel_transport: "sse",
|
|
54
|
+
channel_ordering: "fifo",
|
|
55
|
+
channel_persistence: "full",
|
|
56
|
+
session_ttl_ms: 86_400_000,
|
|
57
|
+
max_connections: 5000,
|
|
58
|
+
rate_limit: 500,
|
|
59
|
+
},
|
|
60
|
+
iot_system: {
|
|
61
|
+
auth: "apikey",
|
|
62
|
+
engine: "dynamodb",
|
|
63
|
+
session_engine: "redis",
|
|
64
|
+
sync: "eventual",
|
|
65
|
+
channel_transport: "grpc_stream",
|
|
66
|
+
channel_ordering: "fifo",
|
|
67
|
+
channel_persistence: "last_1000",
|
|
68
|
+
session_ttl_ms: 7_200_000,
|
|
69
|
+
max_connections: 100000,
|
|
70
|
+
rate_limit: 10000,
|
|
71
|
+
},
|
|
72
|
+
social_network: {
|
|
73
|
+
auth: "oauth2",
|
|
74
|
+
engine: "postgresql",
|
|
75
|
+
session_engine: "redis",
|
|
76
|
+
sync: "eventual",
|
|
77
|
+
channel_transport: "websocket",
|
|
78
|
+
channel_ordering: "causal",
|
|
79
|
+
channel_persistence: "last_100",
|
|
80
|
+
session_ttl_ms: 86_400_000,
|
|
81
|
+
max_connections: 50000,
|
|
82
|
+
rate_limit: 2000,
|
|
83
|
+
},
|
|
84
|
+
marketplace: {
|
|
85
|
+
auth: "oauth2",
|
|
86
|
+
engine: "postgresql",
|
|
87
|
+
session_engine: "redis",
|
|
88
|
+
sync: "transactional",
|
|
89
|
+
channel_transport: "sse",
|
|
90
|
+
channel_ordering: "fifo",
|
|
91
|
+
channel_persistence: "full",
|
|
92
|
+
session_ttl_ms: 3_600_000,
|
|
93
|
+
max_connections: 10000,
|
|
94
|
+
rate_limit: 1000,
|
|
95
|
+
},
|
|
96
|
+
realtime_collaboration: {
|
|
97
|
+
auth: "jwt",
|
|
98
|
+
engine: "postgresql",
|
|
99
|
+
session_engine: "redis",
|
|
100
|
+
sync: "realtime",
|
|
101
|
+
channel_transport: "websocket",
|
|
102
|
+
channel_ordering: "causal",
|
|
103
|
+
channel_persistence: "full",
|
|
104
|
+
session_ttl_ms: 7_200_000,
|
|
105
|
+
max_connections: 10000,
|
|
106
|
+
rate_limit: 5000,
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const FALLBACK_DEFAULTS: DomainDefaults = {
|
|
111
|
+
auth: "jwt",
|
|
112
|
+
engine: "postgresql",
|
|
113
|
+
session_engine: "redis",
|
|
114
|
+
sync: "eventual",
|
|
115
|
+
channel_transport: "websocket",
|
|
116
|
+
channel_ordering: "fifo",
|
|
117
|
+
channel_persistence: "none",
|
|
118
|
+
session_ttl_ms: 3_600_000,
|
|
119
|
+
max_connections: 10000,
|
|
120
|
+
rate_limit: 1000,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// ─── Solver Output ───────────────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
export interface SolverResult {
|
|
126
|
+
resolution: Record<string, string>;
|
|
127
|
+
assumptions: string[];
|
|
128
|
+
warnings: string[];
|
|
129
|
+
errors: string[];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ─── Solver ──────────────────────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
export class ConstraintSolver {
|
|
135
|
+
solve(system: IR.IRSystem): SolverResult {
|
|
136
|
+
const resolution: Record<string, string> = {};
|
|
137
|
+
const assumptions: string[] = [];
|
|
138
|
+
const warnings: string[] = [];
|
|
139
|
+
const errors: string[] = [];
|
|
140
|
+
|
|
141
|
+
const defaults = DOMAIN_DEFAULTS[system.domain || ""] || FALLBACK_DEFAULTS;
|
|
142
|
+
|
|
143
|
+
if (!system.domain) {
|
|
144
|
+
assumptions.push("No domain specified. Using fallback defaults.");
|
|
145
|
+
} else {
|
|
146
|
+
assumptions.push(`Domain '${system.domain}' selected. Applying domain defaults.`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ─── Phase 1: Collect ────────────────────────────────────────────────────
|
|
150
|
+
|
|
151
|
+
// Collect all unresolved variables from modules
|
|
152
|
+
const unresolvedVars: Map<string, string | null> = new Map();
|
|
153
|
+
|
|
154
|
+
for (const mod of system.modules) {
|
|
155
|
+
const prefix = `${mod.name}`;
|
|
156
|
+
|
|
157
|
+
if (mod.kind === "data_store") {
|
|
158
|
+
const engine = mod.config["engine"] as string | undefined;
|
|
159
|
+
if (!engine) {
|
|
160
|
+
unresolvedVars.set(`${prefix}.engine`, null);
|
|
161
|
+
} else {
|
|
162
|
+
resolution[`${prefix}.engine`] = engine;
|
|
163
|
+
}
|
|
164
|
+
if (mod.config["replicas"] === undefined) {
|
|
165
|
+
unresolvedVars.set(`${prefix}.replicas`, null);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (mod.kind === "api_service") {
|
|
170
|
+
const authMethod = mod.config["auth_method"] as string | undefined;
|
|
171
|
+
if (!authMethod || authMethod === "none") {
|
|
172
|
+
unresolvedVars.set(`${prefix}.auth_method`, null);
|
|
173
|
+
} else {
|
|
174
|
+
resolution[`${prefix}.auth_method`] = authMethod;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (mod.kind === "realtime_service") {
|
|
179
|
+
const transport = mod.config["transport"] as string | undefined;
|
|
180
|
+
if (!transport) unresolvedVars.set(`${prefix}.transport`, null);
|
|
181
|
+
else resolution[`${prefix}.transport`] = transport;
|
|
182
|
+
|
|
183
|
+
const ordering = mod.config["ordering"] as string | undefined;
|
|
184
|
+
if (!ordering) unresolvedVars.set(`${prefix}.ordering`, null);
|
|
185
|
+
else resolution[`${prefix}.ordering`] = ordering;
|
|
186
|
+
|
|
187
|
+
const persistence = mod.config["persistence"] as string | undefined;
|
|
188
|
+
if (!persistence || persistence === "none") {
|
|
189
|
+
unresolvedVars.set(`${prefix}.persistence`, null);
|
|
190
|
+
} else {
|
|
191
|
+
resolution[`${prefix}.persistence`] = persistence;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (mod.kind === "gateway") {
|
|
196
|
+
if (!mod.config["rate_limit"]) {
|
|
197
|
+
unresolvedVars.set(`${prefix}.rate_limit`, null);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ─── Phase 2: Normalize ──────────────────────────────────────────────────
|
|
203
|
+
// (constraints are already in canonical form from the IR)
|
|
204
|
+
|
|
205
|
+
// ─── Phase 3: Propagate ──────────────────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
// Apply domain defaults to unresolved variables
|
|
208
|
+
for (const [varName] of unresolvedVars) {
|
|
209
|
+
const parts = varName.split(".");
|
|
210
|
+
const prop = parts[parts.length - 1];
|
|
211
|
+
|
|
212
|
+
switch (prop) {
|
|
213
|
+
case "engine":
|
|
214
|
+
resolution[varName] = defaults.engine;
|
|
215
|
+
assumptions.push(`${varName} = ${defaults.engine} (domain default)`);
|
|
216
|
+
break;
|
|
217
|
+
case "auth_method":
|
|
218
|
+
resolution[varName] = defaults.auth;
|
|
219
|
+
assumptions.push(`${varName} = ${defaults.auth} (domain default)`);
|
|
220
|
+
break;
|
|
221
|
+
case "transport":
|
|
222
|
+
resolution[varName] = defaults.channel_transport;
|
|
223
|
+
assumptions.push(`${varName} = ${defaults.channel_transport} (domain default)`);
|
|
224
|
+
break;
|
|
225
|
+
case "ordering":
|
|
226
|
+
resolution[varName] = defaults.channel_ordering;
|
|
227
|
+
assumptions.push(`${varName} = ${defaults.channel_ordering} (domain default)`);
|
|
228
|
+
break;
|
|
229
|
+
case "persistence":
|
|
230
|
+
resolution[varName] = defaults.channel_persistence;
|
|
231
|
+
assumptions.push(`${varName} = ${defaults.channel_persistence} (domain default)`);
|
|
232
|
+
break;
|
|
233
|
+
case "rate_limit":
|
|
234
|
+
resolution[varName] = String(defaults.rate_limit);
|
|
235
|
+
assumptions.push(`${varName} = ${defaults.rate_limit} (domain default)`);
|
|
236
|
+
break;
|
|
237
|
+
case "replicas":
|
|
238
|
+
resolution[varName] = "1";
|
|
239
|
+
assumptions.push(`${varName} = 1 (minimum default)`);
|
|
240
|
+
break;
|
|
241
|
+
default:
|
|
242
|
+
warnings.push(`Unresolved variable '${varName}' has no default.`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ─── Implication Rules ───────────────────────────────────────────────────
|
|
247
|
+
|
|
248
|
+
// Rule: If any module has auth != none, system needs a session store
|
|
249
|
+
const hasAuth = system.modules.some(m =>
|
|
250
|
+
m.config["authenticated"] === true || (m.config["auth_method"] && m.config["auth_method"] !== "none")
|
|
251
|
+
);
|
|
252
|
+
if (hasAuth) {
|
|
253
|
+
const hasSessionStore = system.modules.some(m =>
|
|
254
|
+
m.kind === "data_store" && (m.name.toLowerCase().includes("session") || m.config["engine"] === "redis")
|
|
255
|
+
);
|
|
256
|
+
if (!hasSessionStore) {
|
|
257
|
+
resolution["implied.session_store"] = "required";
|
|
258
|
+
resolution["implied.session_store.engine"] = defaults.session_engine;
|
|
259
|
+
resolution["implied.session_store.ttl_ms"] = String(defaults.session_ttl_ms);
|
|
260
|
+
assumptions.push(`Auth detected → session store required (${defaults.session_engine}, TTL=${defaults.session_ttl_ms}ms)`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Rule: If any realtime_service exists, event_bus is implied
|
|
265
|
+
const hasRealtime = system.modules.some(m => m.kind === "realtime_service");
|
|
266
|
+
if (hasRealtime) {
|
|
267
|
+
resolution["implied.event_bus"] = "required";
|
|
268
|
+
resolution["implied.event_bus.engine"] = "redis_pubsub";
|
|
269
|
+
assumptions.push("Realtime service detected → event bus required (redis_pubsub)");
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Rule: If events exist with delivery=exactly_once, transactional outbox is implied
|
|
273
|
+
const hasExactlyOnce = system.events.some(e => e.delivery === "exactly_once");
|
|
274
|
+
if (hasExactlyOnce) {
|
|
275
|
+
resolution["implied.outbox"] = "required";
|
|
276
|
+
resolution["implied.outbox.engine"] = defaults.engine;
|
|
277
|
+
assumptions.push("exactly_once delivery detected → transactional outbox required");
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Rule: If flows exist, saga coordinator is implied
|
|
281
|
+
if (system.flows.length > 0) {
|
|
282
|
+
resolution["implied.saga_coordinator"] = "required";
|
|
283
|
+
assumptions.push("Flow declarations detected → saga coordinator required");
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ─── Phase 4: Check Consistency ──────────────────────────────────────────
|
|
287
|
+
|
|
288
|
+
// Check: total ordering + polling is invalid
|
|
289
|
+
for (const mod of system.modules) {
|
|
290
|
+
if (mod.kind === "realtime_service") {
|
|
291
|
+
const transport = resolution[`${mod.name}.transport`] || mod.config["transport"];
|
|
292
|
+
const ordering = resolution[`${mod.name}.ordering`] || mod.config["ordering"];
|
|
293
|
+
if (ordering === "total" && transport === "polling") {
|
|
294
|
+
errors.push(`Exclusion rule violated: ${mod.name} has ordering=total with transport=polling (incompatible)`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Check: realtime sync requires at_least_once delivery
|
|
300
|
+
for (const ev of system.events) {
|
|
301
|
+
if (ev.delivery === "at_most_once") {
|
|
302
|
+
const sourceModule = system.modules.find(m => m.id === ev.source);
|
|
303
|
+
if (sourceModule && sourceModule.kind === "realtime_service") {
|
|
304
|
+
warnings.push(`Event '${ev.name}' uses at_most_once delivery on a realtime service. Consider at_least_once.`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ─── Phase 5: Complete ───────────────────────────────────────────────────
|
|
310
|
+
|
|
311
|
+
// Global resolution entries
|
|
312
|
+
resolution["system.name"] = system.name;
|
|
313
|
+
resolution["system.version"] = system.version;
|
|
314
|
+
resolution["system.domain"] = system.domain || "generic";
|
|
315
|
+
resolution["system.default_timeout_ms"] = "30000";
|
|
316
|
+
resolution["system.default_page_size"] = "50";
|
|
317
|
+
resolution["system.max_connections"] = String(defaults.max_connections);
|
|
318
|
+
|
|
319
|
+
// ─── Phase 6: Verify ─────────────────────────────────────────────────────
|
|
320
|
+
|
|
321
|
+
// All variables should now be resolved
|
|
322
|
+
for (const [varName] of unresolvedVars) {
|
|
323
|
+
if (!resolution[varName]) {
|
|
324
|
+
errors.push(`C002: Unresolvable variable '${varName}' — no default available. Programmer must specify.`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return { resolution, assumptions, warnings, errors };
|
|
329
|
+
}
|
|
330
|
+
}
|