@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.
- package/README.md +126 -0
- package/dist/ConfigurationLoader.d.ts +76 -0
- package/dist/ConfigurationLoader.d.ts.map +1 -0
- package/dist/ConfigurationLoader.js +144 -0
- package/dist/ConfigurationLoader.js.map +1 -0
- package/dist/ConfigurationValidator.d.ts +31 -0
- package/dist/ConfigurationValidator.d.ts.map +1 -0
- package/dist/ConfigurationValidator.js +242 -0
- package/dist/ConfigurationValidator.js.map +1 -0
- package/dist/EventProcessor.d.ts +49 -0
- package/dist/EventProcessor.d.ts.map +1 -0
- package/dist/EventProcessor.js +215 -0
- package/dist/EventProcessor.js.map +1 -0
- package/dist/EventRecorderService.d.ts +305 -0
- package/dist/EventRecorderService.d.ts.map +1 -0
- package/dist/EventRecorderService.js +463 -0
- package/dist/EventRecorderService.js.map +1 -0
- package/dist/LibraryLoader.d.ts +63 -0
- package/dist/LibraryLoader.d.ts.map +1 -0
- package/dist/LibraryLoader.js +188 -0
- package/dist/LibraryLoader.js.map +1 -0
- package/dist/PathBasedEventProcessor.d.ts +90 -0
- package/dist/PathBasedEventProcessor.d.ts.map +1 -0
- package/dist/PathBasedEventProcessor.js +239 -0
- package/dist/PathBasedEventProcessor.js.map +1 -0
- package/dist/SessionManager.d.ts +194 -0
- package/dist/SessionManager.d.ts.map +1 -0
- package/dist/SessionManager.js +299 -0
- package/dist/SessionManager.js.map +1 -0
- package/dist/ValidationEngine.d.ts +31 -0
- package/dist/ValidationEngine.d.ts.map +1 -0
- package/dist/ValidationEngine.js +158 -0
- package/dist/ValidationEngine.js.map +1 -0
- package/dist/helpers/GraphInstrumentationHelper.d.ts +93 -0
- package/dist/helpers/GraphInstrumentationHelper.d.ts.map +1 -0
- package/dist/helpers/GraphInstrumentationHelper.js +248 -0
- package/dist/helpers/GraphInstrumentationHelper.js.map +1 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +34 -0
- package/dist/index.js.map +1 -0
- package/dist/rules/config.d.ts +57 -0
- package/dist/rules/config.d.ts.map +1 -0
- package/dist/rules/config.js +382 -0
- package/dist/rules/config.js.map +1 -0
- package/dist/rules/engine.d.ts +70 -0
- package/dist/rules/engine.d.ts.map +1 -0
- package/dist/rules/engine.js +252 -0
- package/dist/rules/engine.js.map +1 -0
- package/dist/rules/implementations/connection-type-references.d.ts +7 -0
- package/dist/rules/implementations/connection-type-references.d.ts.map +1 -0
- package/dist/rules/implementations/connection-type-references.js +104 -0
- package/dist/rules/implementations/connection-type-references.js.map +1 -0
- package/dist/rules/implementations/dead-end-states.d.ts +17 -0
- package/dist/rules/implementations/dead-end-states.d.ts.map +1 -0
- package/dist/rules/implementations/dead-end-states.js +72 -0
- package/dist/rules/implementations/dead-end-states.js.map +1 -0
- package/dist/rules/implementations/index.d.ts +24 -0
- package/dist/rules/implementations/index.d.ts.map +1 -0
- package/dist/rules/implementations/index.js +62 -0
- package/dist/rules/implementations/index.js.map +1 -0
- package/dist/rules/implementations/library-node-type-match.d.ts +17 -0
- package/dist/rules/implementations/library-node-type-match.d.ts.map +1 -0
- package/dist/rules/implementations/library-node-type-match.js +123 -0
- package/dist/rules/implementations/library-node-type-match.js.map +1 -0
- package/dist/rules/implementations/minimum-node-sources.d.ts +22 -0
- package/dist/rules/implementations/minimum-node-sources.d.ts.map +1 -0
- package/dist/rules/implementations/minimum-node-sources.js +54 -0
- package/dist/rules/implementations/minimum-node-sources.js.map +1 -0
- package/dist/rules/implementations/no-unknown-fields.d.ts +7 -0
- package/dist/rules/implementations/no-unknown-fields.d.ts.map +1 -0
- package/dist/rules/implementations/no-unknown-fields.js +211 -0
- package/dist/rules/implementations/no-unknown-fields.js.map +1 -0
- package/dist/rules/implementations/orphaned-edge-types.d.ts +7 -0
- package/dist/rules/implementations/orphaned-edge-types.d.ts.map +1 -0
- package/dist/rules/implementations/orphaned-edge-types.js +47 -0
- package/dist/rules/implementations/orphaned-edge-types.js.map +1 -0
- package/dist/rules/implementations/orphaned-node-types.d.ts +7 -0
- package/dist/rules/implementations/orphaned-node-types.d.ts.map +1 -0
- package/dist/rules/implementations/orphaned-node-types.js +50 -0
- package/dist/rules/implementations/orphaned-node-types.js.map +1 -0
- package/dist/rules/implementations/required-metadata.d.ts +7 -0
- package/dist/rules/implementations/required-metadata.d.ts.map +1 -0
- package/dist/rules/implementations/required-metadata.js +57 -0
- package/dist/rules/implementations/required-metadata.js.map +1 -0
- package/dist/rules/implementations/state-transition-references.d.ts +7 -0
- package/dist/rules/implementations/state-transition-references.d.ts.map +1 -0
- package/dist/rules/implementations/state-transition-references.js +135 -0
- package/dist/rules/implementations/state-transition-references.js.map +1 -0
- package/dist/rules/implementations/unreachable-states.d.ts +7 -0
- package/dist/rules/implementations/unreachable-states.d.ts.map +1 -0
- package/dist/rules/implementations/unreachable-states.js +80 -0
- package/dist/rules/implementations/unreachable-states.js.map +1 -0
- package/dist/rules/implementations/valid-action-patterns.d.ts +17 -0
- package/dist/rules/implementations/valid-action-patterns.d.ts.map +1 -0
- package/dist/rules/implementations/valid-action-patterns.js +109 -0
- package/dist/rules/implementations/valid-action-patterns.js.map +1 -0
- package/dist/rules/implementations/valid-color-format.d.ts +7 -0
- package/dist/rules/implementations/valid-color-format.d.ts.map +1 -0
- package/dist/rules/implementations/valid-color-format.js +91 -0
- package/dist/rules/implementations/valid-color-format.js.map +1 -0
- package/dist/rules/implementations/valid-edge-types.d.ts +7 -0
- package/dist/rules/implementations/valid-edge-types.d.ts.map +1 -0
- package/dist/rules/implementations/valid-edge-types.js +244 -0
- package/dist/rules/implementations/valid-edge-types.js.map +1 -0
- package/dist/rules/implementations/valid-node-types.d.ts +7 -0
- package/dist/rules/implementations/valid-node-types.d.ts.map +1 -0
- package/dist/rules/implementations/valid-node-types.js +175 -0
- package/dist/rules/implementations/valid-node-types.js.map +1 -0
- package/dist/rules/index.d.ts +28 -0
- package/dist/rules/index.d.ts.map +1 -0
- package/dist/rules/index.js +45 -0
- package/dist/rules/index.js.map +1 -0
- package/dist/rules/types.d.ts +309 -0
- package/dist/rules/types.d.ts.map +1 -0
- package/dist/rules/types.js +35 -0
- package/dist/rules/types.js.map +1 -0
- package/dist/types/canvas.d.ts +409 -0
- package/dist/types/canvas.d.ts.map +1 -0
- package/dist/types/canvas.js +70 -0
- package/dist/types/canvas.js.map +1 -0
- package/dist/types/index.d.ts +311 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +13 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/library.d.ts +185 -0
- package/dist/types/library.d.ts.map +1 -0
- package/dist/types/library.js +15 -0
- package/dist/types/library.js.map +1 -0
- package/dist/types/path-based-config.d.ts +230 -0
- package/dist/types/path-based-config.d.ts.map +1 -0
- package/dist/types/path-based-config.js +9 -0
- package/dist/types/path-based-config.js.map +1 -0
- package/dist/utils/CanvasConverter.d.ts +118 -0
- package/dist/utils/CanvasConverter.d.ts.map +1 -0
- package/dist/utils/CanvasConverter.js +315 -0
- package/dist/utils/CanvasConverter.js.map +1 -0
- package/dist/utils/GraphConverter.d.ts +18 -0
- package/dist/utils/GraphConverter.d.ts.map +1 -0
- package/dist/utils/GraphConverter.js +61 -0
- package/dist/utils/GraphConverter.js.map +1 -0
- package/dist/utils/LibraryConverter.d.ts +113 -0
- package/dist/utils/LibraryConverter.d.ts.map +1 -0
- package/dist/utils/LibraryConverter.js +166 -0
- package/dist/utils/LibraryConverter.js.map +1 -0
- package/dist/utils/PathMatcher.d.ts +55 -0
- package/dist/utils/PathMatcher.d.ts.map +1 -0
- package/dist/utils/PathMatcher.js +172 -0
- package/dist/utils/PathMatcher.js.map +1 -0
- package/dist/utils/YamlParser.d.ts +36 -0
- package/dist/utils/YamlParser.d.ts.map +1 -0
- package/dist/utils/YamlParser.js +63 -0
- package/dist/utils/YamlParser.js.map +1 -0
- package/package.json +47 -0
- package/src/ConfigurationLoader.test.ts +490 -0
- package/src/ConfigurationLoader.ts +185 -0
- package/src/ConfigurationValidator.test.ts +200 -0
- package/src/ConfigurationValidator.ts +283 -0
- package/src/EventProcessor.test.ts +405 -0
- package/src/EventProcessor.ts +250 -0
- package/src/EventRecorderService.test.ts +541 -0
- package/src/EventRecorderService.ts +744 -0
- package/src/LibraryLoader.ts +215 -0
- package/src/PathBasedEventProcessor.test.ts +567 -0
- package/src/PathBasedEventProcessor.ts +332 -0
- package/src/SessionManager.test.ts +424 -0
- package/src/SessionManager.ts +470 -0
- package/src/ValidationEngine.test.ts +371 -0
- package/src/ValidationEngine.ts +196 -0
- package/src/helpers/GraphInstrumentationHelper.test.ts +340 -0
- package/src/helpers/GraphInstrumentationHelper.ts +326 -0
- package/src/index.ts +85 -0
- package/src/rules/config.test.ts +278 -0
- package/src/rules/config.ts +459 -0
- package/src/rules/engine.test.ts +332 -0
- package/src/rules/engine.ts +318 -0
- package/src/rules/implementations/connection-type-references.ts +117 -0
- package/src/rules/implementations/dead-end-states.ts +101 -0
- package/src/rules/implementations/index.ts +73 -0
- package/src/rules/implementations/library-node-type-match.ts +148 -0
- package/src/rules/implementations/minimum-node-sources.ts +82 -0
- package/src/rules/implementations/no-unknown-fields.ts +342 -0
- package/src/rules/implementations/orphaned-edge-types.ts +55 -0
- package/src/rules/implementations/orphaned-node-types.ts +58 -0
- package/src/rules/implementations/required-metadata.ts +64 -0
- package/src/rules/implementations/state-transition-references.ts +151 -0
- package/src/rules/implementations/unreachable-states.ts +94 -0
- package/src/rules/implementations/valid-action-patterns.ts +136 -0
- package/src/rules/implementations/valid-color-format.ts +140 -0
- package/src/rules/implementations/valid-edge-types.ts +258 -0
- package/src/rules/implementations/valid-node-types.ts +189 -0
- package/src/rules/index.ts +95 -0
- package/src/rules/types.ts +426 -0
- package/src/types/canvas.ts +496 -0
- package/src/types/index.ts +382 -0
- package/src/types/library.ts +233 -0
- package/src/types/path-based-config.ts +281 -0
- package/src/utils/CanvasConverter.ts +431 -0
- package/src/utils/GraphConverter.test.ts +195 -0
- package/src/utils/GraphConverter.ts +71 -0
- package/src/utils/LibraryConverter.ts +245 -0
- package/src/utils/PathMatcher.test.ts +148 -0
- package/src/utils/PathMatcher.ts +183 -0
- 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
|
+
}
|