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.
- package/LICENSE +21 -21
- package/dist/algorithm_catalog.js +166 -166
- package/dist/cli.d.ts +1 -2
- package/dist/cli.js +543 -75
- package/dist/cli.js.map +1 -1
- package/dist/emit_capability.d.ts +0 -13
- package/dist/emit_capability.js +134 -296
- package/dist/emit_capability.js.map +1 -1
- package/dist/emit_composition.js +3 -37
- package/dist/emit_composition.js.map +1 -1
- package/dist/emit_deploy.js +167 -165
- package/dist/emit_deploy.js.map +1 -1
- package/dist/emit_events.d.ts +0 -1
- package/dist/emit_events.js +275 -325
- package/dist/emit_events.js.map +1 -1
- package/dist/emit_extras.js +5 -3
- package/dist/emit_extras.js.map +1 -1
- package/dist/emit_full.js +112 -272
- package/dist/emit_full.js.map +1 -1
- package/dist/emit_maintenance.js +249 -249
- package/dist/emit_runtime.d.ts +11 -17
- package/dist/emit_runtime.js +688 -29
- package/dist/emit_runtime.js.map +1 -1
- package/dist/emit_sourcemap.js +66 -66
- package/dist/emit_tests.js +12 -47
- package/dist/emit_tests.js.map +1 -1
- package/dist/emit_websocket.js +3 -0
- package/dist/emit_websocket.js.map +1 -1
- package/dist/emitter.js +49 -94
- package/dist/emitter.js.map +1 -1
- package/dist/extension_manager.d.ts +2 -2
- package/dist/extension_manager.js +20 -9
- package/dist/extension_manager.js.map +1 -1
- package/dist/ir.d.ts +0 -4
- package/dist/lowering.d.ts +14 -5
- package/dist/lowering.js +417 -66
- package/dist/lowering.js.map +1 -1
- package/dist/module_loader.d.ts +2 -2
- package/dist/module_loader.js +23 -20
- package/dist/module_loader.js.map +1 -1
- package/dist/optimizer.js +3 -6
- package/dist/optimizer.js.map +1 -1
- package/dist/scaffold.d.ts +2 -2
- package/dist/scaffold.js +319 -315
- package/dist/scaffold.js.map +1 -1
- package/dist/solver.js +1 -1
- package/dist/solver.js.map +1 -1
- package/dist/source_map.js.map +1 -0
- package/dist/test.js.map +1 -0
- package/dist/test_typechecker.d.ts +5 -0
- package/dist/test_typechecker.js +126 -0
- package/dist/test_typechecker.js.map +1 -0
- package/dist/typechecker.d.ts +0 -7
- package/dist/typechecker.js +16 -103
- package/dist/typechecker.js.map +1 -1
- package/dist/verifier.d.ts +1 -5
- package/dist/verifier.js +38 -142
- package/dist/verifier.js.map +1 -1
- package/package.json +52 -62
- package/src/algorithm_catalog.ts +345 -345
- package/src/ast.d.ts +244 -0
- package/src/ast.ts +334 -334
- package/src/cli.ts +624 -98
- package/src/emit_batch.ts +140 -140
- package/src/emit_capability.ts +436 -613
- package/src/emit_composition.ts +196 -229
- package/src/emit_deploy.ts +190 -187
- package/src/emit_events.ts +307 -362
- package/src/emit_extras.ts +240 -237
- package/src/emit_full.ts +309 -472
- package/src/emit_maintenance.ts +459 -459
- package/src/emit_runtime.ts +730 -17
- package/src/emit_sourcemap.ts +140 -140
- package/src/emit_tests.ts +205 -243
- package/src/emit_websocket.ts +229 -226
- package/src/emitter.ts +578 -626
- package/src/extension_manager.ts +187 -177
- package/src/formatter.ts +297 -297
- package/src/index.ts +88 -88
- package/src/ir.ts +215 -216
- package/src/lexer.d.ts +195 -0
- package/src/lexer.ts +630 -630
- package/src/lowering.ts +556 -168
- package/src/module_loader.ts +114 -112
- package/src/optimizer.ts +196 -199
- package/src/parse_decls.d.ts +13 -0
- package/src/parse_decls.ts +409 -409
- package/src/parse_decls2.d.ts +13 -0
- package/src/parse_decls2.ts +244 -244
- package/src/parse_expr.d.ts +7 -0
- package/src/parse_expr.ts +197 -197
- package/src/parse_types.d.ts +6 -0
- package/src/parse_types.ts +54 -54
- package/src/parser.d.ts +10 -0
- package/src/parser.ts +1 -1
- package/src/parser_base.d.ts +19 -0
- package/src/parser_base.ts +57 -57
- package/src/parser_recovery.ts +153 -153
- package/src/scaffold.ts +375 -371
- package/src/solver.ts +330 -330
- package/src/typechecker.d.ts +52 -0
- package/src/typechecker.ts +591 -700
- package/src/types.d.ts +38 -0
- package/src/types.ts +122 -122
- package/src/verifier.ts +49 -154
- package/README.md +0 -382
- package/dist/commands/check.d.ts +0 -5
- package/dist/commands/check.js +0 -34
- package/dist/commands/check.js.map +0 -1
- package/dist/commands/compile.d.ts +0 -5
- package/dist/commands/compile.js +0 -215
- package/dist/commands/compile.js.map +0 -1
- package/dist/commands/debug.d.ts +0 -5
- package/dist/commands/debug.js +0 -59
- package/dist/commands/debug.js.map +0 -1
- package/dist/commands/diff.d.ts +0 -5
- package/dist/commands/diff.js +0 -123
- package/dist/commands/diff.js.map +0 -1
- package/dist/commands/fmt.d.ts +0 -5
- package/dist/commands/fmt.js +0 -49
- package/dist/commands/fmt.js.map +0 -1
- package/dist/commands/init.d.ts +0 -5
- package/dist/commands/init.js +0 -96
- package/dist/commands/init.js.map +0 -1
- package/dist/commands/ir.d.ts +0 -5
- package/dist/commands/ir.js +0 -27
- package/dist/commands/ir.js.map +0 -1
- package/dist/commands/lex.d.ts +0 -5
- package/dist/commands/lex.js +0 -21
- package/dist/commands/lex.js.map +0 -1
- package/dist/commands/parse.d.ts +0 -5
- package/dist/commands/parse.js +0 -30
- package/dist/commands/parse.js.map +0 -1
- package/dist/commands/test.d.ts +0 -5
- package/dist/commands/test.js +0 -61
- package/dist/commands/test.js.map +0 -1
- package/dist/commands/verify_determinism.d.ts +0 -5
- package/dist/commands/verify_determinism.js +0 -64
- package/dist/commands/verify_determinism.js.map +0 -1
- package/dist/commands/watch.d.ts +0 -5
- package/dist/commands/watch.js +0 -50
- package/dist/commands/watch.js.map +0 -1
- package/dist/emit_auth.d.ts +0 -18
- package/dist/emit_auth.js +0 -507
- package/dist/emit_auth.js.map +0 -1
- package/dist/emit_database.d.ts +0 -7
- package/dist/emit_database.js +0 -72
- package/dist/emit_database.js.map +0 -1
- package/dist/emit_index.d.ts +0 -6
- package/dist/emit_index.js +0 -202
- package/dist/emit_index.js.map +0 -1
- package/dist/emit_models.d.ts +0 -12
- package/dist/emit_models.js +0 -171
- package/dist/emit_models.js.map +0 -1
- package/dist/emit_openapi.d.ts +0 -9
- package/dist/emit_openapi.js +0 -306
- package/dist/emit_openapi.js.map +0 -1
- package/dist/emit_package.d.ts +0 -7
- package/dist/emit_package.js +0 -68
- package/dist/emit_package.js.map +0 -1
- package/dist/emit_router.d.ts +0 -12
- package/dist/emit_router.js +0 -389
- package/dist/emit_router.js.map +0 -1
- package/dist/lowering_channels.d.ts +0 -11
- package/dist/lowering_channels.js +0 -103
- package/dist/lowering_channels.js.map +0 -1
- package/dist/lowering_entities.d.ts +0 -11
- package/dist/lowering_entities.js +0 -232
- package/dist/lowering_entities.js.map +0 -1
- package/dist/lowering_helpers.d.ts +0 -13
- package/dist/lowering_helpers.js +0 -76
- package/dist/lowering_helpers.js.map +0 -1
- package/src/commands/check.ts +0 -33
- package/src/commands/compile.ts +0 -191
- package/src/commands/debug.ts +0 -33
- package/src/commands/diff.ts +0 -105
- package/src/commands/fmt.ts +0 -22
- package/src/commands/init.ts +0 -72
- package/src/commands/ir.ts +0 -23
- package/src/commands/lex.ts +0 -17
- package/src/commands/parse.ts +0 -24
- package/src/commands/test.ts +0 -36
- package/src/commands/verify_determinism.ts +0 -66
- package/src/commands/watch.ts +0 -25
- package/src/emit_auth.ts +0 -513
- package/src/emit_database.ts +0 -72
- package/src/emit_index.ts +0 -210
- package/src/emit_models.ts +0 -176
- package/src/emit_openapi.ts +0 -315
- package/src/emit_package.ts +0 -66
- package/src/emit_router.ts +0 -408
- package/src/lowering_channels.ts +0 -108
- package/src/lowering_entities.ts +0 -258
- package/src/lowering_helpers.ts +0 -75
package/src/solver.ts
CHANGED
|
@@ -1,330 +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: "
|
|
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
|
-
}
|
|
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
|
+
}
|