@principal-ai/principal-view-core 0.5.6

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 (204) hide show
  1. package/README.md +126 -0
  2. package/dist/ConfigurationLoader.d.ts +76 -0
  3. package/dist/ConfigurationLoader.d.ts.map +1 -0
  4. package/dist/ConfigurationLoader.js +144 -0
  5. package/dist/ConfigurationLoader.js.map +1 -0
  6. package/dist/ConfigurationValidator.d.ts +31 -0
  7. package/dist/ConfigurationValidator.d.ts.map +1 -0
  8. package/dist/ConfigurationValidator.js +242 -0
  9. package/dist/ConfigurationValidator.js.map +1 -0
  10. package/dist/EventProcessor.d.ts +49 -0
  11. package/dist/EventProcessor.d.ts.map +1 -0
  12. package/dist/EventProcessor.js +215 -0
  13. package/dist/EventProcessor.js.map +1 -0
  14. package/dist/EventRecorderService.d.ts +305 -0
  15. package/dist/EventRecorderService.d.ts.map +1 -0
  16. package/dist/EventRecorderService.js +463 -0
  17. package/dist/EventRecorderService.js.map +1 -0
  18. package/dist/LibraryLoader.d.ts +63 -0
  19. package/dist/LibraryLoader.d.ts.map +1 -0
  20. package/dist/LibraryLoader.js +188 -0
  21. package/dist/LibraryLoader.js.map +1 -0
  22. package/dist/PathBasedEventProcessor.d.ts +90 -0
  23. package/dist/PathBasedEventProcessor.d.ts.map +1 -0
  24. package/dist/PathBasedEventProcessor.js +239 -0
  25. package/dist/PathBasedEventProcessor.js.map +1 -0
  26. package/dist/SessionManager.d.ts +194 -0
  27. package/dist/SessionManager.d.ts.map +1 -0
  28. package/dist/SessionManager.js +299 -0
  29. package/dist/SessionManager.js.map +1 -0
  30. package/dist/ValidationEngine.d.ts +31 -0
  31. package/dist/ValidationEngine.d.ts.map +1 -0
  32. package/dist/ValidationEngine.js +158 -0
  33. package/dist/ValidationEngine.js.map +1 -0
  34. package/dist/helpers/GraphInstrumentationHelper.d.ts +93 -0
  35. package/dist/helpers/GraphInstrumentationHelper.d.ts.map +1 -0
  36. package/dist/helpers/GraphInstrumentationHelper.js +248 -0
  37. package/dist/helpers/GraphInstrumentationHelper.js.map +1 -0
  38. package/dist/index.d.ts +33 -0
  39. package/dist/index.d.ts.map +1 -0
  40. package/dist/index.js +34 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/rules/config.d.ts +57 -0
  43. package/dist/rules/config.d.ts.map +1 -0
  44. package/dist/rules/config.js +382 -0
  45. package/dist/rules/config.js.map +1 -0
  46. package/dist/rules/engine.d.ts +70 -0
  47. package/dist/rules/engine.d.ts.map +1 -0
  48. package/dist/rules/engine.js +252 -0
  49. package/dist/rules/engine.js.map +1 -0
  50. package/dist/rules/implementations/connection-type-references.d.ts +7 -0
  51. package/dist/rules/implementations/connection-type-references.d.ts.map +1 -0
  52. package/dist/rules/implementations/connection-type-references.js +104 -0
  53. package/dist/rules/implementations/connection-type-references.js.map +1 -0
  54. package/dist/rules/implementations/dead-end-states.d.ts +17 -0
  55. package/dist/rules/implementations/dead-end-states.d.ts.map +1 -0
  56. package/dist/rules/implementations/dead-end-states.js +72 -0
  57. package/dist/rules/implementations/dead-end-states.js.map +1 -0
  58. package/dist/rules/implementations/index.d.ts +24 -0
  59. package/dist/rules/implementations/index.d.ts.map +1 -0
  60. package/dist/rules/implementations/index.js +62 -0
  61. package/dist/rules/implementations/index.js.map +1 -0
  62. package/dist/rules/implementations/library-node-type-match.d.ts +17 -0
  63. package/dist/rules/implementations/library-node-type-match.d.ts.map +1 -0
  64. package/dist/rules/implementations/library-node-type-match.js +123 -0
  65. package/dist/rules/implementations/library-node-type-match.js.map +1 -0
  66. package/dist/rules/implementations/minimum-node-sources.d.ts +22 -0
  67. package/dist/rules/implementations/minimum-node-sources.d.ts.map +1 -0
  68. package/dist/rules/implementations/minimum-node-sources.js +54 -0
  69. package/dist/rules/implementations/minimum-node-sources.js.map +1 -0
  70. package/dist/rules/implementations/no-unknown-fields.d.ts +7 -0
  71. package/dist/rules/implementations/no-unknown-fields.d.ts.map +1 -0
  72. package/dist/rules/implementations/no-unknown-fields.js +211 -0
  73. package/dist/rules/implementations/no-unknown-fields.js.map +1 -0
  74. package/dist/rules/implementations/orphaned-edge-types.d.ts +7 -0
  75. package/dist/rules/implementations/orphaned-edge-types.d.ts.map +1 -0
  76. package/dist/rules/implementations/orphaned-edge-types.js +47 -0
  77. package/dist/rules/implementations/orphaned-edge-types.js.map +1 -0
  78. package/dist/rules/implementations/orphaned-node-types.d.ts +7 -0
  79. package/dist/rules/implementations/orphaned-node-types.d.ts.map +1 -0
  80. package/dist/rules/implementations/orphaned-node-types.js +50 -0
  81. package/dist/rules/implementations/orphaned-node-types.js.map +1 -0
  82. package/dist/rules/implementations/required-metadata.d.ts +7 -0
  83. package/dist/rules/implementations/required-metadata.d.ts.map +1 -0
  84. package/dist/rules/implementations/required-metadata.js +57 -0
  85. package/dist/rules/implementations/required-metadata.js.map +1 -0
  86. package/dist/rules/implementations/state-transition-references.d.ts +7 -0
  87. package/dist/rules/implementations/state-transition-references.d.ts.map +1 -0
  88. package/dist/rules/implementations/state-transition-references.js +135 -0
  89. package/dist/rules/implementations/state-transition-references.js.map +1 -0
  90. package/dist/rules/implementations/unreachable-states.d.ts +7 -0
  91. package/dist/rules/implementations/unreachable-states.d.ts.map +1 -0
  92. package/dist/rules/implementations/unreachable-states.js +80 -0
  93. package/dist/rules/implementations/unreachable-states.js.map +1 -0
  94. package/dist/rules/implementations/valid-action-patterns.d.ts +17 -0
  95. package/dist/rules/implementations/valid-action-patterns.d.ts.map +1 -0
  96. package/dist/rules/implementations/valid-action-patterns.js +109 -0
  97. package/dist/rules/implementations/valid-action-patterns.js.map +1 -0
  98. package/dist/rules/implementations/valid-color-format.d.ts +7 -0
  99. package/dist/rules/implementations/valid-color-format.d.ts.map +1 -0
  100. package/dist/rules/implementations/valid-color-format.js +91 -0
  101. package/dist/rules/implementations/valid-color-format.js.map +1 -0
  102. package/dist/rules/implementations/valid-edge-types.d.ts +7 -0
  103. package/dist/rules/implementations/valid-edge-types.d.ts.map +1 -0
  104. package/dist/rules/implementations/valid-edge-types.js +244 -0
  105. package/dist/rules/implementations/valid-edge-types.js.map +1 -0
  106. package/dist/rules/implementations/valid-node-types.d.ts +7 -0
  107. package/dist/rules/implementations/valid-node-types.d.ts.map +1 -0
  108. package/dist/rules/implementations/valid-node-types.js +175 -0
  109. package/dist/rules/implementations/valid-node-types.js.map +1 -0
  110. package/dist/rules/index.d.ts +28 -0
  111. package/dist/rules/index.d.ts.map +1 -0
  112. package/dist/rules/index.js +45 -0
  113. package/dist/rules/index.js.map +1 -0
  114. package/dist/rules/types.d.ts +309 -0
  115. package/dist/rules/types.d.ts.map +1 -0
  116. package/dist/rules/types.js +35 -0
  117. package/dist/rules/types.js.map +1 -0
  118. package/dist/types/canvas.d.ts +409 -0
  119. package/dist/types/canvas.d.ts.map +1 -0
  120. package/dist/types/canvas.js +70 -0
  121. package/dist/types/canvas.js.map +1 -0
  122. package/dist/types/index.d.ts +311 -0
  123. package/dist/types/index.d.ts.map +1 -0
  124. package/dist/types/index.js +13 -0
  125. package/dist/types/index.js.map +1 -0
  126. package/dist/types/library.d.ts +185 -0
  127. package/dist/types/library.d.ts.map +1 -0
  128. package/dist/types/library.js +15 -0
  129. package/dist/types/library.js.map +1 -0
  130. package/dist/types/path-based-config.d.ts +230 -0
  131. package/dist/types/path-based-config.d.ts.map +1 -0
  132. package/dist/types/path-based-config.js +9 -0
  133. package/dist/types/path-based-config.js.map +1 -0
  134. package/dist/utils/CanvasConverter.d.ts +118 -0
  135. package/dist/utils/CanvasConverter.d.ts.map +1 -0
  136. package/dist/utils/CanvasConverter.js +315 -0
  137. package/dist/utils/CanvasConverter.js.map +1 -0
  138. package/dist/utils/GraphConverter.d.ts +18 -0
  139. package/dist/utils/GraphConverter.d.ts.map +1 -0
  140. package/dist/utils/GraphConverter.js +61 -0
  141. package/dist/utils/GraphConverter.js.map +1 -0
  142. package/dist/utils/LibraryConverter.d.ts +113 -0
  143. package/dist/utils/LibraryConverter.d.ts.map +1 -0
  144. package/dist/utils/LibraryConverter.js +166 -0
  145. package/dist/utils/LibraryConverter.js.map +1 -0
  146. package/dist/utils/PathMatcher.d.ts +55 -0
  147. package/dist/utils/PathMatcher.d.ts.map +1 -0
  148. package/dist/utils/PathMatcher.js +172 -0
  149. package/dist/utils/PathMatcher.js.map +1 -0
  150. package/dist/utils/YamlParser.d.ts +36 -0
  151. package/dist/utils/YamlParser.d.ts.map +1 -0
  152. package/dist/utils/YamlParser.js +63 -0
  153. package/dist/utils/YamlParser.js.map +1 -0
  154. package/package.json +47 -0
  155. package/src/ConfigurationLoader.test.ts +490 -0
  156. package/src/ConfigurationLoader.ts +185 -0
  157. package/src/ConfigurationValidator.test.ts +200 -0
  158. package/src/ConfigurationValidator.ts +283 -0
  159. package/src/EventProcessor.test.ts +405 -0
  160. package/src/EventProcessor.ts +250 -0
  161. package/src/EventRecorderService.test.ts +541 -0
  162. package/src/EventRecorderService.ts +744 -0
  163. package/src/LibraryLoader.ts +215 -0
  164. package/src/PathBasedEventProcessor.test.ts +567 -0
  165. package/src/PathBasedEventProcessor.ts +332 -0
  166. package/src/SessionManager.test.ts +424 -0
  167. package/src/SessionManager.ts +470 -0
  168. package/src/ValidationEngine.test.ts +371 -0
  169. package/src/ValidationEngine.ts +196 -0
  170. package/src/helpers/GraphInstrumentationHelper.test.ts +340 -0
  171. package/src/helpers/GraphInstrumentationHelper.ts +326 -0
  172. package/src/index.ts +85 -0
  173. package/src/rules/config.test.ts +278 -0
  174. package/src/rules/config.ts +459 -0
  175. package/src/rules/engine.test.ts +332 -0
  176. package/src/rules/engine.ts +318 -0
  177. package/src/rules/implementations/connection-type-references.ts +117 -0
  178. package/src/rules/implementations/dead-end-states.ts +101 -0
  179. package/src/rules/implementations/index.ts +73 -0
  180. package/src/rules/implementations/library-node-type-match.ts +148 -0
  181. package/src/rules/implementations/minimum-node-sources.ts +82 -0
  182. package/src/rules/implementations/no-unknown-fields.ts +342 -0
  183. package/src/rules/implementations/orphaned-edge-types.ts +55 -0
  184. package/src/rules/implementations/orphaned-node-types.ts +58 -0
  185. package/src/rules/implementations/required-metadata.ts +64 -0
  186. package/src/rules/implementations/state-transition-references.ts +151 -0
  187. package/src/rules/implementations/unreachable-states.ts +94 -0
  188. package/src/rules/implementations/valid-action-patterns.ts +136 -0
  189. package/src/rules/implementations/valid-color-format.ts +140 -0
  190. package/src/rules/implementations/valid-edge-types.ts +258 -0
  191. package/src/rules/implementations/valid-node-types.ts +189 -0
  192. package/src/rules/index.ts +95 -0
  193. package/src/rules/types.ts +426 -0
  194. package/src/types/canvas.ts +496 -0
  195. package/src/types/index.ts +382 -0
  196. package/src/types/library.ts +233 -0
  197. package/src/types/path-based-config.ts +281 -0
  198. package/src/utils/CanvasConverter.ts +431 -0
  199. package/src/utils/GraphConverter.test.ts +195 -0
  200. package/src/utils/GraphConverter.ts +71 -0
  201. package/src/utils/LibraryConverter.ts +245 -0
  202. package/src/utils/PathMatcher.test.ts +148 -0
  203. package/src/utils/PathMatcher.ts +183 -0
  204. package/src/utils/YamlParser.ts +75 -0
@@ -0,0 +1,371 @@
1
+ import { ValidationEngine } from './ValidationEngine';
2
+ import type { GraphEvent, GraphState, ValidationRules } from './types';
3
+
4
+ describe('ValidationEngine', () => {
5
+ let testState: GraphState;
6
+
7
+ beforeEach(() => {
8
+ testState = {
9
+ nodes: new Map([
10
+ [
11
+ 'user-1',
12
+ {
13
+ id: 'user-1',
14
+ type: 'user',
15
+ data: { userId: 'alice' },
16
+ state: 'offline',
17
+ createdAt: Date.now(),
18
+ updatedAt: Date.now(),
19
+ },
20
+ ],
21
+ [
22
+ 'server-1',
23
+ {
24
+ id: 'server-1',
25
+ type: 'server',
26
+ data: { uptime: 0 },
27
+ createdAt: Date.now(),
28
+ updatedAt: Date.now(),
29
+ },
30
+ ],
31
+ ]),
32
+ edges: new Map(),
33
+ configuration: {
34
+ metadata: { name: 'Test', version: '1.0.0' },
35
+ nodeTypes: {
36
+ user: { shape: 'circle', dataSchema: {} },
37
+ server: { shape: 'hexagon', dataSchema: {} },
38
+ },
39
+ edgeTypes: {
40
+ connection: { style: 'solid', directed: true },
41
+ },
42
+ allowedConnections: [{ from: 'user', to: 'server', via: 'connection' }],
43
+ },
44
+ };
45
+ });
46
+
47
+ describe('Connection Validation', () => {
48
+ it('should allow valid connections', () => {
49
+ const rules: ValidationRules = {};
50
+ const engine = new ValidationEngine(rules);
51
+
52
+ const event: GraphEvent = {
53
+ id: 'evt-1',
54
+ type: 'edge_created',
55
+ timestamp: Date.now(),
56
+ category: 'edge',
57
+ operation: 'create',
58
+ payload: {
59
+ operation: 'create',
60
+ edgeId: 'conn-1',
61
+ edgeType: 'connection',
62
+ from: 'user-1',
63
+ to: 'server-1',
64
+ },
65
+ expected: true,
66
+ };
67
+
68
+ const result = engine.validate(event, testState);
69
+
70
+ expect(result.valid).toBe(true);
71
+ expect(result.violations).toHaveLength(0);
72
+ });
73
+
74
+ it('should reject invalid connections', () => {
75
+ const rules: ValidationRules = {};
76
+ const engine = new ValidationEngine(rules);
77
+
78
+ // Try to connect server to user (not allowed)
79
+ const event: GraphEvent = {
80
+ id: 'evt-1',
81
+ type: 'edge_created',
82
+ timestamp: Date.now(),
83
+ category: 'edge',
84
+ operation: 'create',
85
+ payload: {
86
+ operation: 'create',
87
+ edgeId: 'conn-1',
88
+ edgeType: 'connection',
89
+ from: 'server-1',
90
+ to: 'user-1',
91
+ },
92
+ expected: true,
93
+ };
94
+
95
+ const result = engine.validate(event, testState);
96
+
97
+ expect(result.valid).toBe(false);
98
+ expect(result.violations).toHaveLength(1);
99
+ expect(result.violations[0].type).toBe('connection');
100
+ expect(result.violations[0].severity).toBe('error');
101
+ });
102
+
103
+ it('should reject connections when nodes do not exist', () => {
104
+ const rules: ValidationRules = {};
105
+ const engine = new ValidationEngine(rules);
106
+
107
+ const event: GraphEvent = {
108
+ id: 'evt-1',
109
+ type: 'edge_created',
110
+ timestamp: Date.now(),
111
+ category: 'edge',
112
+ operation: 'create',
113
+ payload: {
114
+ operation: 'create',
115
+ edgeId: 'conn-1',
116
+ edgeType: 'connection',
117
+ from: 'user-1',
118
+ to: 'nonexistent-node',
119
+ },
120
+ expected: true,
121
+ };
122
+
123
+ const result = engine.validate(event, testState);
124
+
125
+ expect(result.valid).toBe(false);
126
+ expect(result.violations).toHaveLength(1);
127
+ expect(result.violations[0].description).toContain('do not exist');
128
+ });
129
+ });
130
+
131
+ describe('State Transition Validation', () => {
132
+ it('should allow valid state transitions', () => {
133
+ const rules: ValidationRules = {
134
+ stateTransitions: {
135
+ user: [
136
+ { from: 'offline', to: ['online'] },
137
+ { from: 'online', to: ['offline', 'grace'] },
138
+ ],
139
+ },
140
+ };
141
+ const engine = new ValidationEngine(rules);
142
+
143
+ const event: GraphEvent = {
144
+ id: 'evt-1',
145
+ type: 'state_changed',
146
+ timestamp: Date.now(),
147
+ category: 'state',
148
+ operation: 'update',
149
+ payload: {
150
+ nodeId: 'user-1',
151
+ previousState: 'offline',
152
+ newState: 'online',
153
+ },
154
+ expected: true,
155
+ };
156
+
157
+ const result = engine.validate(event, testState);
158
+
159
+ expect(result.valid).toBe(true);
160
+ expect(result.violations).toHaveLength(0);
161
+ });
162
+
163
+ it('should reject invalid state transitions', () => {
164
+ const rules: ValidationRules = {
165
+ stateTransitions: {
166
+ user: [
167
+ { from: 'offline', to: ['online'] },
168
+ { from: 'online', to: ['offline', 'grace'] },
169
+ ],
170
+ },
171
+ };
172
+ const engine = new ValidationEngine(rules);
173
+
174
+ // Try to go from offline to grace (not allowed)
175
+ const event: GraphEvent = {
176
+ id: 'evt-1',
177
+ type: 'state_changed',
178
+ timestamp: Date.now(),
179
+ category: 'state',
180
+ operation: 'update',
181
+ payload: {
182
+ nodeId: 'user-1',
183
+ previousState: 'offline',
184
+ newState: 'grace',
185
+ },
186
+ expected: true,
187
+ };
188
+
189
+ const result = engine.validate(event, testState);
190
+
191
+ expect(result.valid).toBe(false);
192
+ expect(result.violations).toHaveLength(1);
193
+ expect(result.violations[0].type).toBe('state');
194
+ expect(result.violations[0].description).toContain('Invalid state transition');
195
+ });
196
+ });
197
+
198
+ describe('Cardinality Constraints', () => {
199
+ it('should check minimum cardinality', () => {
200
+ const rules: ValidationRules = {
201
+ cardinality: {
202
+ server: { min: 1, max: 1 },
203
+ },
204
+ };
205
+ const engine = new ValidationEngine(rules);
206
+
207
+ // Remove server node
208
+ const stateWithoutServer: GraphState = {
209
+ ...testState,
210
+ nodes: new Map(Array.from(testState.nodes.entries()).filter(([id]) => id !== 'server-1')),
211
+ };
212
+
213
+ const violations = engine.checkConstraints(stateWithoutServer);
214
+
215
+ expect(violations).toHaveLength(1);
216
+ expect(violations[0].type).toBe('cardinality');
217
+ expect(violations[0].description).toContain('at least 1');
218
+ });
219
+
220
+ it('should check maximum cardinality', () => {
221
+ const rules: ValidationRules = {
222
+ cardinality: {
223
+ server: { min: 1, max: 1 },
224
+ },
225
+ };
226
+ const engine = new ValidationEngine(rules);
227
+
228
+ // Add second server node
229
+ testState.nodes.set('server-2', {
230
+ id: 'server-2',
231
+ type: 'server',
232
+ data: { uptime: 0 },
233
+ createdAt: Date.now(),
234
+ updatedAt: Date.now(),
235
+ });
236
+
237
+ const violations = engine.checkConstraints(testState);
238
+
239
+ expect(violations).toHaveLength(1);
240
+ expect(violations[0].type).toBe('cardinality');
241
+ expect(violations[0].description).toContain('at most 1');
242
+ });
243
+
244
+ it('should pass when cardinality is within bounds', () => {
245
+ const rules: ValidationRules = {
246
+ cardinality: {
247
+ server: { min: 1, max: 2 },
248
+ },
249
+ };
250
+ const engine = new ValidationEngine(rules);
251
+
252
+ const violations = engine.checkConstraints(testState);
253
+
254
+ expect(violations).toHaveLength(0);
255
+ });
256
+ });
257
+
258
+ describe('Unexpected Events', () => {
259
+ it('should flag unexpected events', () => {
260
+ const rules: ValidationRules = {};
261
+ const engine = new ValidationEngine(rules);
262
+
263
+ const event: GraphEvent = {
264
+ id: 'evt-1',
265
+ type: 'unexpected_event',
266
+ timestamp: Date.now(),
267
+ category: 'node',
268
+ operation: 'create',
269
+ payload: {
270
+ operation: 'create',
271
+ nodeId: 'user-2',
272
+ nodeType: 'user',
273
+ data: {},
274
+ },
275
+ expected: false, // Marked as unexpected
276
+ };
277
+
278
+ const result = engine.validate(event, testState);
279
+
280
+ expect(result.valid).toBe(true); // Still valid, just a warning
281
+ expect(result.violations).toHaveLength(1);
282
+ expect(result.violations[0].type).toBe('unexpected_event');
283
+ expect(result.violations[0].severity).toBe('warning');
284
+ });
285
+
286
+ it('should not flag expected events', () => {
287
+ const rules: ValidationRules = {};
288
+ const engine = new ValidationEngine(rules);
289
+
290
+ const event: GraphEvent = {
291
+ id: 'evt-1',
292
+ type: 'expected_event',
293
+ timestamp: Date.now(),
294
+ category: 'node',
295
+ operation: 'create',
296
+ payload: {
297
+ operation: 'create',
298
+ nodeId: 'user-2',
299
+ nodeType: 'user',
300
+ data: {},
301
+ },
302
+ expected: true,
303
+ };
304
+
305
+ const result = engine.validate(event, testState);
306
+
307
+ const unexpectedViolations = result.violations.filter(
308
+ (v) => v.type === 'unexpected_event'
309
+ );
310
+ expect(unexpectedViolations).toHaveLength(0);
311
+ });
312
+ });
313
+
314
+ describe('Validation Metrics', () => {
315
+ it('should track validation metrics', () => {
316
+ const rules: ValidationRules = {};
317
+ const engine = new ValidationEngine(rules);
318
+
319
+ const event: GraphEvent = {
320
+ id: 'evt-1',
321
+ type: 'edge_created',
322
+ timestamp: Date.now(),
323
+ category: 'edge',
324
+ operation: 'create',
325
+ payload: {
326
+ operation: 'create',
327
+ edgeId: 'conn-1',
328
+ edgeType: 'connection',
329
+ from: 'user-1',
330
+ to: 'server-1',
331
+ },
332
+ expected: true,
333
+ };
334
+
335
+ const result = engine.validate(event, testState);
336
+
337
+ expect(result.metrics.totalEvents).toBe(1);
338
+ expect(result.metrics.validEvents).toBe(1);
339
+ expect(result.metrics.violations).toBe(0);
340
+ expect(result.metrics.unexpectedEvents).toBe(0);
341
+ });
342
+
343
+ it('should count violations in metrics', () => {
344
+ const rules: ValidationRules = {};
345
+ const engine = new ValidationEngine(rules);
346
+
347
+ // Invalid connection
348
+ const event: GraphEvent = {
349
+ id: 'evt-1',
350
+ type: 'edge_created',
351
+ timestamp: Date.now(),
352
+ category: 'edge',
353
+ operation: 'create',
354
+ payload: {
355
+ operation: 'create',
356
+ edgeId: 'conn-1',
357
+ edgeType: 'connection',
358
+ from: 'server-1',
359
+ to: 'user-1',
360
+ },
361
+ expected: true,
362
+ };
363
+
364
+ const result = engine.validate(event, testState);
365
+
366
+ expect(result.metrics.totalEvents).toBe(1);
367
+ expect(result.metrics.validEvents).toBe(0);
368
+ expect(result.metrics.violations).toBe(1);
369
+ });
370
+ });
371
+ });
@@ -0,0 +1,196 @@
1
+ import type {
2
+ GraphEvent,
3
+ GraphState,
4
+ ValidationResult,
5
+ ValidationRules,
6
+ Violation,
7
+ Warning,
8
+ } from './types';
9
+
10
+ /**
11
+ * ValidationEngine checks events against defined rules
12
+ */
13
+ export class ValidationEngine {
14
+ private rules: ValidationRules;
15
+ private violationCount = 0;
16
+ private warningCount = 0;
17
+
18
+ constructor(rules: ValidationRules) {
19
+ this.rules = rules;
20
+ }
21
+
22
+ /**
23
+ * Validate an event against all rules
24
+ */
25
+ validate(event: GraphEvent, state: GraphState): ValidationResult {
26
+ const violations: Violation[] = [];
27
+ const warnings: Warning[] = [];
28
+
29
+ // Check connection rules
30
+ if (event.category === 'edge' && event.operation === 'create') {
31
+ violations.push(...this.validateConnection(event, state));
32
+ }
33
+
34
+ // Check state transition rules
35
+ if (event.category === 'state') {
36
+ violations.push(...this.validateStateTransition(event, state));
37
+ }
38
+
39
+ // Check if event was expected
40
+ if (event.expected === false) {
41
+ violations.push(this.createUnexpectedEventViolation(event));
42
+ }
43
+
44
+ // Update counters
45
+ this.violationCount += violations.filter((v) => v.severity === 'error').length;
46
+ this.warningCount += violations.filter((v) => v.severity === 'warning').length;
47
+
48
+ return {
49
+ valid: violations.filter((v) => v.severity === 'error').length === 0,
50
+ violations,
51
+ warnings,
52
+ metrics: {
53
+ totalEvents: 1,
54
+ validEvents: violations.length === 0 ? 1 : 0,
55
+ violations: violations.filter((v) => v.severity === 'error').length,
56
+ warnings: violations.filter((v) => v.severity === 'warning').length,
57
+ unexpectedEvents: event.expected === false ? 1 : 0,
58
+ expectedEventsMissing: 0,
59
+ },
60
+ };
61
+ }
62
+
63
+ /**
64
+ * Check constraints across the entire graph state
65
+ */
66
+ checkConstraints(state: GraphState): Violation[] {
67
+ const violations: Violation[] = [];
68
+
69
+ // Check cardinality constraints
70
+ if (this.rules.cardinality) {
71
+ for (const [nodeType, rule] of Object.entries(this.rules.cardinality)) {
72
+ const count = Array.from(state.nodes.values()).filter((n) => n.type === nodeType).length;
73
+
74
+ if (rule.min !== undefined && count < rule.min) {
75
+ violations.push({
76
+ id: `cardinality-${nodeType}-min`,
77
+ severity: 'error',
78
+ type: 'cardinality',
79
+ description: `Node type '${nodeType}' has ${count} instances but requires at least ${rule.min}`,
80
+ });
81
+ }
82
+
83
+ if (rule.max !== undefined && count > rule.max) {
84
+ violations.push({
85
+ id: `cardinality-${nodeType}-max`,
86
+ severity: 'error',
87
+ type: 'cardinality',
88
+ description: `Node type '${nodeType}' has ${count} instances but allows at most ${rule.max}`,
89
+ });
90
+ }
91
+ }
92
+ }
93
+
94
+ return violations;
95
+ }
96
+
97
+ /**
98
+ * Validate connection rules
99
+ */
100
+ private validateConnection(event: GraphEvent, state: GraphState): Violation[] {
101
+ const violations: Violation[] = [];
102
+ const payload = event.payload as any;
103
+
104
+ // Get the nodes being connected
105
+ const fromNode = state.nodes.get(payload.from);
106
+ const toNode = state.nodes.get(payload.to);
107
+
108
+ if (!fromNode || !toNode) {
109
+ violations.push({
110
+ id: `connection-${event.id}`,
111
+ severity: 'error',
112
+ type: 'connection',
113
+ description: 'Cannot create edge: one or both nodes do not exist',
114
+ event,
115
+ context: {
116
+ edgeId: payload.edgeId,
117
+ },
118
+ });
119
+ return violations;
120
+ }
121
+
122
+ // Check if this connection is allowed
123
+ const allowedConnections = state.configuration.allowedConnections || [];
124
+ const isAllowed = allowedConnections.some(
125
+ (rule) =>
126
+ rule.from === fromNode.type && rule.to === toNode.type && rule.via === payload.edgeType
127
+ );
128
+
129
+ if (!isAllowed) {
130
+ violations.push({
131
+ id: `connection-${event.id}`,
132
+ severity: 'error',
133
+ type: 'connection',
134
+ description: `Connection from '${fromNode.type}' to '${toNode.type}' via '${payload.edgeType}' is not allowed`,
135
+ event,
136
+ context: {
137
+ edgeId: payload.edgeId,
138
+ },
139
+ });
140
+ }
141
+
142
+ return violations;
143
+ }
144
+
145
+ /**
146
+ * Validate state transitions
147
+ */
148
+ private validateStateTransition(event: GraphEvent, state: GraphState): Violation[] {
149
+ const violations: Violation[] = [];
150
+ const payload = event.payload as any;
151
+
152
+ const node = state.nodes.get(payload.nodeId);
153
+ if (!node) {
154
+ return violations;
155
+ }
156
+
157
+ const transitions = this.rules.stateTransitions?.[node.type];
158
+ if (!transitions) {
159
+ return violations;
160
+ }
161
+
162
+ const currentState = payload.previousState || node.state;
163
+ const newState = payload.newState;
164
+
165
+ // Find valid transitions from current state
166
+ const validTransition = transitions.find((t) => t.from === currentState);
167
+
168
+ if (validTransition && !validTransition.to.includes(newState)) {
169
+ violations.push({
170
+ id: `state-transition-${event.id}`,
171
+ severity: 'error',
172
+ type: 'state',
173
+ description: `Invalid state transition for '${node.type}': ${currentState} -> ${newState}. Valid transitions: ${validTransition.to.join(', ')}`,
174
+ event,
175
+ context: {
176
+ nodeId: payload.nodeId,
177
+ },
178
+ });
179
+ }
180
+
181
+ return violations;
182
+ }
183
+
184
+ /**
185
+ * Create violation for unexpected event
186
+ */
187
+ private createUnexpectedEventViolation(event: GraphEvent): Violation {
188
+ return {
189
+ id: `unexpected-${event.id}`,
190
+ severity: 'warning',
191
+ type: 'unexpected_event',
192
+ description: `Unexpected event: ${event.type}`,
193
+ event,
194
+ };
195
+ }
196
+ }