bonescript-compiler 0.2.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/dist/algorithm_catalog.js +166 -166
- package/dist/cli.d.ts +2 -1
- package/dist/cli.js +75 -543
- package/dist/cli.js.map +1 -1
- package/dist/commands/check.d.ts +5 -0
- package/dist/commands/check.js +34 -0
- package/dist/commands/check.js.map +1 -0
- package/dist/commands/compile.d.ts +5 -0
- package/dist/commands/compile.js +215 -0
- package/dist/commands/compile.js.map +1 -0
- package/dist/commands/debug.d.ts +5 -0
- package/dist/commands/debug.js +59 -0
- package/dist/commands/debug.js.map +1 -0
- package/dist/commands/diff.d.ts +5 -0
- package/dist/commands/diff.js +125 -0
- package/dist/commands/diff.js.map +1 -0
- package/dist/commands/fmt.d.ts +5 -0
- package/dist/commands/fmt.js +49 -0
- package/dist/commands/fmt.js.map +1 -0
- package/dist/commands/init.d.ts +5 -0
- package/dist/commands/init.js +96 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/ir.d.ts +5 -0
- package/dist/commands/ir.js +27 -0
- package/dist/commands/ir.js.map +1 -0
- package/dist/commands/lex.d.ts +5 -0
- package/dist/commands/lex.js +21 -0
- package/dist/commands/lex.js.map +1 -0
- package/dist/commands/parse.d.ts +5 -0
- package/dist/commands/parse.js +30 -0
- package/dist/commands/parse.js.map +1 -0
- package/dist/commands/test.d.ts +5 -0
- package/dist/commands/test.js +61 -0
- package/dist/commands/test.js.map +1 -0
- package/dist/commands/verify_determinism.d.ts +5 -0
- package/dist/commands/verify_determinism.js +64 -0
- package/dist/commands/verify_determinism.js.map +1 -0
- package/dist/commands/watch.d.ts +5 -0
- package/dist/commands/watch.js +50 -0
- package/dist/commands/watch.js.map +1 -0
- package/dist/emit_auth.d.ts +6 -0
- package/dist/emit_auth.js +69 -0
- package/dist/emit_auth.js.map +1 -0
- package/dist/emit_capability.d.ts +13 -0
- package/dist/emit_capability.js +292 -128
- 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_database.d.ts +7 -0
- package/dist/emit_database.js +74 -0
- package/dist/emit_database.js.map +1 -0
- package/dist/emit_deploy.js +162 -162
- package/dist/emit_events.d.ts +1 -0
- package/dist/emit_events.js +342 -275
- package/dist/emit_events.js.map +1 -1
- package/dist/emit_full.js +135 -95
- package/dist/emit_full.js.map +1 -1
- package/dist/emit_index.d.ts +6 -0
- package/dist/emit_index.js +157 -0
- package/dist/emit_index.js.map +1 -0
- package/dist/emit_maintenance.js +249 -249
- 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_package.d.ts +7 -0
- package/dist/emit_package.js +70 -0
- package/dist/emit_package.js.map +1 -0
- package/dist/emit_router.d.ts +12 -0
- package/dist/emit_router.js +390 -0
- package/dist/emit_router.js.map +1 -0
- package/dist/emit_runtime.d.ts +17 -11
- package/dist/emit_runtime.js +29 -686
- package/dist/emit_runtime.js.map +1 -1
- package/dist/emit_sourcemap.js +66 -66
- package/dist/emit_tests.js +37 -0
- package/dist/emit_tests.js.map +1 -1
- package/dist/emitter.js +34 -5
- package/dist/emitter.js.map +1 -1
- package/dist/extension_manager.d.ts +2 -2
- package/dist/extension_manager.js +6 -3
- package/dist/extension_manager.js.map +1 -1
- package/dist/lowering.d.ts +5 -14
- package/dist/lowering.js +47 -417
- package/dist/lowering.js.map +1 -1
- package/dist/lowering_channels.d.ts +11 -0
- package/dist/lowering_channels.js +102 -0
- package/dist/lowering_channels.js.map +1 -0
- package/dist/lowering_entities.d.ts +11 -0
- package/dist/lowering_entities.js +222 -0
- package/dist/lowering_entities.js.map +1 -0
- package/dist/lowering_helpers.d.ts +13 -0
- package/dist/lowering_helpers.js +76 -0
- package/dist/lowering_helpers.js.map +1 -0
- package/dist/module_loader.d.ts +2 -2
- package/dist/module_loader.js +20 -23
- package/dist/module_loader.js.map +1 -1
- package/dist/scaffold.d.ts +2 -2
- package/dist/scaffold.js +316 -319
- package/dist/scaffold.js.map +1 -1
- package/dist/typechecker.js +32 -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 +62 -52
- package/src/algorithm_catalog.ts +345 -345
- package/src/ast.ts +334 -334
- package/src/cli.ts +98 -624
- package/src/commands/check.ts +33 -0
- package/src/commands/compile.ts +191 -0
- package/src/commands/debug.ts +33 -0
- package/src/commands/diff.ts +108 -0
- package/src/commands/fmt.ts +22 -0
- package/src/commands/init.ts +72 -0
- package/src/commands/ir.ts +23 -0
- package/src/commands/lex.ts +17 -0
- package/src/commands/parse.ts +24 -0
- package/src/commands/test.ts +36 -0
- package/src/commands/verify_determinism.ts +66 -0
- package/src/commands/watch.ts +25 -0
- package/src/emit_auth.ts +67 -0
- package/src/emit_batch.ts +140 -140
- package/src/emit_capability.ts +617 -436
- package/src/emit_composition.ts +229 -196
- package/src/emit_database.ts +75 -0
- package/src/emit_deploy.ts +190 -190
- package/src/emit_events.ts +377 -307
- package/src/emit_extras.ts +240 -240
- package/src/emit_full.ts +351 -309
- package/src/emit_index.ts +161 -0
- package/src/emit_maintenance.ts +459 -459
- package/src/emit_models.ts +176 -0
- package/src/emit_openapi.ts +318 -0
- package/src/emit_package.ts +69 -0
- package/src/emit_router.ts +409 -0
- package/src/emit_runtime.ts +17 -728
- package/src/emit_sourcemap.ts +140 -140
- package/src/emit_tests.ts +246 -205
- package/src/emit_websocket.ts +229 -229
- package/src/emitter.ts +31 -5
- package/src/extension_manager.ts +189 -187
- package/src/formatter.ts +297 -297
- package/src/index.ts +88 -88
- package/src/ir.ts +215 -215
- package/src/lexer.ts +630 -630
- package/src/lowering.ts +142 -556
- package/src/lowering_channels.ts +107 -0
- package/src/lowering_entities.ts +248 -0
- package/src/lowering_helpers.ts +75 -0
- package/src/module_loader.ts +112 -114
- package/src/optimizer.ts +196 -196
- package/src/parse_decls.ts +409 -409
- package/src/parse_decls2.ts +244 -244
- package/src/parse_expr.ts +197 -197
- package/src/parse_types.ts +54 -54
- package/src/parser.ts +1 -1
- package/src/parser_base.ts +57 -57
- package/src/parser_recovery.ts +153 -153
- package/src/scaffold.ts +372 -375
- package/src/solver.ts +330 -330
- package/src/typechecker.ts +30 -15
- package/src/types.ts +122 -122
- package/src/verifier.ts +151 -4
package/src/typechecker.ts
CHANGED
|
@@ -336,24 +336,26 @@ export class TypeChecker {
|
|
|
336
336
|
// ─── Flow Checking ─────────────────────────────────────────────────────────
|
|
337
337
|
|
|
338
338
|
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
339
|
// Check at least 2 steps (ontology requirement)
|
|
354
340
|
if (decl.steps.length < 2) {
|
|
355
341
|
this.addError("T012", `Flow '${decl.name}' must have at least 2 steps`, decl.loc);
|
|
356
342
|
}
|
|
343
|
+
|
|
344
|
+
for (const step of decl.steps) {
|
|
345
|
+
// Flow steps may call external service endpoints (not just local capabilities).
|
|
346
|
+
// Only error if the name collides with a declared entity name, which would be
|
|
347
|
+
// a definite mistake. Undeclared names are treated as external HTTP calls.
|
|
348
|
+
if (this.symbols.entities.has(step.action.name)) {
|
|
349
|
+
this.addError("T013",
|
|
350
|
+
`Flow '${decl.name}' step '${step.name}' uses entity name '${step.action.name}' as a call — did you mean a capability?`,
|
|
351
|
+
decl.loc);
|
|
352
|
+
}
|
|
353
|
+
if (step.compensate && this.symbols.entities.has(step.compensate.name)) {
|
|
354
|
+
this.addError("T013",
|
|
355
|
+
`Flow '${decl.name}' step '${step.name}' compensation uses entity name '${step.compensate.name}' as a call — did you mean a capability?`,
|
|
356
|
+
decl.loc);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
357
359
|
}
|
|
358
360
|
|
|
359
361
|
// ─── Constraint Checking ───────────────────────────────────────────────────
|
|
@@ -496,8 +498,21 @@ export class TypeChecker {
|
|
|
496
498
|
if (expr.name === "now") return prim("timestamp");
|
|
497
499
|
if (expr.name === "count") return prim("uint");
|
|
498
500
|
if (expr.name === "sum") return prim("uint");
|
|
501
|
+
if (expr.name === "min" || expr.name === "max") return prim("uint");
|
|
502
|
+
if (expr.name === "abs") return prim("uint");
|
|
503
|
+
if (expr.name === "floor" || expr.name === "ceil" || expr.name === "round") return prim("int");
|
|
504
|
+
if (expr.name === "len" || expr.name === "size") return prim("uint");
|
|
505
|
+
if (expr.name === "contains" || expr.name === "starts_with" || expr.name === "ends_with") return prim("bool");
|
|
506
|
+
if (expr.name === "to_string") return prim("string");
|
|
507
|
+
if (expr.name === "to_int" || expr.name === "to_uint") return prim("int");
|
|
508
|
+
if (expr.name === "to_float") return prim("float");
|
|
509
|
+
|
|
510
|
+
// Check if it's a declared capability — use json as safe fallback for its return
|
|
511
|
+
if (this.symbols.capabilities.has(expr.name)) {
|
|
512
|
+
return prim("json");
|
|
513
|
+
}
|
|
499
514
|
|
|
500
|
-
//
|
|
515
|
+
// Unknown user-defined function — permissive fallback
|
|
501
516
|
return prim("json");
|
|
502
517
|
}
|
|
503
518
|
|
package/src/types.ts
CHANGED
|
@@ -1,122 +1,122 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* BoneScript Type System — Internal type representations.
|
|
3
|
-
* Implements spec/04_TYPE_SYSTEM.md.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
export type CVType =
|
|
7
|
-
| PrimitiveType
|
|
8
|
-
| GenericType
|
|
9
|
-
| RecordType
|
|
10
|
-
| UnionType
|
|
11
|
-
| TupleType
|
|
12
|
-
| BottomType;
|
|
13
|
-
|
|
14
|
-
export interface PrimitiveType {
|
|
15
|
-
tag: "primitive";
|
|
16
|
-
name: "string" | "uint" | "int" | "float" | "bool" | "timestamp" | "uuid" | "bytes" | "json";
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface GenericType {
|
|
20
|
-
tag: "generic";
|
|
21
|
-
name: "list" | "set" | "map" | "optional" | "result";
|
|
22
|
-
args: CVType[];
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export interface RecordType {
|
|
26
|
-
tag: "record";
|
|
27
|
-
name: string; // entity name
|
|
28
|
-
fields: Map<string, CVType>;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface UnionType {
|
|
32
|
-
tag: "union";
|
|
33
|
-
members: CVType[];
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export interface TupleType {
|
|
37
|
-
tag: "tuple";
|
|
38
|
-
elements: CVType[];
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export interface BottomType {
|
|
42
|
-
tag: "bottom"; // unifies with any optional<T>
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// ─── Constructors ────────────────────────────────────────────────────────────
|
|
46
|
-
|
|
47
|
-
export function prim(name: PrimitiveType["name"]): PrimitiveType {
|
|
48
|
-
return { tag: "primitive", name };
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function generic(name: GenericType["name"], ...args: CVType[]): GenericType {
|
|
52
|
-
return { tag: "generic", name, args };
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function record(name: string, fields: Map<string, CVType>): RecordType {
|
|
56
|
-
return { tag: "record", name, fields };
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export const BOTTOM: BottomType = { tag: "bottom" };
|
|
60
|
-
|
|
61
|
-
// ─── Type Equality ───────────────────────────────────────────────────────────
|
|
62
|
-
|
|
63
|
-
export function typeEquals(a: CVType, b: CVType): boolean {
|
|
64
|
-
if (a.tag !== b.tag) {
|
|
65
|
-
// optional<T> accepts T (implicit wrapping)
|
|
66
|
-
if (a.tag === "generic" && a.name === "optional" && typeEquals(a.args[0], b)) return true;
|
|
67
|
-
if (b.tag === "generic" && b.name === "optional" && typeEquals(b.args[0], a)) return true;
|
|
68
|
-
// bottom unifies with any optional
|
|
69
|
-
if (a.tag === "bottom" && b.tag === "generic" && b.name === "optional") return true;
|
|
70
|
-
if (b.tag === "bottom" && a.tag === "generic" && a.name === "optional") return true;
|
|
71
|
-
return false;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
switch (a.tag) {
|
|
75
|
-
case "primitive":
|
|
76
|
-
return a.name === (b as PrimitiveType).name;
|
|
77
|
-
case "generic": {
|
|
78
|
-
const bg = b as GenericType;
|
|
79
|
-
return a.name === bg.name && a.args.length === bg.args.length &&
|
|
80
|
-
a.args.every((arg, i) => typeEquals(arg, bg.args[i]));
|
|
81
|
-
}
|
|
82
|
-
case "record": {
|
|
83
|
-
const br = b as RecordType;
|
|
84
|
-
return a.name === br.name; // nominal for records (entity names)
|
|
85
|
-
}
|
|
86
|
-
case "union": {
|
|
87
|
-
const bu = b as UnionType;
|
|
88
|
-
return a.members.length === bu.members.length &&
|
|
89
|
-
a.members.every((m, i) => typeEquals(m, bu.members[i]));
|
|
90
|
-
}
|
|
91
|
-
case "tuple": {
|
|
92
|
-
const bt = b as TupleType;
|
|
93
|
-
return a.elements.length === bt.elements.length &&
|
|
94
|
-
a.elements.every((e, i) => typeEquals(e, bt.elements[i]));
|
|
95
|
-
}
|
|
96
|
-
case "bottom":
|
|
97
|
-
return true;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// ─── Type Display ────────────────────────────────────────────────────────────
|
|
102
|
-
|
|
103
|
-
export function typeToString(t: CVType): string {
|
|
104
|
-
switch (t.tag) {
|
|
105
|
-
case "primitive": return t.name;
|
|
106
|
-
case "generic": return `${t.name}<${t.args.map(typeToString).join(", ")}>`;
|
|
107
|
-
case "record": return t.name;
|
|
108
|
-
case "union": return t.members.map(typeToString).join(" | ");
|
|
109
|
-
case "tuple": return `(${t.elements.map(typeToString).join(", ")})`;
|
|
110
|
-
case "bottom": return "bottom";
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// ─── Numeric Type Check ──────────────────────────────────────────────────────
|
|
115
|
-
|
|
116
|
-
export function isNumeric(t: CVType): boolean {
|
|
117
|
-
return t.tag === "primitive" && (t.name === "uint" || t.name === "int" || t.name === "float");
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export function isComparable(t: CVType): boolean {
|
|
121
|
-
return isNumeric(t) || (t.tag === "primitive" && (t.name === "string" || t.name === "timestamp"));
|
|
122
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* BoneScript Type System — Internal type representations.
|
|
3
|
+
* Implements spec/04_TYPE_SYSTEM.md.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type CVType =
|
|
7
|
+
| PrimitiveType
|
|
8
|
+
| GenericType
|
|
9
|
+
| RecordType
|
|
10
|
+
| UnionType
|
|
11
|
+
| TupleType
|
|
12
|
+
| BottomType;
|
|
13
|
+
|
|
14
|
+
export interface PrimitiveType {
|
|
15
|
+
tag: "primitive";
|
|
16
|
+
name: "string" | "uint" | "int" | "float" | "bool" | "timestamp" | "uuid" | "bytes" | "json";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface GenericType {
|
|
20
|
+
tag: "generic";
|
|
21
|
+
name: "list" | "set" | "map" | "optional" | "result";
|
|
22
|
+
args: CVType[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface RecordType {
|
|
26
|
+
tag: "record";
|
|
27
|
+
name: string; // entity name
|
|
28
|
+
fields: Map<string, CVType>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface UnionType {
|
|
32
|
+
tag: "union";
|
|
33
|
+
members: CVType[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface TupleType {
|
|
37
|
+
tag: "tuple";
|
|
38
|
+
elements: CVType[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface BottomType {
|
|
42
|
+
tag: "bottom"; // unifies with any optional<T>
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ─── Constructors ────────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
export function prim(name: PrimitiveType["name"]): PrimitiveType {
|
|
48
|
+
return { tag: "primitive", name };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function generic(name: GenericType["name"], ...args: CVType[]): GenericType {
|
|
52
|
+
return { tag: "generic", name, args };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function record(name: string, fields: Map<string, CVType>): RecordType {
|
|
56
|
+
return { tag: "record", name, fields };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const BOTTOM: BottomType = { tag: "bottom" };
|
|
60
|
+
|
|
61
|
+
// ─── Type Equality ───────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
export function typeEquals(a: CVType, b: CVType): boolean {
|
|
64
|
+
if (a.tag !== b.tag) {
|
|
65
|
+
// optional<T> accepts T (implicit wrapping)
|
|
66
|
+
if (a.tag === "generic" && a.name === "optional" && typeEquals(a.args[0], b)) return true;
|
|
67
|
+
if (b.tag === "generic" && b.name === "optional" && typeEquals(b.args[0], a)) return true;
|
|
68
|
+
// bottom unifies with any optional
|
|
69
|
+
if (a.tag === "bottom" && b.tag === "generic" && b.name === "optional") return true;
|
|
70
|
+
if (b.tag === "bottom" && a.tag === "generic" && a.name === "optional") return true;
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
switch (a.tag) {
|
|
75
|
+
case "primitive":
|
|
76
|
+
return a.name === (b as PrimitiveType).name;
|
|
77
|
+
case "generic": {
|
|
78
|
+
const bg = b as GenericType;
|
|
79
|
+
return a.name === bg.name && a.args.length === bg.args.length &&
|
|
80
|
+
a.args.every((arg, i) => typeEquals(arg, bg.args[i]));
|
|
81
|
+
}
|
|
82
|
+
case "record": {
|
|
83
|
+
const br = b as RecordType;
|
|
84
|
+
return a.name === br.name; // nominal for records (entity names)
|
|
85
|
+
}
|
|
86
|
+
case "union": {
|
|
87
|
+
const bu = b as UnionType;
|
|
88
|
+
return a.members.length === bu.members.length &&
|
|
89
|
+
a.members.every((m, i) => typeEquals(m, bu.members[i]));
|
|
90
|
+
}
|
|
91
|
+
case "tuple": {
|
|
92
|
+
const bt = b as TupleType;
|
|
93
|
+
return a.elements.length === bt.elements.length &&
|
|
94
|
+
a.elements.every((e, i) => typeEquals(e, bt.elements[i]));
|
|
95
|
+
}
|
|
96
|
+
case "bottom":
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ─── Type Display ────────────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
export function typeToString(t: CVType): string {
|
|
104
|
+
switch (t.tag) {
|
|
105
|
+
case "primitive": return t.name;
|
|
106
|
+
case "generic": return `${t.name}<${t.args.map(typeToString).join(", ")}>`;
|
|
107
|
+
case "record": return t.name;
|
|
108
|
+
case "union": return t.members.map(typeToString).join(" | ");
|
|
109
|
+
case "tuple": return `(${t.elements.map(typeToString).join(", ")})`;
|
|
110
|
+
case "bottom": return "bottom";
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ─── Numeric Type Check ──────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
export function isNumeric(t: CVType): boolean {
|
|
117
|
+
return t.tag === "primitive" && (t.name === "uint" || t.name === "int" || t.name === "float");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function isComparable(t: CVType): boolean {
|
|
121
|
+
return isNumeric(t) || (t.tag === "primitive" && (t.name === "string" || t.name === "timestamp"));
|
|
122
|
+
}
|
package/src/verifier.ts
CHANGED
|
@@ -41,16 +41,19 @@ export class Verifier {
|
|
|
41
41
|
verify(system: IR.IRSystem, files: EmittedFile[]): VerifyResult {
|
|
42
42
|
const issues: VerifyIssue[] = [];
|
|
43
43
|
|
|
44
|
-
//
|
|
45
|
-
|
|
44
|
+
// ─── IR Validation ────────────────────────────────────────────────────────
|
|
46
45
|
this.checkDependencies(system, issues);
|
|
46
|
+
this.checkEventSources(system, issues); // V002
|
|
47
47
|
this.checkDuplicateIds(system, issues);
|
|
48
48
|
this.checkModels(system, issues);
|
|
49
49
|
this.checkStateMachines(system, issues);
|
|
50
50
|
this.checkCircularDeps(system, issues);
|
|
51
|
+
this.checkPreconditions(system, issues); // V005
|
|
52
|
+
this.checkMethodEffects(system, issues); // V006
|
|
53
|
+
this.checkAuthDependencies(system, issues); // V011
|
|
54
|
+
this.checkResolutionMap(system, issues); // V012
|
|
51
55
|
|
|
52
|
-
//
|
|
53
|
-
|
|
56
|
+
// ─── Generated Code Validation ────────────────────────────────────────────
|
|
54
57
|
this.checkTypeScriptSyntax(files, issues);
|
|
55
58
|
this.checkSqlSyntax(files, issues);
|
|
56
59
|
this.checkImports(files, issues);
|
|
@@ -333,6 +336,150 @@ export class Verifier {
|
|
|
333
336
|
}
|
|
334
337
|
}
|
|
335
338
|
}
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
// ─── V002: Event source exists as a module ────────────────────────────────
|
|
342
|
+
private checkEventSources(system: IR.IRSystem, issues: VerifyIssue[]) {
|
|
343
|
+
const moduleIds = new Set(system.modules.map(m => m.id));
|
|
344
|
+
for (const ev of system.events) {
|
|
345
|
+
if (ev.source && ev.source !== "unknown" && !moduleIds.has(ev.source)) {
|
|
346
|
+
issues.push({
|
|
347
|
+
code: "V002",
|
|
348
|
+
severity: "warning",
|
|
349
|
+
message: `Event '${ev.name}' source '${ev.source}' does not match any module id`,
|
|
350
|
+
location: ev.id,
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// ─── V005: Preconditions reference accessible fields ─────────────────────
|
|
357
|
+
private checkPreconditions(system: IR.IRSystem, issues: VerifyIssue[]) {
|
|
358
|
+
// Build a map of all model field names by model name (lowercase for case-insensitive lookup)
|
|
359
|
+
const modelFields = new Map<string, Set<string>>();
|
|
360
|
+
for (const mod of system.modules) {
|
|
361
|
+
for (const model of mod.models) {
|
|
362
|
+
const fields = new Set(model.fields.map(f => f.name));
|
|
363
|
+
// Add ontology-entailed fields always present
|
|
364
|
+
fields.add("id"); fields.add("created_at"); fields.add("updated_at"); fields.add("state");
|
|
365
|
+
modelFields.set(model.name, fields);
|
|
366
|
+
modelFields.set(model.name.toLowerCase(), fields);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Simple field-path extractor: finds "word.word" patterns in a serialized expression
|
|
371
|
+
const fieldPathPattern = /\b([a-zA-Z_]\w*)\.([a-zA-Z_]\w*)\b/g;
|
|
372
|
+
|
|
373
|
+
for (const mod of system.modules) {
|
|
374
|
+
for (const iface of mod.interfaces) {
|
|
375
|
+
for (const method of iface.methods) {
|
|
376
|
+
for (const pre of method.preconditions) {
|
|
377
|
+
let match: RegExpExecArray | null;
|
|
378
|
+
fieldPathPattern.lastIndex = 0;
|
|
379
|
+
while ((match = fieldPathPattern.exec(pre.expression)) !== null) {
|
|
380
|
+
const [, paramName, fieldName] = match;
|
|
381
|
+
// Skip known non-field patterns (e.g. "now()", numeric literals)
|
|
382
|
+
if (paramName === "now" || /^\d/.test(paramName)) continue;
|
|
383
|
+
// Check if the field exists in any model — warn if not found
|
|
384
|
+
const foundInAnyModel = [...modelFields.values()].some(f => f.has(fieldName));
|
|
385
|
+
if (!foundInAnyModel) {
|
|
386
|
+
issues.push({
|
|
387
|
+
code: "V005",
|
|
388
|
+
severity: "warning",
|
|
389
|
+
message: `Precondition in '${method.name}' references '${paramName}.${fieldName}' — field '${fieldName}' not found in any model`,
|
|
390
|
+
location: `${mod.id}.${method.name}`,
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// ─── V006: Effects target fields that exist ───────────────────────────────
|
|
401
|
+
private checkMethodEffects(system: IR.IRSystem, issues: VerifyIssue[]) {
|
|
402
|
+
// Build a map of all model field names by model name
|
|
403
|
+
const modelFields = new Map<string, Set<string>>();
|
|
404
|
+
for (const mod of system.modules) {
|
|
405
|
+
for (const model of mod.models) {
|
|
406
|
+
const fields = new Set(model.fields.map(f => f.name));
|
|
407
|
+
modelFields.set(model.name, fields);
|
|
408
|
+
modelFields.set(model.name.toLowerCase(), fields);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
for (const mod of system.modules) {
|
|
413
|
+
for (const iface of mod.interfaces) {
|
|
414
|
+
for (const method of iface.methods) {
|
|
415
|
+
for (const effect of method.effects) {
|
|
416
|
+
const parts = effect.target.split(".");
|
|
417
|
+
if (parts.length < 2) continue;
|
|
418
|
+
const fieldName = parts[1];
|
|
419
|
+
// Try to find the model — check all models for the field
|
|
420
|
+
// (we can't always resolve the param name to a model here without type info)
|
|
421
|
+
// Only error if the field name looks like a typo (not found in ANY model)
|
|
422
|
+
const foundInAnyModel = [...modelFields.values()].some(fields => fields.has(fieldName));
|
|
423
|
+
if (!foundInAnyModel && !["state", "status", "owner_id"].includes(fieldName)) {
|
|
424
|
+
issues.push({
|
|
425
|
+
code: "V006",
|
|
426
|
+
severity: "warning",
|
|
427
|
+
message: `Effect target '${effect.target}' in method '${method.name}' — field '${fieldName}' not found in any model`,
|
|
428
|
+
location: `${mod.id}.${method.name}`,
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// ─── V011: Authenticated methods' modules depend on auth ─────────────────
|
|
438
|
+
private checkAuthDependencies(system: IR.IRSystem, issues: VerifyIssue[]) {
|
|
439
|
+
const authModuleIds = new Set(
|
|
440
|
+
system.modules
|
|
441
|
+
.filter(m => m.kind === "auth_service" || m.config["auth_method"])
|
|
442
|
+
.map(m => m.id)
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
for (const mod of system.modules) {
|
|
446
|
+
const hasAuthenticatedMethod = mod.interfaces.some(i =>
|
|
447
|
+
i.methods.some(m => m.authenticated)
|
|
448
|
+
);
|
|
449
|
+
if (!hasAuthenticatedMethod) continue;
|
|
450
|
+
|
|
451
|
+
// Module must either be an auth service itself or depend on one
|
|
452
|
+
const isAuthService = mod.kind === "auth_service";
|
|
453
|
+
const dependsOnAuth = mod.dependencies.some(dep => authModuleIds.has(dep));
|
|
454
|
+
const hasAuthConfig = mod.config["auth_method"] && mod.config["auth_method"] !== "none";
|
|
455
|
+
|
|
456
|
+
if (!isAuthService && !dependsOnAuth && !hasAuthConfig && authModuleIds.size > 0) {
|
|
457
|
+
issues.push({
|
|
458
|
+
code: "V011",
|
|
459
|
+
severity: "warning",
|
|
460
|
+
message: `Module '${mod.name}' has authenticated methods but does not declare an auth dependency`,
|
|
461
|
+
location: mod.id,
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// ─── V012: Resolution map is complete ────────────────────────────────────
|
|
468
|
+
private checkResolutionMap(system: IR.IRSystem, issues: VerifyIssue[]) {
|
|
469
|
+
// Resolution map must have at least the system-level keys
|
|
470
|
+
const required = ["system.name", "system.version", "system.domain"];
|
|
471
|
+
for (const key of required) {
|
|
472
|
+
if (!system.resolution[key]) {
|
|
473
|
+
issues.push({
|
|
474
|
+
code: "V012",
|
|
475
|
+
severity: "warning",
|
|
476
|
+
message: `Resolution map missing required key '${key}' — run constraint solver`,
|
|
477
|
+
location: system.name,
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
336
483
|
}
|
|
337
484
|
|
|
338
485
|
function resolvePath(base: string, relative: string): string {
|