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.
Files changed (167) hide show
  1. package/LICENSE +21 -21
  2. package/dist/algorithm_catalog.js +166 -166
  3. package/dist/cli.d.ts +2 -1
  4. package/dist/cli.js +75 -543
  5. package/dist/cli.js.map +1 -1
  6. package/dist/commands/check.d.ts +5 -0
  7. package/dist/commands/check.js +34 -0
  8. package/dist/commands/check.js.map +1 -0
  9. package/dist/commands/compile.d.ts +5 -0
  10. package/dist/commands/compile.js +215 -0
  11. package/dist/commands/compile.js.map +1 -0
  12. package/dist/commands/debug.d.ts +5 -0
  13. package/dist/commands/debug.js +59 -0
  14. package/dist/commands/debug.js.map +1 -0
  15. package/dist/commands/diff.d.ts +5 -0
  16. package/dist/commands/diff.js +125 -0
  17. package/dist/commands/diff.js.map +1 -0
  18. package/dist/commands/fmt.d.ts +5 -0
  19. package/dist/commands/fmt.js +49 -0
  20. package/dist/commands/fmt.js.map +1 -0
  21. package/dist/commands/init.d.ts +5 -0
  22. package/dist/commands/init.js +96 -0
  23. package/dist/commands/init.js.map +1 -0
  24. package/dist/commands/ir.d.ts +5 -0
  25. package/dist/commands/ir.js +27 -0
  26. package/dist/commands/ir.js.map +1 -0
  27. package/dist/commands/lex.d.ts +5 -0
  28. package/dist/commands/lex.js +21 -0
  29. package/dist/commands/lex.js.map +1 -0
  30. package/dist/commands/parse.d.ts +5 -0
  31. package/dist/commands/parse.js +30 -0
  32. package/dist/commands/parse.js.map +1 -0
  33. package/dist/commands/test.d.ts +5 -0
  34. package/dist/commands/test.js +61 -0
  35. package/dist/commands/test.js.map +1 -0
  36. package/dist/commands/verify_determinism.d.ts +5 -0
  37. package/dist/commands/verify_determinism.js +64 -0
  38. package/dist/commands/verify_determinism.js.map +1 -0
  39. package/dist/commands/watch.d.ts +5 -0
  40. package/dist/commands/watch.js +50 -0
  41. package/dist/commands/watch.js.map +1 -0
  42. package/dist/emit_auth.d.ts +6 -0
  43. package/dist/emit_auth.js +69 -0
  44. package/dist/emit_auth.js.map +1 -0
  45. package/dist/emit_capability.d.ts +13 -0
  46. package/dist/emit_capability.js +292 -128
  47. package/dist/emit_capability.js.map +1 -1
  48. package/dist/emit_composition.js +37 -3
  49. package/dist/emit_composition.js.map +1 -1
  50. package/dist/emit_database.d.ts +7 -0
  51. package/dist/emit_database.js +74 -0
  52. package/dist/emit_database.js.map +1 -0
  53. package/dist/emit_deploy.js +162 -162
  54. package/dist/emit_events.d.ts +1 -0
  55. package/dist/emit_events.js +342 -275
  56. package/dist/emit_events.js.map +1 -1
  57. package/dist/emit_full.js +135 -95
  58. package/dist/emit_full.js.map +1 -1
  59. package/dist/emit_index.d.ts +6 -0
  60. package/dist/emit_index.js +157 -0
  61. package/dist/emit_index.js.map +1 -0
  62. package/dist/emit_maintenance.js +249 -249
  63. package/dist/emit_models.d.ts +12 -0
  64. package/dist/emit_models.js +171 -0
  65. package/dist/emit_models.js.map +1 -0
  66. package/dist/emit_openapi.d.ts +9 -0
  67. package/dist/emit_openapi.js +308 -0
  68. package/dist/emit_openapi.js.map +1 -0
  69. package/dist/emit_package.d.ts +7 -0
  70. package/dist/emit_package.js +70 -0
  71. package/dist/emit_package.js.map +1 -0
  72. package/dist/emit_router.d.ts +12 -0
  73. package/dist/emit_router.js +390 -0
  74. package/dist/emit_router.js.map +1 -0
  75. package/dist/emit_runtime.d.ts +17 -11
  76. package/dist/emit_runtime.js +29 -686
  77. package/dist/emit_runtime.js.map +1 -1
  78. package/dist/emit_sourcemap.js +66 -66
  79. package/dist/emit_tests.js +37 -0
  80. package/dist/emit_tests.js.map +1 -1
  81. package/dist/emitter.js +34 -5
  82. package/dist/emitter.js.map +1 -1
  83. package/dist/extension_manager.d.ts +2 -2
  84. package/dist/extension_manager.js +6 -3
  85. package/dist/extension_manager.js.map +1 -1
  86. package/dist/lowering.d.ts +5 -14
  87. package/dist/lowering.js +47 -417
  88. package/dist/lowering.js.map +1 -1
  89. package/dist/lowering_channels.d.ts +11 -0
  90. package/dist/lowering_channels.js +102 -0
  91. package/dist/lowering_channels.js.map +1 -0
  92. package/dist/lowering_entities.d.ts +11 -0
  93. package/dist/lowering_entities.js +222 -0
  94. package/dist/lowering_entities.js.map +1 -0
  95. package/dist/lowering_helpers.d.ts +13 -0
  96. package/dist/lowering_helpers.js +76 -0
  97. package/dist/lowering_helpers.js.map +1 -0
  98. package/dist/module_loader.d.ts +2 -2
  99. package/dist/module_loader.js +20 -23
  100. package/dist/module_loader.js.map +1 -1
  101. package/dist/scaffold.d.ts +2 -2
  102. package/dist/scaffold.js +316 -319
  103. package/dist/scaffold.js.map +1 -1
  104. package/dist/typechecker.js +32 -13
  105. package/dist/typechecker.js.map +1 -1
  106. package/dist/verifier.d.ts +5 -0
  107. package/dist/verifier.js +140 -2
  108. package/dist/verifier.js.map +1 -1
  109. package/package.json +62 -52
  110. package/src/algorithm_catalog.ts +345 -345
  111. package/src/ast.ts +334 -334
  112. package/src/cli.ts +98 -624
  113. package/src/commands/check.ts +33 -0
  114. package/src/commands/compile.ts +191 -0
  115. package/src/commands/debug.ts +33 -0
  116. package/src/commands/diff.ts +108 -0
  117. package/src/commands/fmt.ts +22 -0
  118. package/src/commands/init.ts +72 -0
  119. package/src/commands/ir.ts +23 -0
  120. package/src/commands/lex.ts +17 -0
  121. package/src/commands/parse.ts +24 -0
  122. package/src/commands/test.ts +36 -0
  123. package/src/commands/verify_determinism.ts +66 -0
  124. package/src/commands/watch.ts +25 -0
  125. package/src/emit_auth.ts +67 -0
  126. package/src/emit_batch.ts +140 -140
  127. package/src/emit_capability.ts +617 -436
  128. package/src/emit_composition.ts +229 -196
  129. package/src/emit_database.ts +75 -0
  130. package/src/emit_deploy.ts +190 -190
  131. package/src/emit_events.ts +377 -307
  132. package/src/emit_extras.ts +240 -240
  133. package/src/emit_full.ts +351 -309
  134. package/src/emit_index.ts +161 -0
  135. package/src/emit_maintenance.ts +459 -459
  136. package/src/emit_models.ts +176 -0
  137. package/src/emit_openapi.ts +318 -0
  138. package/src/emit_package.ts +69 -0
  139. package/src/emit_router.ts +409 -0
  140. package/src/emit_runtime.ts +17 -728
  141. package/src/emit_sourcemap.ts +140 -140
  142. package/src/emit_tests.ts +246 -205
  143. package/src/emit_websocket.ts +229 -229
  144. package/src/emitter.ts +31 -5
  145. package/src/extension_manager.ts +189 -187
  146. package/src/formatter.ts +297 -297
  147. package/src/index.ts +88 -88
  148. package/src/ir.ts +215 -215
  149. package/src/lexer.ts +630 -630
  150. package/src/lowering.ts +142 -556
  151. package/src/lowering_channels.ts +107 -0
  152. package/src/lowering_entities.ts +248 -0
  153. package/src/lowering_helpers.ts +75 -0
  154. package/src/module_loader.ts +112 -114
  155. package/src/optimizer.ts +196 -196
  156. package/src/parse_decls.ts +409 -409
  157. package/src/parse_decls2.ts +244 -244
  158. package/src/parse_expr.ts +197 -197
  159. package/src/parse_types.ts +54 -54
  160. package/src/parser.ts +1 -1
  161. package/src/parser_base.ts +57 -57
  162. package/src/parser_recovery.ts +153 -153
  163. package/src/scaffold.ts +372 -375
  164. package/src/solver.ts +330 -330
  165. package/src/typechecker.ts +30 -15
  166. package/src/types.ts +122 -122
  167. package/src/verifier.ts +151 -4
@@ -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
- // User-defined — return json as permissive fallback
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
- // ─── IR Validation ─────────────────────────────────────────────────────
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
- // ─── Generated Code Validation ─────────────────────────────────────────
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 {