dhurandhar 1.0.0
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/.dhurandhar-session-start.md +242 -0
- package/LICENSE +21 -0
- package/README.md +416 -0
- package/docs/ARCHITECTURE_V2.md +249 -0
- package/docs/DECISION_REGISTRY.md +357 -0
- package/docs/IMPLEMENTATION_PERSONAS.md +406 -0
- package/docs/PLUGGABLE_STRATEGIES.md +439 -0
- package/docs/SYSTEM_OBSERVER.md +433 -0
- package/docs/TEST_FIRST_AGILE.md +359 -0
- package/docs/architecture.md +279 -0
- package/docs/engineering-first-philosophy.md +263 -0
- package/docs/getting-started.md +218 -0
- package/docs/module-development.md +323 -0
- package/docs/strategy-example.md +299 -0
- package/docs/test-first-example.md +392 -0
- package/package.json +79 -0
- package/src/core/README.md +92 -0
- package/src/core/agent-instructions/backend-developer.md +412 -0
- package/src/core/agent-instructions/devops-engineer.md +372 -0
- package/src/core/agent-instructions/dhurandhar-council.md +547 -0
- package/src/core/agent-instructions/edge-case-hunter.md +322 -0
- package/src/core/agent-instructions/frontend-developer.md +494 -0
- package/src/core/agent-instructions/lead-system-architect.md +631 -0
- package/src/core/agent-instructions/system-observer.md +319 -0
- package/src/core/agent-instructions/test-architect.md +284 -0
- package/src/core/module.yaml +54 -0
- package/src/core/schemas/design-module-schema.yaml +995 -0
- package/src/core/schemas/system-design-map-schema.yaml +324 -0
- package/src/modules/example/README.md +130 -0
- package/src/modules/example/module.yaml +252 -0
- package/tools/cli/commands/audit.js +267 -0
- package/tools/cli/commands/config.js +113 -0
- package/tools/cli/commands/context.js +170 -0
- package/tools/cli/commands/decisions.js +398 -0
- package/tools/cli/commands/entity.js +218 -0
- package/tools/cli/commands/epic.js +125 -0
- package/tools/cli/commands/install.js +172 -0
- package/tools/cli/commands/module.js +109 -0
- package/tools/cli/commands/service.js +167 -0
- package/tools/cli/commands/story.js +225 -0
- package/tools/cli/commands/strategy.js +294 -0
- package/tools/cli/commands/test.js +277 -0
- package/tools/cli/commands/validate.js +107 -0
- package/tools/cli/dhurandhar.js +212 -0
- package/tools/lib/config-manager.js +170 -0
- package/tools/lib/filesystem.js +126 -0
- package/tools/lib/module-installer.js +61 -0
- package/tools/lib/module-manager.js +149 -0
- package/tools/lib/sdm-manager.js +982 -0
- package/tools/lib/test-engine.js +255 -0
- package/tools/lib/test-templates/api-client.template.js +100 -0
- package/tools/lib/test-templates/vitest.config.template.js +37 -0
- package/tools/lib/validators/config-validator.js +113 -0
- package/tools/lib/validators/module-validator.js +137 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract-First Testing Engine
|
|
3
|
+
* Generates comprehensive test suites BEFORE implementation
|
|
4
|
+
*
|
|
5
|
+
* Philosophy: Tests define the contract, implementation fulfills it
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
import { existsSync, mkdirSync } from 'fs';
|
|
10
|
+
import { writeFile } from 'fs/promises';
|
|
11
|
+
|
|
12
|
+
export class TestEngine {
|
|
13
|
+
constructor(projectRoot) {
|
|
14
|
+
this.projectRoot = projectRoot;
|
|
15
|
+
this.testDir = join(projectRoot, 'tests');
|
|
16
|
+
this.contractsDir = join(this.testDir, 'contracts');
|
|
17
|
+
this.edgeCasesDir = join(this.testDir, 'edge-cases');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Initialize test directory structure
|
|
22
|
+
*/
|
|
23
|
+
async initializeTestStructure() {
|
|
24
|
+
const dirs = [
|
|
25
|
+
this.testDir,
|
|
26
|
+
this.contractsDir,
|
|
27
|
+
this.edgeCasesDir,
|
|
28
|
+
join(this.testDir, 'integration'),
|
|
29
|
+
join(this.testDir, 'utils'),
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
for (const dir of dirs) {
|
|
33
|
+
if (!existsSync(dir)) {
|
|
34
|
+
mkdirSync(dir, { recursive: true });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Generate test suite from Story
|
|
41
|
+
*/
|
|
42
|
+
async generateStoryTests(story, epic) {
|
|
43
|
+
const { interaction_boundary } = story;
|
|
44
|
+
|
|
45
|
+
const tests = {
|
|
46
|
+
standard_flows: this.generateStandardFlowTests(story, interaction_boundary),
|
|
47
|
+
error_states: this.generateErrorStateTests(story, interaction_boundary),
|
|
48
|
+
edge_cases: this.generateEdgeCaseTests(story, interaction_boundary),
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
return tests;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Generate standard flow tests (happy path)
|
|
56
|
+
*/
|
|
57
|
+
generateStandardFlowTests(story, boundary) {
|
|
58
|
+
const testCode = `
|
|
59
|
+
/**
|
|
60
|
+
* Contract Test: ${story.name}
|
|
61
|
+
* Story: ${story.id}
|
|
62
|
+
* Service: ${boundary.service}
|
|
63
|
+
* Endpoint: ${boundary.method} ${boundary.api_endpoint}
|
|
64
|
+
*
|
|
65
|
+
* TEST-FIRST: This test defines the contract BEFORE implementation
|
|
66
|
+
*/
|
|
67
|
+
|
|
68
|
+
import { describe, it, expect, beforeAll } from 'vitest';
|
|
69
|
+
import { apiClient } from '../utils/api-client';
|
|
70
|
+
|
|
71
|
+
describe('${story.name} - Standard Flows', () => {
|
|
72
|
+
let testContext = {};
|
|
73
|
+
|
|
74
|
+
beforeAll(async () => {
|
|
75
|
+
// Setup: Initialize test data
|
|
76
|
+
testContext = await setupTestData();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should handle standard request successfully', async () => {
|
|
80
|
+
// Arrange
|
|
81
|
+
const request = ${JSON.stringify(boundary.request_contract || {}, null, 4)};
|
|
82
|
+
|
|
83
|
+
// Act
|
|
84
|
+
const response = await apiClient.${boundary.method.toLowerCase()}(
|
|
85
|
+
'${boundary.api_endpoint}',
|
|
86
|
+
request
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
// Assert - Contract validation
|
|
90
|
+
expect(response.status).toBe(200);
|
|
91
|
+
expect(response.data).toMatchObject(${JSON.stringify(boundary.response_contract || {}, null, 4)});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should return correct response schema', async () => {
|
|
95
|
+
const response = await apiClient.${boundary.method.toLowerCase()}('${boundary.api_endpoint}');
|
|
96
|
+
|
|
97
|
+
// Validate response contract structure
|
|
98
|
+
const expectedSchema = ${JSON.stringify(boundary.response_contract || {}, null, 4)};
|
|
99
|
+
|
|
100
|
+
Object.keys(expectedSchema).forEach(key => {
|
|
101
|
+
expect(response.data).toHaveProperty(key);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
`;
|
|
106
|
+
|
|
107
|
+
return testCode;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Generate error state tests
|
|
112
|
+
*/
|
|
113
|
+
generateErrorStateTests(story, boundary) {
|
|
114
|
+
const errorStates = boundary.error_states || [400, 401, 404, 500];
|
|
115
|
+
|
|
116
|
+
const testCode = `
|
|
117
|
+
/**
|
|
118
|
+
* Error State Tests: ${story.name}
|
|
119
|
+
* Tests all error conditions defined in the contract
|
|
120
|
+
*/
|
|
121
|
+
|
|
122
|
+
import { describe, it, expect } from 'vitest';
|
|
123
|
+
import { apiClient } from '../utils/api-client';
|
|
124
|
+
|
|
125
|
+
describe('${story.name} - Error States', () => {
|
|
126
|
+
${errorStates.map(status => `
|
|
127
|
+
it('should handle ${status} error correctly', async () => {
|
|
128
|
+
const invalidRequest = generateInvalidRequest${status}();
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
await apiClient.${boundary.method.toLowerCase()}(
|
|
132
|
+
'${boundary.api_endpoint}',
|
|
133
|
+
invalidRequest
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
// Should not reach here
|
|
137
|
+
expect(true).toBe(false);
|
|
138
|
+
} catch (error) {
|
|
139
|
+
expect(error.response.status).toBe(${status});
|
|
140
|
+
expect(error.response.data).toHaveProperty('error');
|
|
141
|
+
expect(error.response.data).toHaveProperty('message');
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
`).join('\n')}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Error case generators
|
|
148
|
+
${errorStates.map(status => `
|
|
149
|
+
function generateInvalidRequest${status}() {
|
|
150
|
+
// TODO: Generate request that triggers ${status}
|
|
151
|
+
return {};
|
|
152
|
+
}
|
|
153
|
+
`).join('\n')}
|
|
154
|
+
`;
|
|
155
|
+
|
|
156
|
+
return testCode;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Generate edge case tests (boundary conditions)
|
|
161
|
+
*/
|
|
162
|
+
generateEdgeCaseTests(story, boundary) {
|
|
163
|
+
const testCode = `
|
|
164
|
+
/**
|
|
165
|
+
* Edge Case Tests: ${story.name}
|
|
166
|
+
* Boundary conditions, race conditions, and security edge cases
|
|
167
|
+
*
|
|
168
|
+
* NOTE: Edge Case Hunter persona should expand this
|
|
169
|
+
*/
|
|
170
|
+
|
|
171
|
+
import { describe, it, expect } from 'vitest';
|
|
172
|
+
import { apiClient } from '../utils/api-client';
|
|
173
|
+
|
|
174
|
+
describe('${story.name} - Edge Cases', () => {
|
|
175
|
+
it('should handle empty request body', async () => {
|
|
176
|
+
// Edge case: Empty payload
|
|
177
|
+
const response = await apiClient.${boundary.method.toLowerCase()}(
|
|
178
|
+
'${boundary.api_endpoint}',
|
|
179
|
+
{}
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
// Define expected behavior for empty input
|
|
183
|
+
expect(response.status).toBeOneOf([200, 400]);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('should handle extremely large payload', async () => {
|
|
187
|
+
// Edge case: Payload size limits
|
|
188
|
+
const largePayload = generateLargePayload(10 * 1024 * 1024); // 10MB
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
await apiClient.${boundary.method.toLowerCase()}(
|
|
192
|
+
'${boundary.api_endpoint}',
|
|
193
|
+
largePayload
|
|
194
|
+
);
|
|
195
|
+
} catch (error) {
|
|
196
|
+
expect(error.response.status).toBe(413); // Payload Too Large
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('should handle concurrent requests', async () => {
|
|
201
|
+
// Edge case: Race conditions
|
|
202
|
+
const requests = Array(100).fill().map(() =>
|
|
203
|
+
apiClient.${boundary.method.toLowerCase()}('${boundary.api_endpoint}')
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
const results = await Promise.allSettled(requests);
|
|
207
|
+
|
|
208
|
+
// All should complete without server errors
|
|
209
|
+
const serverErrors = results.filter(r =>
|
|
210
|
+
r.status === 'rejected' && r.reason.response?.status >= 500
|
|
211
|
+
);
|
|
212
|
+
expect(serverErrors.length).toBe(0);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// TODO: Add security edge cases (injection, XSS, CSRF)
|
|
216
|
+
// TODO: Add boundary value tests (min/max integers, string lengths)
|
|
217
|
+
// TODO: Add timeout and retry scenarios
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
function generateLargePayload(size) {
|
|
221
|
+
return { data: 'x'.repeat(size) };
|
|
222
|
+
}
|
|
223
|
+
`;
|
|
224
|
+
|
|
225
|
+
return testCode;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Save generated tests to files
|
|
230
|
+
*/
|
|
231
|
+
async saveTests(story, tests) {
|
|
232
|
+
const storyId = story.id.toLowerCase();
|
|
233
|
+
|
|
234
|
+
const files = [
|
|
235
|
+
{
|
|
236
|
+
path: join(this.contractsDir, `${storyId}-standard.test.js`),
|
|
237
|
+
content: tests.standard_flows,
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
path: join(this.contractsDir, `${storyId}-errors.test.js`),
|
|
241
|
+
content: tests.error_states,
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
path: join(this.edgeCasesDir, `${storyId}-edge.test.js`),
|
|
245
|
+
content: tests.edge_cases,
|
|
246
|
+
},
|
|
247
|
+
];
|
|
248
|
+
|
|
249
|
+
for (const file of files) {
|
|
250
|
+
await writeFile(file.path, file.content, 'utf-8');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return files.map(f => f.path);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Client Template for Contract Testing
|
|
3
|
+
* Generated by Dhurandhar Test-First Engine
|
|
4
|
+
*
|
|
5
|
+
* This client defines the interaction boundaries for all services
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
class APIClient {
|
|
9
|
+
constructor(baseURL = process.env.API_BASE_URL || 'http://localhost:8080') {
|
|
10
|
+
this.baseURL = baseURL;
|
|
11
|
+
this.headers = {
|
|
12
|
+
'Content-Type': 'application/json',
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Set authentication token
|
|
18
|
+
*/
|
|
19
|
+
setAuthToken(token) {
|
|
20
|
+
this.headers['Authorization'] = `Bearer ${token}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Generic request handler
|
|
25
|
+
*/
|
|
26
|
+
async request(method, endpoint, data = null, options = {}) {
|
|
27
|
+
const url = `${this.baseURL}${endpoint}`;
|
|
28
|
+
|
|
29
|
+
const config = {
|
|
30
|
+
method,
|
|
31
|
+
headers: { ...this.headers, ...options.headers },
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
if (data && ['POST', 'PUT', 'PATCH'].includes(method)) {
|
|
35
|
+
config.body = JSON.stringify(data);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const response = await fetch(url, config);
|
|
40
|
+
|
|
41
|
+
const responseData = await response.json().catch(() => ({}));
|
|
42
|
+
|
|
43
|
+
if (!response.ok) {
|
|
44
|
+
const error = new Error(`HTTP ${response.status}`);
|
|
45
|
+
error.response = {
|
|
46
|
+
status: response.status,
|
|
47
|
+
data: responseData,
|
|
48
|
+
};
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
status: response.status,
|
|
54
|
+
data: responseData,
|
|
55
|
+
headers: response.headers,
|
|
56
|
+
};
|
|
57
|
+
} catch (error) {
|
|
58
|
+
if (error.response) {
|
|
59
|
+
throw error; // HTTP error with response
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Network or other error
|
|
63
|
+
const networkError = new Error('Network error');
|
|
64
|
+
networkError.response = {
|
|
65
|
+
status: 0,
|
|
66
|
+
data: { error: 'NetworkError', message: error.message },
|
|
67
|
+
};
|
|
68
|
+
throw networkError;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* HTTP Methods
|
|
74
|
+
*/
|
|
75
|
+
get(endpoint, options) {
|
|
76
|
+
return this.request('GET', endpoint, null, options);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
post(endpoint, data, options) {
|
|
80
|
+
return this.request('POST', endpoint, data, options);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
put(endpoint, data, options) {
|
|
84
|
+
return this.request('PUT', endpoint, data, options);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
patch(endpoint, data, options) {
|
|
88
|
+
return this.request('PATCH', endpoint, data, options);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
delete(endpoint, options) {
|
|
92
|
+
return this.request('DELETE', endpoint, null, options);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Export singleton instance
|
|
97
|
+
export const apiClient = new APIClient();
|
|
98
|
+
|
|
99
|
+
// Export class for custom instances
|
|
100
|
+
export default APIClient;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vitest Configuration for Contract-First Testing
|
|
3
|
+
* Generated by Dhurandhar
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { defineConfig } from 'vitest/config';
|
|
7
|
+
|
|
8
|
+
export default defineConfig({
|
|
9
|
+
test: {
|
|
10
|
+
// Test environment
|
|
11
|
+
environment: 'node',
|
|
12
|
+
|
|
13
|
+
// Global test timeout
|
|
14
|
+
testTimeout: 10000,
|
|
15
|
+
|
|
16
|
+
// Coverage configuration
|
|
17
|
+
coverage: {
|
|
18
|
+
provider: 'v8',
|
|
19
|
+
reporter: ['text', 'json', 'html'],
|
|
20
|
+
exclude: [
|
|
21
|
+
'node_modules/**',
|
|
22
|
+
'tests/utils/**',
|
|
23
|
+
'**/*.config.js',
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
// Test execution
|
|
28
|
+
globals: true,
|
|
29
|
+
watch: false,
|
|
30
|
+
|
|
31
|
+
// Setup files
|
|
32
|
+
setupFiles: ['./tests/setup.js'],
|
|
33
|
+
|
|
34
|
+
// Reporters
|
|
35
|
+
reporters: ['verbose'],
|
|
36
|
+
},
|
|
37
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration Validator
|
|
3
|
+
* Validates configuration files and settings
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { ConfigManager } from '../config-manager.js';
|
|
7
|
+
|
|
8
|
+
export class ConfigValidator {
|
|
9
|
+
constructor(projectRoot) {
|
|
10
|
+
this.projectRoot = projectRoot;
|
|
11
|
+
this.configManager = new ConfigManager(projectRoot);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Validate configuration
|
|
16
|
+
*/
|
|
17
|
+
async validate(strict = false) {
|
|
18
|
+
const result = {
|
|
19
|
+
valid: true,
|
|
20
|
+
errors: [],
|
|
21
|
+
warnings: [],
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
// Check if config exists
|
|
26
|
+
if (!this.configManager.exists()) {
|
|
27
|
+
result.valid = false;
|
|
28
|
+
result.errors.push('Configuration file not found');
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Load and validate structure
|
|
33
|
+
const config = await this.configManager.load();
|
|
34
|
+
|
|
35
|
+
// Required fields
|
|
36
|
+
const required = ['version', 'projectName', 'userName', 'outputFolder'];
|
|
37
|
+
for (const field of required) {
|
|
38
|
+
if (!config[field]) {
|
|
39
|
+
result.valid = false;
|
|
40
|
+
result.errors.push(`Missing required field: ${field}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Validate project name format
|
|
45
|
+
if (config.projectName && !/^[a-z0-9-]+$/.test(config.projectName)) {
|
|
46
|
+
if (strict) {
|
|
47
|
+
result.valid = false;
|
|
48
|
+
result.errors.push('Project name must contain only lowercase letters, numbers, and hyphens');
|
|
49
|
+
} else {
|
|
50
|
+
result.warnings.push('Project name should contain only lowercase letters, numbers, and hyphens');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Validate output folder
|
|
55
|
+
if (config.outputFolder && config.outputFolder.includes('..')) {
|
|
56
|
+
result.valid = false;
|
|
57
|
+
result.errors.push('Output folder cannot contain parent directory references (..)');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Validate modules array
|
|
61
|
+
if (config.modules && !Array.isArray(config.modules)) {
|
|
62
|
+
result.valid = false;
|
|
63
|
+
result.errors.push('Modules field must be an array');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Validate version format
|
|
67
|
+
if (config.version && !/^\d+\.\d+\.\d+/.test(config.version)) {
|
|
68
|
+
result.warnings.push('Version should follow semantic versioning (x.y.z)');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Check for unknown fields (in strict mode)
|
|
72
|
+
if (strict) {
|
|
73
|
+
const knownFields = ['version', 'projectName', 'userName', 'outputFolder', 'modules', 'settings', 'variables'];
|
|
74
|
+
const unknownFields = Object.keys(config).filter(key => !knownFields.includes(key));
|
|
75
|
+
|
|
76
|
+
if (unknownFields.length > 0) {
|
|
77
|
+
result.warnings.push(`Unknown configuration fields: ${unknownFields.join(', ')}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
} catch (error) {
|
|
82
|
+
result.valid = false;
|
|
83
|
+
result.errors.push(`Validation error: ${error.message}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Validate a specific configuration value
|
|
91
|
+
*/
|
|
92
|
+
validateValue(field, value, rules) {
|
|
93
|
+
const errors = [];
|
|
94
|
+
|
|
95
|
+
if (rules.required && !value) {
|
|
96
|
+
errors.push(`${field} is required`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (rules.pattern && value && !rules.pattern.test(value)) {
|
|
100
|
+
errors.push(`${field} does not match required pattern`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (rules.minLength && value && value.length < rules.minLength) {
|
|
104
|
+
errors.push(`${field} must be at least ${rules.minLength} characters`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (rules.maxLength && value && value.length > rules.maxLength) {
|
|
108
|
+
errors.push(`${field} must be at most ${rules.maxLength} characters`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return errors;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module Validator
|
|
3
|
+
* Validates module structure and metadata
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
import { existsSync } from 'fs';
|
|
8
|
+
import { readFile } from 'fs/promises';
|
|
9
|
+
import yaml from 'yaml';
|
|
10
|
+
import { ModuleManager } from '../module-manager.js';
|
|
11
|
+
|
|
12
|
+
export class ModuleValidator {
|
|
13
|
+
constructor(projectRoot) {
|
|
14
|
+
this.projectRoot = projectRoot;
|
|
15
|
+
this.moduleManager = new ModuleManager(projectRoot);
|
|
16
|
+
this.modulesDir = join(projectRoot, '.dhurandhar/modules');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Validate all installed modules
|
|
21
|
+
*/
|
|
22
|
+
async validate(strict = false) {
|
|
23
|
+
const result = {
|
|
24
|
+
valid: true,
|
|
25
|
+
errors: [],
|
|
26
|
+
warnings: [],
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
// Check if modules directory exists
|
|
31
|
+
if (!existsSync(this.modulesDir)) {
|
|
32
|
+
result.warnings.push('No modules directory found');
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Get installed modules
|
|
37
|
+
const installed = await this.moduleManager.listInstalled();
|
|
38
|
+
|
|
39
|
+
if (installed.length === 0) {
|
|
40
|
+
result.warnings.push('No modules installed');
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Validate each module
|
|
45
|
+
for (const moduleCode of installed) {
|
|
46
|
+
const moduleResult = await this.validateModule(moduleCode, strict);
|
|
47
|
+
|
|
48
|
+
if (!moduleResult.valid) {
|
|
49
|
+
result.valid = false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
result.errors.push(...moduleResult.errors.map(e => `[${moduleCode}] ${e}`));
|
|
53
|
+
result.warnings.push(...moduleResult.warnings.map(w => `[${moduleCode}] ${w}`));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
} catch (error) {
|
|
57
|
+
result.valid = false;
|
|
58
|
+
result.errors.push(`Module validation error: ${error.message}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Validate a single module
|
|
66
|
+
*/
|
|
67
|
+
async validateModule(moduleCode, strict = false) {
|
|
68
|
+
const result = {
|
|
69
|
+
valid: true,
|
|
70
|
+
errors: [],
|
|
71
|
+
warnings: [],
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const modulePath = join(this.modulesDir, moduleCode);
|
|
75
|
+
const moduleYamlPath = join(modulePath, 'module.yaml');
|
|
76
|
+
|
|
77
|
+
// Check if module.yaml exists
|
|
78
|
+
if (!existsSync(moduleYamlPath)) {
|
|
79
|
+
result.valid = false;
|
|
80
|
+
result.errors.push('module.yaml not found');
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
// Parse module.yaml
|
|
86
|
+
const content = await readFile(moduleYamlPath, 'utf-8');
|
|
87
|
+
const metadata = yaml.parse(content);
|
|
88
|
+
|
|
89
|
+
// Required fields
|
|
90
|
+
const required = ['code', 'name', 'description'];
|
|
91
|
+
for (const field of required) {
|
|
92
|
+
if (!metadata[field]) {
|
|
93
|
+
result.valid = false;
|
|
94
|
+
result.errors.push(`Missing required field: ${field}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Validate code matches directory name
|
|
99
|
+
if (metadata.code && metadata.code !== moduleCode) {
|
|
100
|
+
if (strict) {
|
|
101
|
+
result.valid = false;
|
|
102
|
+
result.errors.push(`Module code "${metadata.code}" does not match directory name "${moduleCode}"`);
|
|
103
|
+
} else {
|
|
104
|
+
result.warnings.push(`Module code "${metadata.code}" does not match directory name "${moduleCode}"`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Validate dependencies
|
|
109
|
+
if (metadata.dependencies) {
|
|
110
|
+
if (!Array.isArray(metadata.dependencies)) {
|
|
111
|
+
result.valid = false;
|
|
112
|
+
result.errors.push('Dependencies must be an array');
|
|
113
|
+
} else {
|
|
114
|
+
const installed = await this.moduleManager.listInstalled();
|
|
115
|
+
const missing = metadata.dependencies.filter(dep => !installed.includes(dep));
|
|
116
|
+
|
|
117
|
+
if (missing.length > 0) {
|
|
118
|
+
result.valid = false;
|
|
119
|
+
result.errors.push(`Missing dependencies: ${missing.join(', ')}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Check for README
|
|
125
|
+
const readmePath = join(modulePath, 'README.md');
|
|
126
|
+
if (!existsSync(readmePath)) {
|
|
127
|
+
result.warnings.push('README.md not found');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
} catch (error) {
|
|
131
|
+
result.valid = false;
|
|
132
|
+
result.errors.push(`Failed to parse module.yaml: ${error.message}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
}
|