@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,185 @@
1
+ /**
2
+ * Configuration loader for managing multiple graph configurations
3
+ *
4
+ * Supports loading configurations from the .vgc/ folder using the adapter pattern
5
+ * for environment-agnostic file operations.
6
+ */
7
+
8
+ import type { FileSystemAdapter } from '@principal-ai/repository-abstraction';
9
+ import type { PathBasedGraphConfiguration } from './types/path-based-config';
10
+ import { parseYaml, isYamlFile, getConfigNameFromFilename } from './utils/YamlParser';
11
+
12
+ /**
13
+ * Represents a single configuration file
14
+ */
15
+ export interface ConfigurationFile {
16
+ /** Configuration name (filename without extension) */
17
+ name: string;
18
+ /** Full path to the configuration file */
19
+ path: string;
20
+ /** Parsed configuration data */
21
+ config: PathBasedGraphConfiguration;
22
+ }
23
+
24
+ /**
25
+ * Result of loading all configurations
26
+ */
27
+ export interface ConfigurationLoadResult {
28
+ /** Successfully loaded configurations */
29
+ configs: ConfigurationFile[];
30
+ /** Errors encountered during loading */
31
+ errors: Array<{ file: string; error: string }>;
32
+ }
33
+
34
+ /**
35
+ * Loader for managing multiple graph configurations from .vgc/ folder
36
+ */
37
+ export class ConfigurationLoader {
38
+ private static readonly CONFIG_DIR = '.vgc';
39
+
40
+ constructor(private fsAdapter: FileSystemAdapter) {}
41
+
42
+ /**
43
+ * Check if the .vgc/ configuration directory exists
44
+ *
45
+ * @param baseDir - Base directory to search from
46
+ * @returns True if .vgc/ directory exists
47
+ */
48
+ hasConfigDirectory(baseDir: string): boolean {
49
+ const configPath = this.fsAdapter.join(baseDir, ConfigurationLoader.CONFIG_DIR);
50
+ return this.fsAdapter.exists(configPath) && this.fsAdapter.isDirectory(configPath);
51
+ }
52
+
53
+ /**
54
+ * List all available configuration names in .vgc/ folder
55
+ *
56
+ * @param baseDir - Base directory containing .vgc/ folder
57
+ * @returns Array of configuration names (without extensions)
58
+ */
59
+ listConfigurations(baseDir: string): string[] {
60
+ if (!this.hasConfigDirectory(baseDir)) {
61
+ return [];
62
+ }
63
+
64
+ const configPath = this.fsAdapter.join(baseDir, ConfigurationLoader.CONFIG_DIR);
65
+ const files = this.fsAdapter.readDir(configPath);
66
+
67
+ return files
68
+ .filter(isYamlFile)
69
+ .map(getConfigNameFromFilename)
70
+ .sort();
71
+ }
72
+
73
+ /**
74
+ * Load a specific configuration by name
75
+ *
76
+ * @param name - Configuration name (without extension)
77
+ * @param baseDir - Base directory containing .vgc/ folder
78
+ * @returns Configuration file or null if not found/invalid
79
+ */
80
+ loadByName(name: string, baseDir: string): ConfigurationFile | null {
81
+ if (!this.hasConfigDirectory(baseDir)) {
82
+ return null;
83
+ }
84
+
85
+ const configPath = this.fsAdapter.join(baseDir, ConfigurationLoader.CONFIG_DIR);
86
+
87
+ // Try both .yaml and .yml extensions
88
+ for (const ext of ['yaml', 'yml']) {
89
+ const filename = `${name}.${ext}`;
90
+ const fullPath = this.fsAdapter.join(configPath, filename);
91
+
92
+ if (this.fsAdapter.exists(fullPath)) {
93
+ try {
94
+ const content = this.fsAdapter.readFile(fullPath);
95
+ const parseResult = parseYaml(content, filename);
96
+
97
+ if (parseResult.success && parseResult.data) {
98
+ return {
99
+ name,
100
+ path: fullPath,
101
+ config: parseResult.data,
102
+ };
103
+ }
104
+ } catch (error) {
105
+ // File exists but couldn't be read or parsed
106
+ return null;
107
+ }
108
+ }
109
+ }
110
+
111
+ return null;
112
+ }
113
+
114
+ /**
115
+ * Load all configurations from .vgc/ folder
116
+ *
117
+ * @param baseDir - Base directory containing .vgc/ folder
118
+ * @returns Result containing all loaded configs and any errors
119
+ */
120
+ loadAll(baseDir: string): ConfigurationLoadResult {
121
+ const result: ConfigurationLoadResult = {
122
+ configs: [],
123
+ errors: [],
124
+ };
125
+
126
+ if (!this.hasConfigDirectory(baseDir)) {
127
+ result.errors.push({
128
+ file: '.vgc',
129
+ error: 'Configuration directory .vgc/ not found',
130
+ });
131
+ return result;
132
+ }
133
+
134
+ const configPath = this.fsAdapter.join(baseDir, ConfigurationLoader.CONFIG_DIR);
135
+ const files = this.fsAdapter.readDir(configPath);
136
+
137
+ for (const filename of files) {
138
+ if (!isYamlFile(filename)) {
139
+ continue; // Skip non-YAML files
140
+ }
141
+
142
+ const fullPath = this.fsAdapter.join(configPath, filename);
143
+ const name = getConfigNameFromFilename(filename);
144
+
145
+ try {
146
+ const content = this.fsAdapter.readFile(fullPath);
147
+ const parseResult = parseYaml(content, filename);
148
+
149
+ if (parseResult.success && parseResult.data) {
150
+ result.configs.push({
151
+ name,
152
+ path: fullPath,
153
+ config: parseResult.data,
154
+ });
155
+ } else if (parseResult.error) {
156
+ result.errors.push({
157
+ file: filename,
158
+ error: parseResult.error,
159
+ });
160
+ }
161
+ } catch (error) {
162
+ const errorMessage = error instanceof Error ? error.message : String(error);
163
+ result.errors.push({
164
+ file: filename,
165
+ error: `Failed to read file: ${errorMessage}`,
166
+ });
167
+ }
168
+ }
169
+
170
+ // Sort configs by name for consistent ordering
171
+ result.configs.sort((a, b) => a.name.localeCompare(b.name));
172
+
173
+ return result;
174
+ }
175
+
176
+ /**
177
+ * Get the configuration directory path
178
+ *
179
+ * @param baseDir - Base directory
180
+ * @returns Full path to .vgc/ directory
181
+ */
182
+ getConfigDirectoryPath(baseDir: string): string {
183
+ return this.fsAdapter.join(baseDir, ConfigurationLoader.CONFIG_DIR);
184
+ }
185
+ }
@@ -0,0 +1,200 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { ConfigurationValidator } from './ConfigurationValidator';
3
+ import type { GraphConfiguration } from './types';
4
+
5
+ describe('ConfigurationValidator', () => {
6
+ test('validates a correct configuration', () => {
7
+ const validConfig: GraphConfiguration = {
8
+ metadata: {
9
+ name: 'Test Graph',
10
+ version: '1.0.0',
11
+ },
12
+ nodeTypes: {
13
+ process: {
14
+ shape: 'rectangle',
15
+ color: '#4A90E2',
16
+ dataSchema: {
17
+ name: { type: 'string', required: true },
18
+ },
19
+ },
20
+ },
21
+ edgeTypes: {
22
+ dataflow: {
23
+ style: 'solid',
24
+ color: '#999',
25
+ },
26
+ },
27
+ allowedConnections: [
28
+ {
29
+ from: 'process',
30
+ to: 'process',
31
+ via: 'dataflow',
32
+ },
33
+ ],
34
+ };
35
+
36
+ const result = ConfigurationValidator.validate(validConfig);
37
+ expect(result.valid).toBe(true);
38
+ expect(result.errors).toHaveLength(0);
39
+ });
40
+
41
+ test('catches missing node type in connection rules', () => {
42
+ const invalidConfig: GraphConfiguration = {
43
+ metadata: {
44
+ name: 'Test Graph',
45
+ version: '1.0.0',
46
+ },
47
+ nodeTypes: {
48
+ process: {
49
+ shape: 'rectangle',
50
+ dataSchema: {},
51
+ },
52
+ },
53
+ edgeTypes: {
54
+ dataflow: {
55
+ style: 'solid',
56
+ },
57
+ },
58
+ allowedConnections: [
59
+ {
60
+ from: 'process',
61
+ to: 'nonexistent', // āŒ This doesn't exist
62
+ via: 'dataflow',
63
+ },
64
+ ],
65
+ };
66
+
67
+ const result = ConfigurationValidator.validate(invalidConfig);
68
+ expect(result.valid).toBe(false);
69
+ expect(result.errors.some(e => e.message.includes('nonexistent'))).toBe(true);
70
+ });
71
+
72
+ test('catches missing edge type in connection rules', () => {
73
+ const invalidConfig: GraphConfiguration = {
74
+ metadata: {
75
+ name: 'Test Graph',
76
+ version: '1.0.0',
77
+ },
78
+ nodeTypes: {
79
+ process: {
80
+ shape: 'rectangle',
81
+ dataSchema: {},
82
+ },
83
+ },
84
+ edgeTypes: {
85
+ dataflow: {
86
+ style: 'solid',
87
+ },
88
+ },
89
+ allowedConnections: [
90
+ {
91
+ from: 'process',
92
+ to: 'process',
93
+ via: 'nonexistent_edge', // āŒ This doesn't exist
94
+ },
95
+ ],
96
+ };
97
+
98
+ const result = ConfigurationValidator.validate(invalidConfig);
99
+ expect(result.valid).toBe(false);
100
+ expect(result.errors.some(e => e.message.includes('nonexistent_edge'))).toBe(true);
101
+ });
102
+
103
+ test('catches missing metadata', () => {
104
+ const invalidConfig = {
105
+ nodeTypes: {
106
+ process: { shape: 'rectangle', dataSchema: {} },
107
+ },
108
+ edgeTypes: {
109
+ dataflow: { style: 'solid' },
110
+ },
111
+ allowedConnections: [],
112
+ } as unknown as GraphConfiguration;
113
+
114
+ const result = ConfigurationValidator.validate(invalidConfig);
115
+ expect(result.valid).toBe(false);
116
+ expect(result.errors.some(e => e.path.includes('metadata'))).toBe(true);
117
+ });
118
+
119
+ test('warns about undefined states in state transitions', () => {
120
+ const configWithBadStates: GraphConfiguration = {
121
+ metadata: {
122
+ name: 'Test Graph',
123
+ version: '1.0.0',
124
+ },
125
+ nodeTypes: {
126
+ order: {
127
+ shape: 'rectangle',
128
+ dataSchema: {},
129
+ states: {
130
+ pending: { color: '#FFA500' },
131
+ shipped: { color: '#00FF00' },
132
+ },
133
+ },
134
+ },
135
+ edgeTypes: {
136
+ flow: { style: 'solid' },
137
+ },
138
+ allowedConnections: [],
139
+ validation: {
140
+ stateTransitions: {
141
+ order: [
142
+ {
143
+ from: 'pending',
144
+ to: ['shipped', 'nonexistent'], // āŒ nonexistent state
145
+ },
146
+ ],
147
+ },
148
+ },
149
+ };
150
+
151
+ const result = ConfigurationValidator.validate(configWithBadStates);
152
+ expect(result.warnings.some(w => w.message.includes('nonexistent'))).toBe(true);
153
+ });
154
+
155
+ test('validateOrThrow throws on invalid config', () => {
156
+ const invalidConfig: GraphConfiguration = {
157
+ metadata: {
158
+ name: 'Test',
159
+ version: '1.0.0',
160
+ },
161
+ nodeTypes: {}, // Empty!
162
+ edgeTypes: {
163
+ flow: { style: 'solid' },
164
+ },
165
+ allowedConnections: [],
166
+ };
167
+
168
+ expect(() => {
169
+ ConfigurationValidator.validateOrThrow(invalidConfig);
170
+ }).toThrow('Invalid GraphConfiguration');
171
+ });
172
+
173
+ test('formatReport creates readable output', () => {
174
+ const result = {
175
+ valid: false,
176
+ errors: [
177
+ {
178
+ type: 'error' as const,
179
+ message: 'Node type not found',
180
+ path: 'nodeTypes.missing',
181
+ suggestion: 'Add the node type definition',
182
+ },
183
+ ],
184
+ warnings: [
185
+ {
186
+ type: 'warning' as const,
187
+ message: 'No data schema',
188
+ path: 'nodeTypes.process.dataSchema',
189
+ },
190
+ ],
191
+ };
192
+
193
+ const report = ConfigurationValidator.formatReport(result);
194
+ expect(report).toContain('āŒ');
195
+ expect(report).toContain('Node type not found');
196
+ expect(report).toContain('āš ļø');
197
+ expect(report).toContain('No data schema');
198
+ expect(report).toContain('šŸ’”');
199
+ });
200
+ });
@@ -0,0 +1,283 @@
1
+ import type { GraphConfiguration } from './types';
2
+
3
+ export interface ConfigurationValidationError {
4
+ type: 'error' | 'warning';
5
+ message: string;
6
+ path: string;
7
+ suggestion?: string;
8
+ }
9
+
10
+ export interface ConfigurationValidationResult {
11
+ valid: boolean;
12
+ errors: ConfigurationValidationError[];
13
+ warnings: ConfigurationValidationError[];
14
+ }
15
+
16
+ /**
17
+ * Validates GraphConfiguration to catch issues early
18
+ * Use this before creating EventProcessor or rendering graphs
19
+ */
20
+ export class ConfigurationValidator {
21
+ /**
22
+ * Validate a GraphConfiguration
23
+ */
24
+ static validate(config: GraphConfiguration): ConfigurationValidationResult {
25
+ const errors: ConfigurationValidationError[] = [];
26
+ const warnings: ConfigurationValidationError[] = [];
27
+
28
+ // Validate metadata
29
+ if (!config.metadata?.name) {
30
+ errors.push({
31
+ type: 'error',
32
+ message: 'Configuration must have a name',
33
+ path: 'metadata.name',
34
+ });
35
+ }
36
+
37
+ if (!config.metadata?.version) {
38
+ errors.push({
39
+ type: 'error',
40
+ message: 'Configuration must have a version',
41
+ path: 'metadata.version',
42
+ });
43
+ }
44
+
45
+ // Validate nodeTypes
46
+ if (!config.nodeTypes || Object.keys(config.nodeTypes).length === 0) {
47
+ errors.push({
48
+ type: 'error',
49
+ message: 'Configuration must define at least one node type',
50
+ path: 'nodeTypes',
51
+ });
52
+ } else {
53
+ // Validate each node type
54
+ for (const [typeName, nodeType] of Object.entries(config.nodeTypes)) {
55
+ if (!nodeType.shape) {
56
+ errors.push({
57
+ type: 'error',
58
+ message: `Node type '${typeName}' must have a shape`,
59
+ path: `nodeTypes.${typeName}.shape`,
60
+ });
61
+ }
62
+
63
+ if (!nodeType.dataSchema || Object.keys(nodeType.dataSchema).length === 0) {
64
+ warnings.push({
65
+ type: 'warning',
66
+ message: `Node type '${typeName}' has no data schema defined`,
67
+ path: `nodeTypes.${typeName}.dataSchema`,
68
+ suggestion: 'Consider defining data schema for better type safety',
69
+ });
70
+ }
71
+
72
+ // Validate states if defined
73
+ if (nodeType.states) {
74
+ for (const [stateName, state] of Object.entries(nodeType.states)) {
75
+ if (!state.label && !state.color && !state.icon) {
76
+ warnings.push({
77
+ type: 'warning',
78
+ message: `State '${stateName}' of node type '${typeName}' has no visual properties`,
79
+ path: `nodeTypes.${typeName}.states.${stateName}`,
80
+ suggestion: 'Add at least one of: label, color, or icon',
81
+ });
82
+ }
83
+ }
84
+ }
85
+ }
86
+ }
87
+
88
+ // Validate edgeTypes
89
+ if (!config.edgeTypes || Object.keys(config.edgeTypes).length === 0) {
90
+ errors.push({
91
+ type: 'error',
92
+ message: 'Configuration must define at least one edge type',
93
+ path: 'edgeTypes',
94
+ });
95
+ } else {
96
+ // Validate each edge type
97
+ for (const [typeName, edgeType] of Object.entries(config.edgeTypes)) {
98
+ if (!edgeType.style) {
99
+ errors.push({
100
+ type: 'error',
101
+ message: `Edge type '${typeName}' must have a style`,
102
+ path: `edgeTypes.${typeName}.style`,
103
+ });
104
+ }
105
+ }
106
+ }
107
+
108
+ // Validate allowedConnections
109
+ if (!config.allowedConnections || config.allowedConnections.length === 0) {
110
+ warnings.push({
111
+ type: 'warning',
112
+ message: 'No connection rules defined',
113
+ path: 'allowedConnections',
114
+ suggestion: 'Define connection rules to validate graph structure',
115
+ });
116
+ } else {
117
+ const nodeTypeNames = Object.keys(config.nodeTypes);
118
+ const edgeTypeNames = Object.keys(config.edgeTypes);
119
+
120
+ config.allowedConnections.forEach((rule, index) => {
121
+ // Check if 'from' node type exists
122
+ if (!nodeTypeNames.includes(rule.from)) {
123
+ errors.push({
124
+ type: 'error',
125
+ message: `Connection rule references undefined node type '${rule.from}'`,
126
+ path: `allowedConnections[${index}].from`,
127
+ suggestion: `Available node types: ${nodeTypeNames.join(', ')}`,
128
+ });
129
+ }
130
+
131
+ // Check if 'to' node type exists
132
+ if (!nodeTypeNames.includes(rule.to)) {
133
+ errors.push({
134
+ type: 'error',
135
+ message: `Connection rule references undefined node type '${rule.to}'`,
136
+ path: `allowedConnections[${index}].to`,
137
+ suggestion: `Available node types: ${nodeTypeNames.join(', ')}`,
138
+ });
139
+ }
140
+
141
+ // Check if 'via' edge type exists
142
+ if (!edgeTypeNames.includes(rule.via)) {
143
+ errors.push({
144
+ type: 'error',
145
+ message: `Connection rule references undefined edge type '${rule.via}'`,
146
+ path: `allowedConnections[${index}].via`,
147
+ suggestion: `Available edge types: ${edgeTypeNames.join(', ')}`,
148
+ });
149
+ }
150
+ });
151
+ }
152
+
153
+ // Validate validation rules if present
154
+ if (config.validation) {
155
+ // Validate state transitions
156
+ if (config.validation.stateTransitions) {
157
+ const nodeTypeNames = Object.keys(config.nodeTypes);
158
+
159
+ for (const [nodeType, transitions] of Object.entries(config.validation.stateTransitions)) {
160
+ if (!nodeTypeNames.includes(nodeType)) {
161
+ errors.push({
162
+ type: 'error',
163
+ message: `State transition rule references undefined node type '${nodeType}'`,
164
+ path: `validation.stateTransitions.${nodeType}`,
165
+ suggestion: `Available node types: ${nodeTypeNames.join(', ')}`,
166
+ });
167
+ } else {
168
+ // Check if states exist in node type definition
169
+ const nodeTypeDef = config.nodeTypes[nodeType];
170
+ const definedStates = nodeTypeDef.states ? Object.keys(nodeTypeDef.states) : [];
171
+
172
+ transitions.forEach((transition, index) => {
173
+ if (definedStates.length > 0 && !definedStates.includes(transition.from)) {
174
+ warnings.push({
175
+ type: 'warning',
176
+ message: `State transition references undefined state '${transition.from}' for node type '${nodeType}'`,
177
+ path: `validation.stateTransitions.${nodeType}[${index}].from`,
178
+ suggestion: `Defined states: ${definedStates.join(', ')}`,
179
+ });
180
+ }
181
+
182
+ transition.to.forEach((toState) => {
183
+ if (definedStates.length > 0 && !definedStates.includes(toState)) {
184
+ warnings.push({
185
+ type: 'warning',
186
+ message: `State transition references undefined state '${toState}' for node type '${nodeType}'`,
187
+ path: `validation.stateTransitions.${nodeType}[${index}].to`,
188
+ suggestion: `Defined states: ${definedStates.join(', ')}`,
189
+ });
190
+ }
191
+ });
192
+ });
193
+ }
194
+ }
195
+ }
196
+
197
+ // Validate cardinality rules
198
+ if (config.validation.cardinality) {
199
+ const nodeTypeNames = Object.keys(config.nodeTypes);
200
+
201
+ for (const nodeType of Object.keys(config.validation.cardinality)) {
202
+ if (!nodeTypeNames.includes(nodeType)) {
203
+ errors.push({
204
+ type: 'error',
205
+ message: `Cardinality rule references undefined node type '${nodeType}'`,
206
+ path: `validation.cardinality.${nodeType}`,
207
+ suggestion: `Available node types: ${nodeTypeNames.join(', ')}`,
208
+ });
209
+ }
210
+ }
211
+ }
212
+ }
213
+
214
+ return {
215
+ valid: errors.length === 0,
216
+ errors,
217
+ warnings,
218
+ };
219
+ }
220
+
221
+ /**
222
+ * Validate and throw if invalid
223
+ */
224
+ static validateOrThrow(config: GraphConfiguration): void {
225
+ const result = this.validate(config);
226
+
227
+ if (!result.valid) {
228
+ const errorMessages = result.errors.map(
229
+ (e) => ` - ${e.path}: ${e.message}${e.suggestion ? ` (${e.suggestion})` : ''}`
230
+ );
231
+
232
+ throw new Error(
233
+ `Invalid GraphConfiguration:\n${errorMessages.join('\n')}\n\n` +
234
+ `Found ${result.errors.length} error(s) and ${result.warnings.length} warning(s)`
235
+ );
236
+ }
237
+
238
+ // Log warnings
239
+ if (result.warnings.length > 0) {
240
+ console.warn(
241
+ `Configuration has ${result.warnings.length} warning(s):`
242
+ );
243
+ result.warnings.forEach((w) => {
244
+ console.warn(` - ${w.path}: ${w.message}${w.suggestion ? ` (${w.suggestion})` : ''}`);
245
+ });
246
+ }
247
+ }
248
+
249
+ /**
250
+ * Get a formatted report of validation results
251
+ */
252
+ static formatReport(result: ConfigurationValidationResult): string {
253
+ const lines: string[] = [];
254
+
255
+ if (result.valid) {
256
+ lines.push('āœ… Configuration is valid');
257
+ } else {
258
+ lines.push(`āŒ Configuration has ${result.errors.length} error(s)`);
259
+ }
260
+
261
+ if (result.errors.length > 0) {
262
+ lines.push('\nErrors:');
263
+ result.errors.forEach((e) => {
264
+ lines.push(` āŒ ${e.path}: ${e.message}`);
265
+ if (e.suggestion) {
266
+ lines.push(` šŸ’” ${e.suggestion}`);
267
+ }
268
+ });
269
+ }
270
+
271
+ if (result.warnings.length > 0) {
272
+ lines.push(`\nāš ļø ${result.warnings.length} warning(s):`);
273
+ result.warnings.forEach((w) => {
274
+ lines.push(` āš ļø ${w.path}: ${w.message}`);
275
+ if (w.suggestion) {
276
+ lines.push(` šŸ’” ${w.suggestion}`);
277
+ }
278
+ });
279
+ }
280
+
281
+ return lines.join('\n');
282
+ }
283
+ }