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.
Files changed (71) hide show
  1. package/dist/commands/compile.js +42 -10
  2. package/dist/commands/compile.js.map +1 -1
  3. package/dist/commands/init.d.ts +1 -1
  4. package/dist/commands/init.js +29 -2
  5. package/dist/commands/init.js.map +1 -1
  6. package/dist/emit_auth.d.ts +14 -2
  7. package/dist/emit_auth.js +498 -60
  8. package/dist/emit_auth.js.map +1 -1
  9. package/dist/emit_capability.js +61 -7
  10. package/dist/emit_capability.js.map +1 -1
  11. package/dist/emit_composition.js +37 -3
  12. package/dist/emit_composition.js.map +1 -1
  13. package/dist/emit_events.d.ts +1 -0
  14. package/dist/emit_events.js +68 -1
  15. package/dist/emit_events.js.map +1 -1
  16. package/dist/emit_full.js +166 -11
  17. package/dist/emit_full.js.map +1 -1
  18. package/dist/emit_index.js +46 -1
  19. package/dist/emit_index.js.map +1 -1
  20. package/dist/emit_models.d.ts +12 -0
  21. package/dist/emit_models.js +171 -0
  22. package/dist/emit_models.js.map +1 -0
  23. package/dist/emit_openapi.d.ts +9 -0
  24. package/dist/emit_openapi.js +308 -0
  25. package/dist/emit_openapi.js.map +1 -0
  26. package/dist/emit_router.js +19 -4
  27. package/dist/emit_router.js.map +1 -1
  28. package/dist/emit_tests.js +37 -0
  29. package/dist/emit_tests.js.map +1 -1
  30. package/dist/emitter.js +81 -5
  31. package/dist/emitter.js.map +1 -1
  32. package/dist/ir.d.ts +4 -0
  33. package/dist/lowering.js +16 -1
  34. package/dist/lowering.js.map +1 -1
  35. package/dist/lowering_channels.d.ts +1 -1
  36. package/dist/lowering_channels.js +3 -2
  37. package/dist/lowering_channels.js.map +1 -1
  38. package/dist/lowering_entities.js +11 -1
  39. package/dist/lowering_entities.js.map +1 -1
  40. package/dist/optimizer.js +1 -1
  41. package/dist/optimizer.js.map +1 -1
  42. package/dist/scaffold.js +0 -1
  43. package/dist/scaffold.js.map +1 -1
  44. package/dist/typechecker.d.ts +5 -0
  45. package/dist/typechecker.js +68 -13
  46. package/dist/typechecker.js.map +1 -1
  47. package/dist/verifier.d.ts +5 -0
  48. package/dist/verifier.js +140 -2
  49. package/dist/verifier.js.map +1 -1
  50. package/package.json +1 -1
  51. package/src/commands/compile.ts +41 -10
  52. package/src/commands/init.ts +28 -2
  53. package/src/emit_auth.ts +513 -67
  54. package/src/emit_capability.ts +61 -6
  55. package/src/emit_composition.ts +36 -3
  56. package/src/emit_events.ts +70 -0
  57. package/src/emit_full.ts +172 -13
  58. package/src/emit_index.ts +210 -161
  59. package/src/emit_models.ts +176 -0
  60. package/src/emit_openapi.ts +318 -0
  61. package/src/emit_router.ts +18 -4
  62. package/src/emit_tests.ts +41 -0
  63. package/src/emitter.ts +81 -5
  64. package/src/ir.ts +1 -0
  65. package/src/lowering.ts +19 -1
  66. package/src/lowering_channels.ts +3 -2
  67. package/src/lowering_entities.ts +258 -248
  68. package/src/optimizer.ts +1 -1
  69. package/src/scaffold.ts +0 -1
  70. package/src/typechecker.ts +81 -15
  71. package/src/verifier.ts +495 -348
@@ -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 { name: rel.name, kind: rel.relationType, from_entity: entity.name, to_entity: rel.target, from_table: fromTable, to_table: toTable, foreign_key: foreignKey, junction_table: junctionTable };
228
- });
229
-
230
- const relatedStore = stores.find(s => s.name.toLowerCase().includes(entity.name.toLowerCase()));
231
- const deps = relatedStore ? [makeId(systemName, "data_store", relatedStore.name)] : [];
232
-
233
- return {
234
- id: moduleId,
235
- kind: "api_service",
236
- name: `${entity.name}Service`,
237
- interfaces: [{ name: `I${entity.name}Service`, methods }],
238
- models: [model],
239
- events: [],
240
- state_machines: stateMachines,
241
- relations,
242
- dependencies: deps,
243
- config: {
244
- authenticated: entity.auth !== null && entity.auth !== "none",
245
- auth_method: entity.auth || "none",
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
@@ -163,7 +163,6 @@ const TEMPLATES: Record<ScaffoldDomain, string> = {
163
163
  }
164
164
 
165
165
  store DeviceStore {
166
- engine: dynamodb
167
166
  schema: {
168
167
  id: uuid,
169
168
  serial: string,
@@ -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
- // User-defined — return json as permissive fallback
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 ────────────────────────────────────────────────────────────