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