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.
Files changed (54) hide show
  1. package/.dhurandhar-session-start.md +242 -0
  2. package/LICENSE +21 -0
  3. package/README.md +416 -0
  4. package/docs/ARCHITECTURE_V2.md +249 -0
  5. package/docs/DECISION_REGISTRY.md +357 -0
  6. package/docs/IMPLEMENTATION_PERSONAS.md +406 -0
  7. package/docs/PLUGGABLE_STRATEGIES.md +439 -0
  8. package/docs/SYSTEM_OBSERVER.md +433 -0
  9. package/docs/TEST_FIRST_AGILE.md +359 -0
  10. package/docs/architecture.md +279 -0
  11. package/docs/engineering-first-philosophy.md +263 -0
  12. package/docs/getting-started.md +218 -0
  13. package/docs/module-development.md +323 -0
  14. package/docs/strategy-example.md +299 -0
  15. package/docs/test-first-example.md +392 -0
  16. package/package.json +79 -0
  17. package/src/core/README.md +92 -0
  18. package/src/core/agent-instructions/backend-developer.md +412 -0
  19. package/src/core/agent-instructions/devops-engineer.md +372 -0
  20. package/src/core/agent-instructions/dhurandhar-council.md +547 -0
  21. package/src/core/agent-instructions/edge-case-hunter.md +322 -0
  22. package/src/core/agent-instructions/frontend-developer.md +494 -0
  23. package/src/core/agent-instructions/lead-system-architect.md +631 -0
  24. package/src/core/agent-instructions/system-observer.md +319 -0
  25. package/src/core/agent-instructions/test-architect.md +284 -0
  26. package/src/core/module.yaml +54 -0
  27. package/src/core/schemas/design-module-schema.yaml +995 -0
  28. package/src/core/schemas/system-design-map-schema.yaml +324 -0
  29. package/src/modules/example/README.md +130 -0
  30. package/src/modules/example/module.yaml +252 -0
  31. package/tools/cli/commands/audit.js +267 -0
  32. package/tools/cli/commands/config.js +113 -0
  33. package/tools/cli/commands/context.js +170 -0
  34. package/tools/cli/commands/decisions.js +398 -0
  35. package/tools/cli/commands/entity.js +218 -0
  36. package/tools/cli/commands/epic.js +125 -0
  37. package/tools/cli/commands/install.js +172 -0
  38. package/tools/cli/commands/module.js +109 -0
  39. package/tools/cli/commands/service.js +167 -0
  40. package/tools/cli/commands/story.js +225 -0
  41. package/tools/cli/commands/strategy.js +294 -0
  42. package/tools/cli/commands/test.js +277 -0
  43. package/tools/cli/commands/validate.js +107 -0
  44. package/tools/cli/dhurandhar.js +212 -0
  45. package/tools/lib/config-manager.js +170 -0
  46. package/tools/lib/filesystem.js +126 -0
  47. package/tools/lib/module-installer.js +61 -0
  48. package/tools/lib/module-manager.js +149 -0
  49. package/tools/lib/sdm-manager.js +982 -0
  50. package/tools/lib/test-engine.js +255 -0
  51. package/tools/lib/test-templates/api-client.template.js +100 -0
  52. package/tools/lib/test-templates/vitest.config.template.js +37 -0
  53. package/tools/lib/validators/config-validator.js +113 -0
  54. 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
+ }