bonescript-compiler 0.3.0 → 0.5.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/dist/commands/compile.js +42 -10
- package/dist/commands/compile.js.map +1 -1
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.js +29 -2
- package/dist/commands/init.js.map +1 -1
- package/dist/emit_auth.d.ts +14 -2
- package/dist/emit_auth.js +498 -60
- package/dist/emit_auth.js.map +1 -1
- package/dist/emit_capability.js +61 -7
- package/dist/emit_capability.js.map +1 -1
- package/dist/emit_composition.js +37 -3
- package/dist/emit_composition.js.map +1 -1
- package/dist/emit_events.d.ts +1 -0
- package/dist/emit_events.js +68 -1
- package/dist/emit_events.js.map +1 -1
- package/dist/emit_full.js +166 -11
- package/dist/emit_full.js.map +1 -1
- package/dist/emit_index.js +46 -1
- package/dist/emit_index.js.map +1 -1
- package/dist/emit_models.d.ts +12 -0
- package/dist/emit_models.js +171 -0
- package/dist/emit_models.js.map +1 -0
- package/dist/emit_openapi.d.ts +9 -0
- package/dist/emit_openapi.js +308 -0
- package/dist/emit_openapi.js.map +1 -0
- package/dist/emit_router.js +19 -4
- package/dist/emit_router.js.map +1 -1
- package/dist/emit_tests.js +37 -0
- package/dist/emit_tests.js.map +1 -1
- package/dist/emitter.js +81 -5
- package/dist/emitter.js.map +1 -1
- package/dist/ir.d.ts +4 -0
- package/dist/lowering.js +16 -1
- package/dist/lowering.js.map +1 -1
- package/dist/lowering_channels.d.ts +1 -1
- package/dist/lowering_channels.js +3 -2
- package/dist/lowering_channels.js.map +1 -1
- package/dist/lowering_entities.js +11 -1
- package/dist/lowering_entities.js.map +1 -1
- package/dist/optimizer.js +1 -1
- package/dist/optimizer.js.map +1 -1
- package/dist/scaffold.js +0 -1
- package/dist/scaffold.js.map +1 -1
- package/dist/typechecker.d.ts +5 -0
- package/dist/typechecker.js +68 -13
- package/dist/typechecker.js.map +1 -1
- package/dist/verifier.d.ts +5 -0
- package/dist/verifier.js +140 -2
- package/dist/verifier.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/compile.ts +41 -10
- package/src/commands/init.ts +28 -2
- package/src/emit_auth.ts +513 -67
- package/src/emit_capability.ts +61 -6
- package/src/emit_composition.ts +36 -3
- package/src/emit_events.ts +70 -0
- package/src/emit_full.ts +172 -13
- package/src/emit_index.ts +210 -161
- package/src/emit_models.ts +176 -0
- package/src/emit_openapi.ts +318 -0
- package/src/emit_router.ts +18 -4
- package/src/emit_tests.ts +41 -0
- package/src/emitter.ts +81 -5
- package/src/ir.ts +1 -0
- package/src/lowering.ts +19 -1
- package/src/lowering_channels.ts +3 -2
- package/src/lowering_entities.ts +258 -248
- package/src/optimizer.ts +1 -1
- package/src/scaffold.ts +0 -1
- package/src/typechecker.ts +81 -15
- package/src/verifier.ts +495 -348
package/src/lowering_entities.ts
CHANGED
|
@@ -1,248 +1,258 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* BoneScript Entity Lowering
|
|
3
|
-
* Converts EntityDecl + CapabilityDecl AST nodes into IR api_service modules.
|
|
4
|
-
* Also handles field lowering and CRUD method generation.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import * as AST from "./ast";
|
|
8
|
-
import * as IR from "./ir";
|
|
9
|
-
import { makeId, parseDurationMs, serializeType, serializeExpr, toSnakeCase } from "./lowering_helpers";
|
|
10
|
-
|
|
11
|
-
// ─── Field Lowering ───────────────────────────────────────────────────────────
|
|
12
|
-
|
|
13
|
-
export function lowerField(f: AST.FieldNode): IR.IRField {
|
|
14
|
-
const type = serializeType(f.type);
|
|
15
|
-
// defaultValue is an ExprNode | null — serialize it to a string if present
|
|
16
|
-
const defaultValue = f.defaultValue ? serializeExpr(f.defaultValue) : null;
|
|
17
|
-
return {
|
|
18
|
-
name: f.name,
|
|
19
|
-
type,
|
|
20
|
-
nullable: false,
|
|
21
|
-
unique: false,
|
|
22
|
-
indexed: false,
|
|
23
|
-
default_value: defaultValue,
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// ─── CRUD Method Generation ───────────────────────────────────────────────────
|
|
28
|
-
|
|
29
|
-
export function makeCrudMethod(op: string, entityName: string, fields: IR.IRField[]): IR.IRMethod {
|
|
30
|
-
const input: IR.IRField[] =
|
|
31
|
-
op === "create" || op === "update"
|
|
32
|
-
? fields.filter(f => f.name !== "id" && f.name !== "created_at" && f.name !== "updated_at")
|
|
33
|
-
: op === "list"
|
|
34
|
-
? [
|
|
35
|
-
{ name: "page", type: "uint", nullable: true, unique: false, indexed: false, default_value: "1" },
|
|
36
|
-
{ name: "page_size", type: "uint", nullable: true, unique: false, indexed: false, default_value: "50" },
|
|
37
|
-
]
|
|
38
|
-
: [{ name: "id", type: "uuid", nullable: false, unique: true, indexed: true, default_value: null }];
|
|
39
|
-
|
|
40
|
-
return {
|
|
41
|
-
name: op,
|
|
42
|
-
input,
|
|
43
|
-
output: op === "list" ? `list<${entityName}>` : op === "delete" ? "bool" : entityName,
|
|
44
|
-
preconditions: [],
|
|
45
|
-
effects: [],
|
|
46
|
-
emissions: [],
|
|
47
|
-
idempotent: op === "read" || op === "list",
|
|
48
|
-
authenticated: true,
|
|
49
|
-
timeout_ms: 30000,
|
|
50
|
-
retry: null,
|
|
51
|
-
pipeline: null,
|
|
52
|
-
algorithm: null,
|
|
53
|
-
sync: null,
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// ─── Capability Lowering ──────────────────────────────────────────────────────
|
|
58
|
-
|
|
59
|
-
export function lowerCapability(cap: AST.CapabilityDeclNode): IR.IRMethod {
|
|
60
|
-
const input: IR.IRField[] = cap.params.map(p => ({
|
|
61
|
-
name: p.name,
|
|
62
|
-
type: serializeType(p.type),
|
|
63
|
-
nullable: false,
|
|
64
|
-
unique: false,
|
|
65
|
-
indexed: false,
|
|
66
|
-
default_value: null,
|
|
67
|
-
}));
|
|
68
|
-
|
|
69
|
-
const preconditions: IR.IRPrecondition[] = cap.requires.map(r => ({
|
|
70
|
-
expression: serializeExpr(r),
|
|
71
|
-
description: serializeExpr(r),
|
|
72
|
-
}));
|
|
73
|
-
|
|
74
|
-
const effects: IR.IREffect[] = cap.effects.map(e => ({
|
|
75
|
-
target: e.target.path.join("."),
|
|
76
|
-
op: e.op === "=" ? "assign" as const : e.op === "+=" ? "add" as const : "remove" as const,
|
|
77
|
-
value: serializeExpr(e.value),
|
|
78
|
-
}));
|
|
79
|
-
|
|
80
|
-
let pipeline: IR.IRPipeline | null = null;
|
|
81
|
-
if (cap.pipeline) {
|
|
82
|
-
pipeline = {
|
|
83
|
-
parallel: cap.pipeline.parallel,
|
|
84
|
-
steps: cap.pipeline.steps.map(step => ({
|
|
85
|
-
call_name: step.call.name,
|
|
86
|
-
call_args: step.call.args.map(a => serializeExpr(a)),
|
|
87
|
-
bind_as: step.bindAs,
|
|
88
|
-
})),
|
|
89
|
-
on_error: cap.pipeline.onError ? {
|
|
90
|
-
action: cap.pipeline.onError.action,
|
|
91
|
-
call_name: cap.pipeline.onError.call?.name || null,
|
|
92
|
-
call_args: cap.pipeline.onError.call?.args.map(a => serializeExpr(a)) || [],
|
|
93
|
-
} : null,
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
let algorithm: IR.IRAlgorithm | null = null;
|
|
98
|
-
if (cap.algorithm) {
|
|
99
|
-
algorithm = {
|
|
100
|
-
catalog_name: cap.algorithm.name,
|
|
101
|
-
bindings: cap.algorithm.using.map(b => ({
|
|
102
|
-
param: b.param,
|
|
103
|
-
value: serializeExpr(b.value),
|
|
104
|
-
})),
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
return {
|
|
109
|
-
name: cap.name,
|
|
110
|
-
input,
|
|
111
|
-
output: cap.returns ? serializeType(cap.returns) : "result<void, error>",
|
|
112
|
-
preconditions,
|
|
113
|
-
effects,
|
|
114
|
-
emissions: cap.emits.map(e => e.eventName),
|
|
115
|
-
idempotent: cap.idempotent || false,
|
|
116
|
-
authenticated: true,
|
|
117
|
-
timeout_ms: parseDurationMs(cap.timeout) || 30000,
|
|
118
|
-
retry: cap.retry ? {
|
|
119
|
-
max_attempts: cap.retry.maxAttempts || 3,
|
|
120
|
-
backoff: (cap.retry.backoff as IR.IRRetryPolicy["backoff"]) || "exponential",
|
|
121
|
-
interval_ms: parseDurationMs(cap.retry.interval) || 1000,
|
|
122
|
-
} : null,
|
|
123
|
-
pipeline,
|
|
124
|
-
algorithm,
|
|
125
|
-
sync: cap.sync,
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// ─── Entity Lowering ──────────────────────────────────────────────────────────
|
|
130
|
-
|
|
131
|
-
export function lowerEntity(
|
|
132
|
-
systemName: string,
|
|
133
|
-
entity: AST.EntityDeclNode,
|
|
134
|
-
capabilities: AST.CapabilityDeclNode[],
|
|
135
|
-
stores: AST.StoreDeclNode[],
|
|
136
|
-
): IR.IRModule {
|
|
137
|
-
const moduleId = makeId(systemName, "api_service", `${entity.name}Service`);
|
|
138
|
-
|
|
139
|
-
// Ontology-entailed fields + declared fields
|
|
140
|
-
const fields: IR.IRField[] = [
|
|
141
|
-
{ name: "id", type: "uuid", nullable: false, unique: true, indexed: true, default_value: "gen_random_uuid()" },
|
|
142
|
-
{ name: "created_at", type: "timestamp", nullable: false, unique: false, indexed: true, default_value: "now()" },
|
|
143
|
-
{ name: "updated_at", type: "timestamp", nullable: false, unique: false, indexed: false, default_value: "now()" },
|
|
144
|
-
...entity.owns.map(lowerField),
|
|
145
|
-
];
|
|
146
|
-
|
|
147
|
-
const derivedFields: IR.IRField[] = entity.derived.map(d => ({
|
|
148
|
-
name: d.name,
|
|
149
|
-
type: "json",
|
|
150
|
-
nullable: true,
|
|
151
|
-
unique: false,
|
|
152
|
-
indexed: false,
|
|
153
|
-
default_value: `GENERATED ALWAYS AS (${serializeExpr(d.expr)}) STORED`,
|
|
154
|
-
}));
|
|
155
|
-
|
|
156
|
-
const indexes: IR.IRIndex[] = entity.indexes.map(idx => ({ fields: idx, unique: false }));
|
|
157
|
-
|
|
158
|
-
const modelConstraints: IR.IRModelConstraint[] = [];
|
|
159
|
-
for (const c of entity.constraints) {
|
|
160
|
-
const serialized = serializeExpr(c);
|
|
161
|
-
if (c.kind === "FieldRef" && c.path[c.path.length - 1] === "unique") {
|
|
162
|
-
const field = c.path.slice(0, -1).join(".");
|
|
163
|
-
modelConstraints.push({ kind: "unique", target: field, params: {} });
|
|
164
|
-
indexes.push({ fields: [field], unique: true });
|
|
165
|
-
} else {
|
|
166
|
-
modelConstraints.push({ kind: "check", target: entity.name, params: { expression: serialized } });
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const model: IR.IRModel = {
|
|
171
|
-
name: entity.name,
|
|
172
|
-
fields: [...fields, ...derivedFields],
|
|
173
|
-
primary_key: "id",
|
|
174
|
-
indexes,
|
|
175
|
-
constraints: modelConstraints,
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
// State machine
|
|
179
|
-
const stateMachines: IR.IRStateMachine[] = [];
|
|
180
|
-
if (entity.states) {
|
|
181
|
-
const states = entity.states.nodes.map(n => n.name);
|
|
182
|
-
const transitions: IR.IRTransition[] = [];
|
|
183
|
-
for (const node of entity.states.nodes) {
|
|
184
|
-
for (const target of node.transitions) {
|
|
185
|
-
transitions.push({ from: node.name, to: target, trigger: `${node.name}_to_${target}`, guard: null });
|
|
186
|
-
}
|
|
187
|
-
for (const target of node.branches) {
|
|
188
|
-
transitions.push({ from: node.name, to: target, trigger: `${node.name}_to_${target}`, guard: null });
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
stateMachines.push({ entity: entity.name, states, initial: states[0], transitions });
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// Methods: CRUD + capabilities
|
|
195
|
-
const methods: IR.IRMethod[] = [
|
|
196
|
-
makeCrudMethod("create", entity.name, fields),
|
|
197
|
-
makeCrudMethod("read", entity.name, fields),
|
|
198
|
-
makeCrudMethod("update", entity.name, fields),
|
|
199
|
-
makeCrudMethod("delete", entity.name, fields),
|
|
200
|
-
makeCrudMethod("list", entity.name, fields),
|
|
201
|
-
...capabilities.map(lowerCapability),
|
|
202
|
-
];
|
|
203
|
-
|
|
204
|
-
// Relations
|
|
205
|
-
const relations: IR.IRRelation[] = entity.relations.map(rel => {
|
|
206
|
-
const fromTable = toSnakeCase(entity.name) + "s";
|
|
207
|
-
const toTable = toSnakeCase(rel.target) + "s";
|
|
208
|
-
let foreignKey: string;
|
|
209
|
-
let junctionTable: string | undefined;
|
|
210
|
-
|
|
211
|
-
switch (rel.relationType) {
|
|
212
|
-
case "belongs_to":
|
|
213
|
-
foreignKey = toSnakeCase(rel.target) + "_id";
|
|
214
|
-
break;
|
|
215
|
-
case "has_one":
|
|
216
|
-
case "has_many":
|
|
217
|
-
foreignKey = toSnakeCase(entity.name) + "_id";
|
|
218
|
-
break;
|
|
219
|
-
case "many_to_many":
|
|
220
|
-
foreignKey = toSnakeCase(entity.name) + "_id";
|
|
221
|
-
junctionTable = [fromTable, toTable].sort().join("_");
|
|
222
|
-
break;
|
|
223
|
-
default:
|
|
224
|
-
foreignKey = toSnakeCase(rel.target) + "_id";
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
return {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
|
|
1
|
+
/**
|
|
2
|
+
* BoneScript Entity Lowering
|
|
3
|
+
* Converts EntityDecl + CapabilityDecl AST nodes into IR api_service modules.
|
|
4
|
+
* Also handles field lowering and CRUD method generation.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as AST from "./ast";
|
|
8
|
+
import * as IR from "./ir";
|
|
9
|
+
import { makeId, parseDurationMs, serializeType, serializeExpr, toSnakeCase } from "./lowering_helpers";
|
|
10
|
+
|
|
11
|
+
// ─── Field Lowering ───────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
export function lowerField(f: AST.FieldNode): IR.IRField {
|
|
14
|
+
const type = serializeType(f.type);
|
|
15
|
+
// defaultValue is an ExprNode | null — serialize it to a string if present
|
|
16
|
+
const defaultValue = f.defaultValue ? serializeExpr(f.defaultValue) : null;
|
|
17
|
+
return {
|
|
18
|
+
name: f.name,
|
|
19
|
+
type,
|
|
20
|
+
nullable: false,
|
|
21
|
+
unique: false,
|
|
22
|
+
indexed: false,
|
|
23
|
+
default_value: defaultValue,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ─── CRUD Method Generation ───────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
export function makeCrudMethod(op: string, entityName: string, fields: IR.IRField[]): IR.IRMethod {
|
|
30
|
+
const input: IR.IRField[] =
|
|
31
|
+
op === "create" || op === "update"
|
|
32
|
+
? fields.filter(f => f.name !== "id" && f.name !== "created_at" && f.name !== "updated_at")
|
|
33
|
+
: op === "list"
|
|
34
|
+
? [
|
|
35
|
+
{ name: "page", type: "uint", nullable: true, unique: false, indexed: false, default_value: "1" },
|
|
36
|
+
{ name: "page_size", type: "uint", nullable: true, unique: false, indexed: false, default_value: "50" },
|
|
37
|
+
]
|
|
38
|
+
: [{ name: "id", type: "uuid", nullable: false, unique: true, indexed: true, default_value: null }];
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
name: op,
|
|
42
|
+
input,
|
|
43
|
+
output: op === "list" ? `list<${entityName}>` : op === "delete" ? "bool" : entityName,
|
|
44
|
+
preconditions: [],
|
|
45
|
+
effects: [],
|
|
46
|
+
emissions: [],
|
|
47
|
+
idempotent: op === "read" || op === "list",
|
|
48
|
+
authenticated: true,
|
|
49
|
+
timeout_ms: 30000,
|
|
50
|
+
retry: null,
|
|
51
|
+
pipeline: null,
|
|
52
|
+
algorithm: null,
|
|
53
|
+
sync: null,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ─── Capability Lowering ──────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
export function lowerCapability(cap: AST.CapabilityDeclNode): IR.IRMethod {
|
|
60
|
+
const input: IR.IRField[] = cap.params.map(p => ({
|
|
61
|
+
name: p.name,
|
|
62
|
+
type: serializeType(p.type),
|
|
63
|
+
nullable: false,
|
|
64
|
+
unique: false,
|
|
65
|
+
indexed: false,
|
|
66
|
+
default_value: null,
|
|
67
|
+
}));
|
|
68
|
+
|
|
69
|
+
const preconditions: IR.IRPrecondition[] = cap.requires.map(r => ({
|
|
70
|
+
expression: serializeExpr(r),
|
|
71
|
+
description: serializeExpr(r),
|
|
72
|
+
}));
|
|
73
|
+
|
|
74
|
+
const effects: IR.IREffect[] = cap.effects.map(e => ({
|
|
75
|
+
target: e.target.path.join("."),
|
|
76
|
+
op: e.op === "=" ? "assign" as const : e.op === "+=" ? "add" as const : "remove" as const,
|
|
77
|
+
value: serializeExpr(e.value),
|
|
78
|
+
}));
|
|
79
|
+
|
|
80
|
+
let pipeline: IR.IRPipeline | null = null;
|
|
81
|
+
if (cap.pipeline) {
|
|
82
|
+
pipeline = {
|
|
83
|
+
parallel: cap.pipeline.parallel,
|
|
84
|
+
steps: cap.pipeline.steps.map(step => ({
|
|
85
|
+
call_name: step.call.name,
|
|
86
|
+
call_args: step.call.args.map(a => serializeExpr(a)),
|
|
87
|
+
bind_as: step.bindAs,
|
|
88
|
+
})),
|
|
89
|
+
on_error: cap.pipeline.onError ? {
|
|
90
|
+
action: cap.pipeline.onError.action,
|
|
91
|
+
call_name: cap.pipeline.onError.call?.name || null,
|
|
92
|
+
call_args: cap.pipeline.onError.call?.args.map(a => serializeExpr(a)) || [],
|
|
93
|
+
} : null,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let algorithm: IR.IRAlgorithm | null = null;
|
|
98
|
+
if (cap.algorithm) {
|
|
99
|
+
algorithm = {
|
|
100
|
+
catalog_name: cap.algorithm.name,
|
|
101
|
+
bindings: cap.algorithm.using.map(b => ({
|
|
102
|
+
param: b.param,
|
|
103
|
+
value: serializeExpr(b.value),
|
|
104
|
+
})),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
name: cap.name,
|
|
110
|
+
input,
|
|
111
|
+
output: cap.returns ? serializeType(cap.returns) : "result<void, error>",
|
|
112
|
+
preconditions,
|
|
113
|
+
effects,
|
|
114
|
+
emissions: cap.emits.map(e => e.eventName),
|
|
115
|
+
idempotent: cap.idempotent || false,
|
|
116
|
+
authenticated: true,
|
|
117
|
+
timeout_ms: parseDurationMs(cap.timeout) || 30000,
|
|
118
|
+
retry: cap.retry ? {
|
|
119
|
+
max_attempts: cap.retry.maxAttempts || 3,
|
|
120
|
+
backoff: (cap.retry.backoff as IR.IRRetryPolicy["backoff"]) || "exponential",
|
|
121
|
+
interval_ms: parseDurationMs(cap.retry.interval) || 1000,
|
|
122
|
+
} : null,
|
|
123
|
+
pipeline,
|
|
124
|
+
algorithm,
|
|
125
|
+
sync: cap.sync,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ─── Entity Lowering ──────────────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
export function lowerEntity(
|
|
132
|
+
systemName: string,
|
|
133
|
+
entity: AST.EntityDeclNode,
|
|
134
|
+
capabilities: AST.CapabilityDeclNode[],
|
|
135
|
+
stores: AST.StoreDeclNode[],
|
|
136
|
+
): IR.IRModule {
|
|
137
|
+
const moduleId = makeId(systemName, "api_service", `${entity.name}Service`);
|
|
138
|
+
|
|
139
|
+
// Ontology-entailed fields + declared fields
|
|
140
|
+
const fields: IR.IRField[] = [
|
|
141
|
+
{ name: "id", type: "uuid", nullable: false, unique: true, indexed: true, default_value: "gen_random_uuid()" },
|
|
142
|
+
{ name: "created_at", type: "timestamp", nullable: false, unique: false, indexed: true, default_value: "now()" },
|
|
143
|
+
{ name: "updated_at", type: "timestamp", nullable: false, unique: false, indexed: false, default_value: "now()" },
|
|
144
|
+
...entity.owns.map(lowerField),
|
|
145
|
+
];
|
|
146
|
+
|
|
147
|
+
const derivedFields: IR.IRField[] = entity.derived.map(d => ({
|
|
148
|
+
name: d.name,
|
|
149
|
+
type: "json",
|
|
150
|
+
nullable: true,
|
|
151
|
+
unique: false,
|
|
152
|
+
indexed: false,
|
|
153
|
+
default_value: `GENERATED ALWAYS AS (${serializeExpr(d.expr)}) STORED`,
|
|
154
|
+
}));
|
|
155
|
+
|
|
156
|
+
const indexes: IR.IRIndex[] = entity.indexes.map(idx => ({ fields: idx, unique: false }));
|
|
157
|
+
|
|
158
|
+
const modelConstraints: IR.IRModelConstraint[] = [];
|
|
159
|
+
for (const c of entity.constraints) {
|
|
160
|
+
const serialized = serializeExpr(c);
|
|
161
|
+
if (c.kind === "FieldRef" && c.path[c.path.length - 1] === "unique") {
|
|
162
|
+
const field = c.path.slice(0, -1).join(".");
|
|
163
|
+
modelConstraints.push({ kind: "unique", target: field, params: {} });
|
|
164
|
+
indexes.push({ fields: [field], unique: true });
|
|
165
|
+
} else {
|
|
166
|
+
modelConstraints.push({ kind: "check", target: entity.name, params: { expression: serialized } });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const model: IR.IRModel = {
|
|
171
|
+
name: entity.name,
|
|
172
|
+
fields: [...fields, ...derivedFields],
|
|
173
|
+
primary_key: "id",
|
|
174
|
+
indexes,
|
|
175
|
+
constraints: modelConstraints,
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// State machine
|
|
179
|
+
const stateMachines: IR.IRStateMachine[] = [];
|
|
180
|
+
if (entity.states) {
|
|
181
|
+
const states = entity.states.nodes.map(n => n.name);
|
|
182
|
+
const transitions: IR.IRTransition[] = [];
|
|
183
|
+
for (const node of entity.states.nodes) {
|
|
184
|
+
for (const target of node.transitions) {
|
|
185
|
+
transitions.push({ from: node.name, to: target, trigger: `${node.name}_to_${target}`, guard: null });
|
|
186
|
+
}
|
|
187
|
+
for (const target of node.branches) {
|
|
188
|
+
transitions.push({ from: node.name, to: target, trigger: `${node.name}_to_${target}`, guard: null });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
stateMachines.push({ entity: entity.name, states, initial: states[0], transitions });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Methods: CRUD + capabilities
|
|
195
|
+
const methods: IR.IRMethod[] = [
|
|
196
|
+
makeCrudMethod("create", entity.name, fields),
|
|
197
|
+
makeCrudMethod("read", entity.name, fields),
|
|
198
|
+
makeCrudMethod("update", entity.name, fields),
|
|
199
|
+
makeCrudMethod("delete", entity.name, fields),
|
|
200
|
+
makeCrudMethod("list", entity.name, fields),
|
|
201
|
+
...capabilities.map(lowerCapability),
|
|
202
|
+
];
|
|
203
|
+
|
|
204
|
+
// Relations
|
|
205
|
+
const relations: IR.IRRelation[] = entity.relations.map(rel => {
|
|
206
|
+
const fromTable = toSnakeCase(entity.name) + "s";
|
|
207
|
+
const toTable = toSnakeCase(rel.target) + "s";
|
|
208
|
+
let foreignKey: string;
|
|
209
|
+
let junctionTable: string | undefined;
|
|
210
|
+
|
|
211
|
+
switch (rel.relationType) {
|
|
212
|
+
case "belongs_to":
|
|
213
|
+
foreignKey = toSnakeCase(rel.target) + "_id";
|
|
214
|
+
break;
|
|
215
|
+
case "has_one":
|
|
216
|
+
case "has_many":
|
|
217
|
+
foreignKey = toSnakeCase(entity.name) + "_id";
|
|
218
|
+
break;
|
|
219
|
+
case "many_to_many":
|
|
220
|
+
foreignKey = toSnakeCase(entity.name) + "_id";
|
|
221
|
+
junctionTable = [fromTable, toTable].sort().join("_");
|
|
222
|
+
break;
|
|
223
|
+
default:
|
|
224
|
+
foreignKey = toSnakeCase(rel.target) + "_id";
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
name: rel.name,
|
|
229
|
+
kind: rel.relationType,
|
|
230
|
+
from_entity: entity.name,
|
|
231
|
+
to_entity: rel.target,
|
|
232
|
+
from_table: fromTable,
|
|
233
|
+
to_table: toTable,
|
|
234
|
+
foreign_key: foreignKey,
|
|
235
|
+
junction_table: junctionTable,
|
|
236
|
+
cardinality: rel.cardinality ?? undefined,
|
|
237
|
+
};
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const relatedStore = stores.find(s => s.name.toLowerCase().includes(entity.name.toLowerCase()));
|
|
241
|
+
const deps = relatedStore ? [makeId(systemName, "data_store", relatedStore.name)] : [];
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
id: moduleId,
|
|
245
|
+
kind: "api_service",
|
|
246
|
+
name: `${entity.name}Service`,
|
|
247
|
+
interfaces: [{ name: `I${entity.name}Service`, methods }],
|
|
248
|
+
models: [model],
|
|
249
|
+
events: [],
|
|
250
|
+
state_machines: stateMachines,
|
|
251
|
+
relations,
|
|
252
|
+
dependencies: deps,
|
|
253
|
+
config: {
|
|
254
|
+
authenticated: entity.auth !== null && entity.auth !== "none",
|
|
255
|
+
auth_method: entity.auth || "none",
|
|
256
|
+
},
|
|
257
|
+
};
|
|
258
|
+
}
|
package/src/optimizer.ts
CHANGED
|
@@ -52,7 +52,7 @@ function deadModuleElimination(s: IR.IRSystem, log: OptimizationLog[]): IR.IRSys
|
|
|
52
52
|
|
|
53
53
|
// Seed: always-reachable kinds
|
|
54
54
|
for (const m of s.modules) {
|
|
55
|
-
if (["gateway", "frontend", "auth_service", "api_service", "realtime_service"].includes(m.kind)) {
|
|
55
|
+
if (["gateway", "frontend", "auth_service", "api_service", "realtime_service", "data_store", "event_bus", "cache"].includes(m.kind)) {
|
|
56
56
|
reachable.add(m.id);
|
|
57
57
|
}
|
|
58
58
|
}
|
package/src/scaffold.ts
CHANGED
package/src/typechecker.ts
CHANGED
|
@@ -178,6 +178,8 @@ export class TypeChecker {
|
|
|
178
178
|
case "FlowDecl": this.checkFlow(decl); break;
|
|
179
179
|
case "ConstraintDecl": this.checkConstraint(decl); break;
|
|
180
180
|
case "ExtensionPointDecl": this.checkExtensionPoint(decl); break;
|
|
181
|
+
case "StoreDecl": this.checkStore(decl); break;
|
|
182
|
+
case "PolicyDecl": this.checkPolicy(decl); break;
|
|
181
183
|
}
|
|
182
184
|
}
|
|
183
185
|
|
|
@@ -336,24 +338,26 @@ export class TypeChecker {
|
|
|
336
338
|
// ─── Flow Checking ─────────────────────────────────────────────────────────
|
|
337
339
|
|
|
338
340
|
private checkFlow(decl: AST.FlowDeclNode) {
|
|
339
|
-
for (const step of decl.steps) {
|
|
340
|
-
// Check step action references a valid capability or function
|
|
341
|
-
if (!this.symbols.capabilities.has(step.action.name) &&
|
|
342
|
-
!this.symbols.entities.has(step.action.name)) {
|
|
343
|
-
// Allow — could be a helper function not yet declared
|
|
344
|
-
// In strict mode this would be T012
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
// Check compensation exists if step has one
|
|
348
|
-
if (step.compensate) {
|
|
349
|
-
// Same check — compensation should reference a valid capability
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
341
|
// Check at least 2 steps (ontology requirement)
|
|
354
342
|
if (decl.steps.length < 2) {
|
|
355
343
|
this.addError("T012", `Flow '${decl.name}' must have at least 2 steps`, decl.loc);
|
|
356
344
|
}
|
|
345
|
+
|
|
346
|
+
for (const step of decl.steps) {
|
|
347
|
+
// Flow steps may call external service endpoints (not just local capabilities).
|
|
348
|
+
// Only error if the name collides with a declared entity name, which would be
|
|
349
|
+
// a definite mistake. Undeclared names are treated as external HTTP calls.
|
|
350
|
+
if (this.symbols.entities.has(step.action.name)) {
|
|
351
|
+
this.addError("T013",
|
|
352
|
+
`Flow '${decl.name}' step '${step.name}' uses entity name '${step.action.name}' as a call — did you mean a capability?`,
|
|
353
|
+
decl.loc);
|
|
354
|
+
}
|
|
355
|
+
if (step.compensate && this.symbols.entities.has(step.compensate.name)) {
|
|
356
|
+
this.addError("T013",
|
|
357
|
+
`Flow '${decl.name}' step '${step.name}' compensation uses entity name '${step.compensate.name}' as a call — did you mean a capability?`,
|
|
358
|
+
decl.loc);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
357
361
|
}
|
|
358
362
|
|
|
359
363
|
// ─── Constraint Checking ───────────────────────────────────────────────────
|
|
@@ -496,8 +500,21 @@ export class TypeChecker {
|
|
|
496
500
|
if (expr.name === "now") return prim("timestamp");
|
|
497
501
|
if (expr.name === "count") return prim("uint");
|
|
498
502
|
if (expr.name === "sum") return prim("uint");
|
|
503
|
+
if (expr.name === "min" || expr.name === "max") return prim("uint");
|
|
504
|
+
if (expr.name === "abs") return prim("uint");
|
|
505
|
+
if (expr.name === "floor" || expr.name === "ceil" || expr.name === "round") return prim("int");
|
|
506
|
+
if (expr.name === "len" || expr.name === "size") return prim("uint");
|
|
507
|
+
if (expr.name === "contains" || expr.name === "starts_with" || expr.name === "ends_with") return prim("bool");
|
|
508
|
+
if (expr.name === "to_string") return prim("string");
|
|
509
|
+
if (expr.name === "to_int" || expr.name === "to_uint") return prim("int");
|
|
510
|
+
if (expr.name === "to_float") return prim("float");
|
|
511
|
+
|
|
512
|
+
// Check if it's a declared capability — use json as safe fallback for its return
|
|
513
|
+
if (this.symbols.capabilities.has(expr.name)) {
|
|
514
|
+
return prim("json");
|
|
515
|
+
}
|
|
499
516
|
|
|
500
|
-
//
|
|
517
|
+
// Unknown user-defined function — permissive fallback
|
|
501
518
|
return prim("json");
|
|
502
519
|
}
|
|
503
520
|
|
|
@@ -569,6 +586,55 @@ export class TypeChecker {
|
|
|
569
586
|
}
|
|
570
587
|
}
|
|
571
588
|
|
|
589
|
+
// ─── Store Checking ───────────────────────────────────────────────────────────
|
|
590
|
+
|
|
591
|
+
/** Supported database engines. Engines not in this set produce T014. */
|
|
592
|
+
private static readonly SUPPORTED_ENGINES = new Set(["postgresql", "redis"]);
|
|
593
|
+
|
|
594
|
+
private checkStore(decl: AST.StoreDeclNode): void {
|
|
595
|
+
if (decl.engine && !TypeChecker.SUPPORTED_ENGINES.has(decl.engine)) {
|
|
596
|
+
this.addError(
|
|
597
|
+
"T014",
|
|
598
|
+
`Store '${decl.name}' uses unsupported engine '${decl.engine}'. ` +
|
|
599
|
+
`Supported engines: ${[...TypeChecker.SUPPORTED_ENGINES].join(", ")}. ` +
|
|
600
|
+
`Other engines (dynamodb, mongodb, sqlite, s3) are not yet implemented — ` +
|
|
601
|
+
`remove the engine declaration to use the domain default (postgresql), ` +
|
|
602
|
+
`or implement a custom emitter.`,
|
|
603
|
+
decl.loc,
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Check schema field types resolve
|
|
608
|
+
for (const field of decl.schema) {
|
|
609
|
+
const resolved = this.resolveTypeExpr(field.type);
|
|
610
|
+
if (!resolved) {
|
|
611
|
+
this.addError("T001", `Store '${decl.name}' field '${field.name}' references undefined type`, field.loc);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// ─── Policy Checking ──────────────────────────────────────────────────────────
|
|
617
|
+
|
|
618
|
+
private static readonly VALID_ENCRYPTION = new Set(["at_rest", "in_transit", "both", "none"]);
|
|
619
|
+
|
|
620
|
+
private checkPolicy(decl: AST.PolicyDeclNode): void {
|
|
621
|
+
if (decl.encryption && !TypeChecker.VALID_ENCRYPTION.has(decl.encryption)) {
|
|
622
|
+
this.addError(
|
|
623
|
+
"T015",
|
|
624
|
+
`Policy '${decl.name}' has invalid encryption value '${decl.encryption}'. ` +
|
|
625
|
+
`Valid values: ${[...TypeChecker.VALID_ENCRYPTION].join(", ")}.`,
|
|
626
|
+
decl.loc,
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
if (decl.rateLimit && decl.rateLimit.count <= 0) {
|
|
630
|
+
this.addError(
|
|
631
|
+
"T015",
|
|
632
|
+
`Policy '${decl.name}' rate_limit count must be positive (> 0), got ${decl.rateLimit.count}.`,
|
|
633
|
+
decl.loc,
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
572
638
|
|
|
573
639
|
}
|
|
574
640
|
// ─── Type Context ────────────────────────────────────────────────────────────
|