@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,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
+ }