@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,332 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { GraphRulesEngine, createRulesEngine } from './engine';
|
|
3
|
+
import { builtinRules } from './implementations';
|
|
4
|
+
import type { GraphConfiguration } from '../types';
|
|
5
|
+
import type { GraphRule } from './types';
|
|
6
|
+
|
|
7
|
+
describe('GraphRulesEngine', () => {
|
|
8
|
+
let engine: GraphRulesEngine;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
engine = new GraphRulesEngine();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe('rule registration', () => {
|
|
15
|
+
it('should register a rule', () => {
|
|
16
|
+
const mockRule: GraphRule = {
|
|
17
|
+
id: 'test-rule',
|
|
18
|
+
name: 'Test Rule',
|
|
19
|
+
description: 'A test rule',
|
|
20
|
+
impact: 'Test impact',
|
|
21
|
+
severity: 'error',
|
|
22
|
+
category: 'schema',
|
|
23
|
+
enabled: true,
|
|
24
|
+
fixable: false,
|
|
25
|
+
check: async () => [],
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
engine.registerRule(mockRule);
|
|
29
|
+
expect(engine.getRule('test-rule')).toBe(mockRule);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should throw when registering duplicate rule', () => {
|
|
33
|
+
const mockRule: GraphRule = {
|
|
34
|
+
id: 'test-rule',
|
|
35
|
+
name: 'Test Rule',
|
|
36
|
+
description: 'A test rule',
|
|
37
|
+
impact: 'Test impact',
|
|
38
|
+
severity: 'error',
|
|
39
|
+
category: 'schema',
|
|
40
|
+
enabled: true,
|
|
41
|
+
fixable: false,
|
|
42
|
+
check: async () => [],
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
engine.registerRule(mockRule);
|
|
46
|
+
expect(() => engine.registerRule(mockRule)).toThrow('already registered');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should register multiple rules', () => {
|
|
50
|
+
const rules: GraphRule[] = [
|
|
51
|
+
{
|
|
52
|
+
id: 'rule-1',
|
|
53
|
+
name: 'Rule 1',
|
|
54
|
+
description: 'Rule 1',
|
|
55
|
+
impact: 'Impact 1',
|
|
56
|
+
severity: 'error',
|
|
57
|
+
category: 'schema',
|
|
58
|
+
enabled: true,
|
|
59
|
+
fixable: false,
|
|
60
|
+
check: async () => [],
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: 'rule-2',
|
|
64
|
+
name: 'Rule 2',
|
|
65
|
+
description: 'Rule 2',
|
|
66
|
+
impact: 'Impact 2',
|
|
67
|
+
severity: 'warn',
|
|
68
|
+
category: 'reference',
|
|
69
|
+
enabled: true,
|
|
70
|
+
fixable: false,
|
|
71
|
+
check: async () => [],
|
|
72
|
+
},
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
engine.registerRules(rules);
|
|
76
|
+
expect(engine.getAllRules().size).toBe(2);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should unregister a rule', () => {
|
|
80
|
+
const mockRule: GraphRule = {
|
|
81
|
+
id: 'test-rule',
|
|
82
|
+
name: 'Test Rule',
|
|
83
|
+
description: 'A test rule',
|
|
84
|
+
impact: 'Test impact',
|
|
85
|
+
severity: 'error',
|
|
86
|
+
category: 'schema',
|
|
87
|
+
enabled: true,
|
|
88
|
+
fixable: false,
|
|
89
|
+
check: async () => [],
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
engine.registerRule(mockRule);
|
|
93
|
+
expect(engine.unregisterRule('test-rule')).toBe(true);
|
|
94
|
+
expect(engine.getRule('test-rule')).toBeUndefined();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should get rules by category', () => {
|
|
98
|
+
engine.registerRules(builtinRules);
|
|
99
|
+
const schemaRules = engine.getRulesByCategory('schema');
|
|
100
|
+
expect(schemaRules.length).toBeGreaterThan(0);
|
|
101
|
+
expect(schemaRules.every((r) => r.category === 'schema')).toBe(true);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('linting', () => {
|
|
106
|
+
const validConfig: GraphConfiguration = {
|
|
107
|
+
metadata: {
|
|
108
|
+
name: 'Test Config',
|
|
109
|
+
version: '1.0.0',
|
|
110
|
+
},
|
|
111
|
+
nodeTypes: {
|
|
112
|
+
service: {
|
|
113
|
+
shape: 'rectangle',
|
|
114
|
+
dataSchema: {
|
|
115
|
+
name: { type: 'string' },
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
edgeTypes: {
|
|
120
|
+
calls: {
|
|
121
|
+
style: 'solid',
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
allowedConnections: [
|
|
125
|
+
{ from: 'service', to: 'service', via: 'calls' },
|
|
126
|
+
],
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
it('should return no violations for valid config', async () => {
|
|
130
|
+
engine.registerRules(builtinRules);
|
|
131
|
+
|
|
132
|
+
// Add sources to avoid minimum-node-sources violation
|
|
133
|
+
const configWithSources = {
|
|
134
|
+
...validConfig,
|
|
135
|
+
nodeTypes: {
|
|
136
|
+
service: {
|
|
137
|
+
...validConfig.nodeTypes.service,
|
|
138
|
+
sources: ['lib/service.ts'],
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const result = await engine.lint(configWithSources);
|
|
144
|
+
expect(result.errorCount).toBe(0);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should detect missing metadata', async () => {
|
|
148
|
+
engine.registerRules(builtinRules);
|
|
149
|
+
|
|
150
|
+
const invalidConfig = {
|
|
151
|
+
nodeTypes: validConfig.nodeTypes,
|
|
152
|
+
edgeTypes: validConfig.edgeTypes,
|
|
153
|
+
allowedConnections: validConfig.allowedConnections,
|
|
154
|
+
} as GraphConfiguration;
|
|
155
|
+
|
|
156
|
+
const result = await engine.lint(invalidConfig);
|
|
157
|
+
expect(result.violations.some((v) => v.ruleId === 'required-metadata')).toBe(true);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should detect invalid color format', async () => {
|
|
161
|
+
engine.registerRules(builtinRules);
|
|
162
|
+
|
|
163
|
+
const invalidConfig: GraphConfiguration = {
|
|
164
|
+
...validConfig,
|
|
165
|
+
nodeTypes: {
|
|
166
|
+
service: {
|
|
167
|
+
...validConfig.nodeTypes.service,
|
|
168
|
+
color: 'blue', // Invalid - should be hex
|
|
169
|
+
sources: ['lib/service.ts'],
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const result = await engine.lint(invalidConfig);
|
|
175
|
+
const colorViolation = result.violations.find((v) => v.ruleId === 'valid-color-format');
|
|
176
|
+
expect(colorViolation).toBeDefined();
|
|
177
|
+
expect(colorViolation?.message).toContain('blue');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('should respect disabled rules', async () => {
|
|
181
|
+
engine.registerRules(builtinRules);
|
|
182
|
+
|
|
183
|
+
const invalidConfig: GraphConfiguration = {
|
|
184
|
+
...validConfig,
|
|
185
|
+
nodeTypes: {
|
|
186
|
+
service: {
|
|
187
|
+
...validConfig.nodeTypes.service,
|
|
188
|
+
color: 'invalid',
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const result = await engine.lint(invalidConfig, {
|
|
194
|
+
disabledRules: ['valid-color-format'],
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
expect(result.violations.some((v) => v.ruleId === 'valid-color-format')).toBe(false);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('should respect enabled rules filter', async () => {
|
|
201
|
+
engine.registerRules(builtinRules);
|
|
202
|
+
|
|
203
|
+
const result = await engine.lint(validConfig, {
|
|
204
|
+
enabledRules: ['required-metadata'],
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// Should only have violations from required-metadata (or none if valid)
|
|
208
|
+
expect(result.violations.every((v) => v.ruleId === 'required-metadata')).toBe(true);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('should apply severity overrides', async () => {
|
|
212
|
+
engine.registerRules(builtinRules);
|
|
213
|
+
|
|
214
|
+
const invalidConfig: GraphConfiguration = {
|
|
215
|
+
...validConfig,
|
|
216
|
+
nodeTypes: {
|
|
217
|
+
service: {
|
|
218
|
+
...validConfig.nodeTypes.service,
|
|
219
|
+
color: 'invalid',
|
|
220
|
+
sources: ['lib/service.ts'],
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const result = await engine.lint(invalidConfig, {
|
|
226
|
+
severityOverrides: new Map([['valid-color-format', 'warn']]),
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
const colorViolation = result.violations.find((v) => v.ruleId === 'valid-color-format');
|
|
230
|
+
expect(colorViolation?.severity).toBe('warn');
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('should count violations correctly', async () => {
|
|
234
|
+
engine.registerRules(builtinRules);
|
|
235
|
+
|
|
236
|
+
const invalidConfig: GraphConfiguration = {
|
|
237
|
+
metadata: { name: 'Test', version: '1.0.0' },
|
|
238
|
+
nodeTypes: {
|
|
239
|
+
service: {
|
|
240
|
+
shape: 'rectangle',
|
|
241
|
+
color: 'invalid1',
|
|
242
|
+
dataSchema: {},
|
|
243
|
+
},
|
|
244
|
+
database: {
|
|
245
|
+
shape: 'rectangle',
|
|
246
|
+
color: 'invalid2',
|
|
247
|
+
dataSchema: {},
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
edgeTypes: {
|
|
251
|
+
calls: { style: 'solid' },
|
|
252
|
+
},
|
|
253
|
+
allowedConnections: [{ from: 'service', to: 'database', via: 'calls' }],
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const result = await engine.lint(invalidConfig);
|
|
257
|
+
expect(result.errorCount).toBeGreaterThan(0);
|
|
258
|
+
expect(result.violations.length).toBe(
|
|
259
|
+
result.errorCount + result.warningCount
|
|
260
|
+
);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('should handle rule execution errors gracefully', async () => {
|
|
264
|
+
const errorRule: GraphRule = {
|
|
265
|
+
id: 'error-rule',
|
|
266
|
+
name: 'Error Rule',
|
|
267
|
+
description: 'A rule that throws',
|
|
268
|
+
impact: 'Test',
|
|
269
|
+
severity: 'error',
|
|
270
|
+
category: 'schema',
|
|
271
|
+
enabled: true,
|
|
272
|
+
fixable: false,
|
|
273
|
+
check: async () => {
|
|
274
|
+
throw new Error('Test error');
|
|
275
|
+
},
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
engine.registerRule(errorRule);
|
|
279
|
+
|
|
280
|
+
const result = await engine.lint(validConfig);
|
|
281
|
+
const errorViolation = result.violations.find((v) => v.ruleId === 'error-rule');
|
|
282
|
+
expect(errorViolation).toBeDefined();
|
|
283
|
+
expect(errorViolation?.message).toContain('Test error');
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
describe('createRulesEngine', () => {
|
|
288
|
+
it('should create engine with provided rules', () => {
|
|
289
|
+
const rules: GraphRule[] = [
|
|
290
|
+
{
|
|
291
|
+
id: 'test-rule',
|
|
292
|
+
name: 'Test',
|
|
293
|
+
description: 'Test',
|
|
294
|
+
impact: 'Test',
|
|
295
|
+
severity: 'error',
|
|
296
|
+
category: 'schema',
|
|
297
|
+
enabled: true,
|
|
298
|
+
fixable: false,
|
|
299
|
+
check: async () => [],
|
|
300
|
+
},
|
|
301
|
+
];
|
|
302
|
+
|
|
303
|
+
const engine = createRulesEngine(rules);
|
|
304
|
+
expect(engine.getAllRules().size).toBe(1);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('should create empty engine when no rules provided', () => {
|
|
308
|
+
const engine = createRulesEngine();
|
|
309
|
+
expect(engine.getAllRules().size).toBe(0);
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
describe('builtinRules', () => {
|
|
315
|
+
it('should have 14 rules', () => {
|
|
316
|
+
expect(builtinRules.length).toBe(14);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('should have unique IDs', () => {
|
|
320
|
+
const ids = builtinRules.map((r) => r.id);
|
|
321
|
+
const uniqueIds = new Set(ids);
|
|
322
|
+
expect(uniqueIds.size).toBe(ids.length);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('should all default to error severity', () => {
|
|
326
|
+
expect(builtinRules.every((r) => r.severity === 'error')).toBe(true);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('should all be enabled by default', () => {
|
|
330
|
+
expect(builtinRules.every((r) => r.enabled === true)).toBe(true);
|
|
331
|
+
});
|
|
332
|
+
});
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Graph Rules Engine
|
|
3
|
+
* Central orchestrator for configuration validation rules
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { GraphConfiguration } from '../types';
|
|
7
|
+
import type { ComponentLibrary } from '../types/library';
|
|
8
|
+
import type {
|
|
9
|
+
GraphRule,
|
|
10
|
+
GraphRuleContext,
|
|
11
|
+
GraphRuleViolation,
|
|
12
|
+
GraphLintResult,
|
|
13
|
+
LintOptions,
|
|
14
|
+
RuleOptions,
|
|
15
|
+
NormalizedSeverity,
|
|
16
|
+
RuleCategory,
|
|
17
|
+
PrivuConfig,
|
|
18
|
+
RuleConfig,
|
|
19
|
+
RuleSeverity,
|
|
20
|
+
} from './types';
|
|
21
|
+
import { normalizeSeverity, isRuleDisabled } from './types';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Graph Rules Engine - validates GraphConfiguration against registered rules
|
|
25
|
+
*/
|
|
26
|
+
export class GraphRulesEngine {
|
|
27
|
+
private rules: Map<string, GraphRule> = new Map();
|
|
28
|
+
|
|
29
|
+
constructor() {
|
|
30
|
+
// Rules are registered via registerRule() or registerBuiltinRules()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Register a single rule
|
|
35
|
+
*/
|
|
36
|
+
registerRule(rule: GraphRule): void {
|
|
37
|
+
if (this.rules.has(rule.id)) {
|
|
38
|
+
throw new Error(`Rule "${rule.id}" is already registered`);
|
|
39
|
+
}
|
|
40
|
+
this.rules.set(rule.id, rule);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Register multiple rules at once
|
|
45
|
+
*/
|
|
46
|
+
registerRules(rules: GraphRule[]): void {
|
|
47
|
+
for (const rule of rules) {
|
|
48
|
+
this.registerRule(rule);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Unregister a rule by ID
|
|
54
|
+
*/
|
|
55
|
+
unregisterRule(ruleId: string): boolean {
|
|
56
|
+
return this.rules.delete(ruleId);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get a rule by ID
|
|
61
|
+
*/
|
|
62
|
+
getRule(ruleId: string): GraphRule | undefined {
|
|
63
|
+
return this.rules.get(ruleId);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get all registered rules
|
|
68
|
+
*/
|
|
69
|
+
getAllRules(): Map<string, GraphRule> {
|
|
70
|
+
return new Map(this.rules);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Get rules by category
|
|
75
|
+
*/
|
|
76
|
+
getRulesByCategory(category: RuleCategory): GraphRule[] {
|
|
77
|
+
return Array.from(this.rules.values()).filter((rule) => rule.category === category);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Lint a configuration against all enabled rules
|
|
82
|
+
*/
|
|
83
|
+
async lint(configuration: GraphConfiguration, options?: LintOptions): Promise<GraphLintResult> {
|
|
84
|
+
const violations: GraphRuleViolation[] = [];
|
|
85
|
+
|
|
86
|
+
for (const [ruleId, rule] of this.rules) {
|
|
87
|
+
// Check if rule should be skipped
|
|
88
|
+
if (!this.shouldRunRule(rule, options)) {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Build context for this rule
|
|
93
|
+
const context = this.buildRuleContext(configuration, rule, options);
|
|
94
|
+
|
|
95
|
+
// Get effective severity
|
|
96
|
+
const effectiveSeverity = this.getEffectiveSeverity(rule, options);
|
|
97
|
+
if (effectiveSeverity === 'off') {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
// Execute rule check
|
|
103
|
+
const ruleViolations = await rule.check(context);
|
|
104
|
+
|
|
105
|
+
// Apply effective severity to all violations
|
|
106
|
+
for (const violation of ruleViolations) {
|
|
107
|
+
violation.severity = effectiveSeverity;
|
|
108
|
+
violations.push(violation);
|
|
109
|
+
}
|
|
110
|
+
} catch (error) {
|
|
111
|
+
// Rule execution error - report as a violation
|
|
112
|
+
violations.push({
|
|
113
|
+
ruleId,
|
|
114
|
+
severity: 'error',
|
|
115
|
+
message: `Rule "${ruleId}" threw an error: ${error instanceof Error ? error.message : String(error)}`,
|
|
116
|
+
impact: 'Rule could not complete validation',
|
|
117
|
+
fixable: false,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return this.buildResult(violations);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Lint a configuration with VGC config file settings applied
|
|
127
|
+
*/
|
|
128
|
+
async lintWithConfig(
|
|
129
|
+
configuration: GraphConfiguration,
|
|
130
|
+
privuConfig: PrivuConfig,
|
|
131
|
+
options?: Omit<LintOptions, 'ruleOptions' | 'severityOverrides'>
|
|
132
|
+
): Promise<GraphLintResult> {
|
|
133
|
+
// Convert VGC config rules to lint options
|
|
134
|
+
const { ruleOptions, severityOverrides, disabledRules } = this.parsePrivuConfigRules(
|
|
135
|
+
privuConfig.rules
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
return this.lint(configuration, {
|
|
139
|
+
...options,
|
|
140
|
+
ruleOptions,
|
|
141
|
+
severityOverrides,
|
|
142
|
+
disabledRules: [...(options?.disabledRules ?? []), ...disabledRules],
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Check if a rule should run based on options
|
|
148
|
+
*/
|
|
149
|
+
private shouldRunRule(rule: GraphRule, options?: LintOptions): boolean {
|
|
150
|
+
// Check disabled rules list
|
|
151
|
+
if (options?.disabledRules?.includes(rule.id)) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Check enabled rules list (if specified, only run those)
|
|
156
|
+
if (options?.enabledRules && options.enabledRules.length > 0) {
|
|
157
|
+
return options.enabledRules.includes(rule.id);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Check rule's default enabled state
|
|
161
|
+
return rule.enabled;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Get the effective severity for a rule
|
|
166
|
+
*/
|
|
167
|
+
private getEffectiveSeverity(rule: GraphRule, options?: LintOptions): NormalizedSeverity {
|
|
168
|
+
// Check for override
|
|
169
|
+
if (options?.severityOverrides?.has(rule.id)) {
|
|
170
|
+
return options.severityOverrides.get(rule.id)!;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Use rule's default severity
|
|
174
|
+
return rule.severity;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Build the context object for a rule
|
|
179
|
+
*/
|
|
180
|
+
private buildRuleContext<TOptions extends RuleOptions>(
|
|
181
|
+
configuration: GraphConfiguration,
|
|
182
|
+
rule: GraphRule<TOptions>,
|
|
183
|
+
options?: LintOptions
|
|
184
|
+
): GraphRuleContext<TOptions> {
|
|
185
|
+
// Merge default options with provided options
|
|
186
|
+
const ruleSpecificOptions = options?.ruleOptions?.get(rule.id) as TOptions | undefined;
|
|
187
|
+
const mergedOptions = {
|
|
188
|
+
...rule.defaultOptions,
|
|
189
|
+
...ruleSpecificOptions,
|
|
190
|
+
} as TOptions;
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
configuration,
|
|
194
|
+
library: options?.library,
|
|
195
|
+
configPath: options?.configPath,
|
|
196
|
+
libraryPath: options?.libraryPath,
|
|
197
|
+
rawContent: options?.rawContent,
|
|
198
|
+
options: mergedOptions,
|
|
199
|
+
allRuleOptions: options?.ruleOptions,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Build the final lint result from violations
|
|
205
|
+
*/
|
|
206
|
+
private buildResult(violations: GraphRuleViolation[]): GraphLintResult {
|
|
207
|
+
const byCategory: Record<RuleCategory, number> = {
|
|
208
|
+
schema: 0,
|
|
209
|
+
reference: 0,
|
|
210
|
+
structure: 0,
|
|
211
|
+
pattern: 0,
|
|
212
|
+
library: 0,
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const byRule: Record<string, number> = {};
|
|
216
|
+
|
|
217
|
+
let errorCount = 0;
|
|
218
|
+
let warningCount = 0;
|
|
219
|
+
let fixableCount = 0;
|
|
220
|
+
|
|
221
|
+
for (const violation of violations) {
|
|
222
|
+
// Count by severity
|
|
223
|
+
if (violation.severity === 'error') {
|
|
224
|
+
errorCount++;
|
|
225
|
+
} else if (violation.severity === 'warn') {
|
|
226
|
+
warningCount++;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Count fixable
|
|
230
|
+
if (violation.fixable) {
|
|
231
|
+
fixableCount++;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Count by rule
|
|
235
|
+
byRule[violation.ruleId] = (byRule[violation.ruleId] ?? 0) + 1;
|
|
236
|
+
|
|
237
|
+
// Count by category
|
|
238
|
+
const rule = this.rules.get(violation.ruleId);
|
|
239
|
+
if (rule) {
|
|
240
|
+
byCategory[rule.category]++;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
violations,
|
|
246
|
+
errorCount,
|
|
247
|
+
warningCount,
|
|
248
|
+
fixableCount,
|
|
249
|
+
byCategory,
|
|
250
|
+
byRule,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Parse VGC config rules into lint options
|
|
256
|
+
*/
|
|
257
|
+
private parsePrivuConfigRules(rules?: PrivuConfig['rules']): {
|
|
258
|
+
ruleOptions: Map<string, RuleOptions>;
|
|
259
|
+
severityOverrides: Map<string, NormalizedSeverity>;
|
|
260
|
+
disabledRules: string[];
|
|
261
|
+
} {
|
|
262
|
+
const ruleOptions = new Map<string, RuleOptions>();
|
|
263
|
+
const severityOverrides = new Map<string, NormalizedSeverity>();
|
|
264
|
+
const disabledRules: string[] = [];
|
|
265
|
+
|
|
266
|
+
if (!rules) {
|
|
267
|
+
return { ruleOptions, severityOverrides, disabledRules };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
for (const [ruleId, config] of Object.entries(rules)) {
|
|
271
|
+
// Handle false (disabled)
|
|
272
|
+
if (config === false) {
|
|
273
|
+
disabledRules.push(ruleId);
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Handle severity shorthand
|
|
278
|
+
if (typeof config === 'string' || typeof config === 'number') {
|
|
279
|
+
const severity = normalizeSeverity(config as RuleSeverity);
|
|
280
|
+
if (severity === 'off') {
|
|
281
|
+
disabledRules.push(ruleId);
|
|
282
|
+
} else {
|
|
283
|
+
severityOverrides.set(ruleId, severity);
|
|
284
|
+
}
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Handle full config object
|
|
289
|
+
const ruleConfig = config as RuleConfig;
|
|
290
|
+
|
|
291
|
+
if (ruleConfig.severity !== undefined) {
|
|
292
|
+
const severity = normalizeSeverity(ruleConfig.severity);
|
|
293
|
+
if (severity === 'off') {
|
|
294
|
+
disabledRules.push(ruleId);
|
|
295
|
+
} else {
|
|
296
|
+
severityOverrides.set(ruleId, severity);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (ruleConfig.options) {
|
|
301
|
+
ruleOptions.set(ruleId, ruleConfig.options);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return { ruleOptions, severityOverrides, disabledRules };
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Create a new rules engine with optional initial rules
|
|
311
|
+
*/
|
|
312
|
+
export function createRulesEngine(rules?: GraphRule[]): GraphRulesEngine {
|
|
313
|
+
const engine = new GraphRulesEngine();
|
|
314
|
+
if (rules) {
|
|
315
|
+
engine.registerRules(rules);
|
|
316
|
+
}
|
|
317
|
+
return engine;
|
|
318
|
+
}
|