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,982 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* System Design Map (SDM) Manager
|
|
3
|
+
* Handles persistent architectural state for cross-session context rehydration
|
|
4
|
+
*
|
|
5
|
+
* Purpose: Eliminate cognitive fatigue by maintaining architectural context
|
|
6
|
+
* between sessions without requiring rediscovery
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
import { existsSync } from 'fs';
|
|
11
|
+
import { readFile, writeFile } from 'fs/promises';
|
|
12
|
+
import yaml from 'yaml';
|
|
13
|
+
|
|
14
|
+
export class SDMManager {
|
|
15
|
+
constructor(projectRoot) {
|
|
16
|
+
this.projectRoot = projectRoot;
|
|
17
|
+
this.sdmPath = join(projectRoot, 'SYSTEM_DESIGN_MAP.yaml');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Check if SDM exists
|
|
22
|
+
*/
|
|
23
|
+
exists() {
|
|
24
|
+
return existsSync(this.sdmPath);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Initialize new SDM
|
|
29
|
+
*/
|
|
30
|
+
async initialize(projectName = 'new-project') {
|
|
31
|
+
const sdm = {
|
|
32
|
+
metadata: {
|
|
33
|
+
project_name: projectName,
|
|
34
|
+
version: '1.0.0',
|
|
35
|
+
last_updated: new Date().toISOString(),
|
|
36
|
+
architecture_type: 'microservices', // or monolith, serverless
|
|
37
|
+
},
|
|
38
|
+
tech_stack: {
|
|
39
|
+
languages: [],
|
|
40
|
+
frameworks: [],
|
|
41
|
+
databases: [],
|
|
42
|
+
infrastructure: [],
|
|
43
|
+
},
|
|
44
|
+
user_types: [],
|
|
45
|
+
entities: [],
|
|
46
|
+
services: [],
|
|
47
|
+
communication: [],
|
|
48
|
+
infrastructure: {
|
|
49
|
+
deployment_target: null,
|
|
50
|
+
components: [],
|
|
51
|
+
},
|
|
52
|
+
build_config: [],
|
|
53
|
+
// NEW: Agile Blueprint for Test-First
|
|
54
|
+
agile_blueprint: {
|
|
55
|
+
epics: [],
|
|
56
|
+
test_suite_status: {
|
|
57
|
+
total_stories: 0,
|
|
58
|
+
tested_stories: 0,
|
|
59
|
+
coverage_percentage: 0,
|
|
60
|
+
last_test_run: null,
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
// NEW: Technical Strategies - Pluggable Architectural Patterns
|
|
64
|
+
technical_strategies: {
|
|
65
|
+
persistence: {
|
|
66
|
+
model: null,
|
|
67
|
+
constraints: [],
|
|
68
|
+
database_assignments: {},
|
|
69
|
+
},
|
|
70
|
+
state_management: {
|
|
71
|
+
caching_layer: null,
|
|
72
|
+
session_storage: null,
|
|
73
|
+
cache_invalidation: null,
|
|
74
|
+
},
|
|
75
|
+
communication: {
|
|
76
|
+
primary_pattern: null,
|
|
77
|
+
event_bus: null,
|
|
78
|
+
api_gateway: null,
|
|
79
|
+
},
|
|
80
|
+
deployment: {
|
|
81
|
+
orchestration: null,
|
|
82
|
+
scaling_strategy: null,
|
|
83
|
+
service_mesh: { enabled: false, technology: 'none' },
|
|
84
|
+
},
|
|
85
|
+
observability: {
|
|
86
|
+
logging: null,
|
|
87
|
+
metrics: null,
|
|
88
|
+
tracing: null,
|
|
89
|
+
distributed_tracing: false,
|
|
90
|
+
},
|
|
91
|
+
security: {
|
|
92
|
+
authentication: null,
|
|
93
|
+
authorization: null,
|
|
94
|
+
secrets_management: null,
|
|
95
|
+
},
|
|
96
|
+
resilience: {
|
|
97
|
+
circuit_breaker: false,
|
|
98
|
+
retry_strategy: null,
|
|
99
|
+
timeout_policy: null,
|
|
100
|
+
bulkhead_isolation: false,
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
await this.save(sdm);
|
|
106
|
+
return sdm;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Load SDM for rehydration
|
|
111
|
+
*/
|
|
112
|
+
async load() {
|
|
113
|
+
if (!this.exists()) {
|
|
114
|
+
throw new Error('System Design Map not found. Run "dhurandhar install" first.');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const content = await readFile(this.sdmPath, 'utf-8');
|
|
118
|
+
return yaml.parse(content);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Save SDM
|
|
123
|
+
*/
|
|
124
|
+
async save(sdm) {
|
|
125
|
+
// Update timestamp
|
|
126
|
+
sdm.metadata.last_updated = new Date().toISOString();
|
|
127
|
+
|
|
128
|
+
const content = yaml.stringify(sdm, {
|
|
129
|
+
indent: 2,
|
|
130
|
+
lineWidth: 0,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
await writeFile(this.sdmPath, content, 'utf-8');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Set architectural strategy
|
|
138
|
+
*/
|
|
139
|
+
async setStrategy(category, strategy) {
|
|
140
|
+
const sdm = await this.load();
|
|
141
|
+
|
|
142
|
+
if (!sdm.technical_strategies) {
|
|
143
|
+
sdm.technical_strategies = {};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (!sdm.technical_strategies[category]) {
|
|
147
|
+
sdm.technical_strategies[category] = {};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Merge strategy into category
|
|
151
|
+
Object.assign(sdm.technical_strategies[category], strategy);
|
|
152
|
+
|
|
153
|
+
await this.save(sdm);
|
|
154
|
+
return sdm;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Get active strategies
|
|
159
|
+
*/
|
|
160
|
+
async getStrategies() {
|
|
161
|
+
const sdm = await this.load();
|
|
162
|
+
return sdm.technical_strategies || {};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Get decision registry
|
|
167
|
+
*/
|
|
168
|
+
async getDecisions() {
|
|
169
|
+
const sdm = await this.load();
|
|
170
|
+
return sdm.decision_registry || {};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Set decision category
|
|
175
|
+
*/
|
|
176
|
+
async setDecision(category, decisions) {
|
|
177
|
+
const sdm = await this.load();
|
|
178
|
+
|
|
179
|
+
if (!sdm.decision_registry) {
|
|
180
|
+
sdm.decision_registry = {};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
sdm.decision_registry[category] = decisions;
|
|
184
|
+
|
|
185
|
+
await this.save(sdm);
|
|
186
|
+
return sdm;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Get rehydration context (including decisions)
|
|
191
|
+
*/
|
|
192
|
+
async getRehydrationContext() {
|
|
193
|
+
const sdm = await this.load();
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
metadata: sdm.metadata || {},
|
|
197
|
+
tech_stack: sdm.tech_stack || {},
|
|
198
|
+
decision_registry: sdm.decision_registry || {}, // NEW
|
|
199
|
+
technical_strategies: sdm.technical_strategies || {},
|
|
200
|
+
services: sdm.services || [],
|
|
201
|
+
entities: sdm.entities || [],
|
|
202
|
+
agile_blueprint: sdm.agile_blueprint || {},
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Apply strategy constraints to service
|
|
208
|
+
*/
|
|
209
|
+
applyStrategyConstraints(service, strategies) {
|
|
210
|
+
// Clone service to avoid mutation
|
|
211
|
+
const enhancedService = { ...service };
|
|
212
|
+
|
|
213
|
+
// Persistence Strategy: Database-per-Service
|
|
214
|
+
if (strategies.persistence?.model === 'database_per_service') {
|
|
215
|
+
if (!enhancedService.dedicated_database) {
|
|
216
|
+
enhancedService.dedicated_database = {
|
|
217
|
+
name: `${service.name.replace('-service', '')}_db`,
|
|
218
|
+
type: service.tech_stack?.database || 'PostgreSQL',
|
|
219
|
+
isolation: 'full',
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Add constraint: No cross-service queries
|
|
224
|
+
if (!enhancedService.constraints) enhancedService.constraints = [];
|
|
225
|
+
if (!enhancedService.constraints.includes('no_cross_service_queries')) {
|
|
226
|
+
enhancedService.constraints.push('no_cross_service_queries');
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Communication Strategy: Event-Driven
|
|
231
|
+
if (strategies.communication?.primary_pattern === 'asynchronous_events') {
|
|
232
|
+
if (!enhancedService.event_boundaries) {
|
|
233
|
+
enhancedService.event_boundaries = {
|
|
234
|
+
produces: [],
|
|
235
|
+
consumes: [],
|
|
236
|
+
event_bus: strategies.communication.event_bus?.technology || 'kafka',
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// State Management: Distributed Cache
|
|
242
|
+
if (strategies.state_management?.caching_layer === 'distributed_redis') {
|
|
243
|
+
if (!enhancedService.dependencies) enhancedService.dependencies = [];
|
|
244
|
+
if (!enhancedService.dependencies.includes('redis')) {
|
|
245
|
+
enhancedService.dependencies.push('redis');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
enhancedService.cache_config = {
|
|
249
|
+
type: 'redis',
|
|
250
|
+
strategy: strategies.state_management.cache_invalidation || 'ttl_based',
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Resilience: Circuit Breaker
|
|
255
|
+
if (strategies.resilience?.circuit_breaker) {
|
|
256
|
+
enhancedService.resilience = {
|
|
257
|
+
circuit_breaker: {
|
|
258
|
+
enabled: true,
|
|
259
|
+
failure_threshold: 5,
|
|
260
|
+
timeout: 30000,
|
|
261
|
+
},
|
|
262
|
+
retry: {
|
|
263
|
+
strategy: strategies.resilience.retry_strategy || 'exponential_backoff',
|
|
264
|
+
max_attempts: 3,
|
|
265
|
+
},
|
|
266
|
+
timeout: strategies.resilience.timeout_policy || 'moderate_30s',
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Security: Authentication
|
|
271
|
+
if (strategies.security?.authentication === 'jwt_centralized') {
|
|
272
|
+
if (!enhancedService.dependencies) enhancedService.dependencies = [];
|
|
273
|
+
if (!enhancedService.dependencies.includes('auth-service')) {
|
|
274
|
+
enhancedService.dependencies.push('auth-service');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
enhancedService.authentication = {
|
|
278
|
+
type: 'jwt',
|
|
279
|
+
validation: 'centralized',
|
|
280
|
+
token_endpoint: '/api/v1/auth/validate',
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return enhancedService;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Apply decision registry constraints to service
|
|
289
|
+
*/
|
|
290
|
+
applyDecisionConstraints(service, decisions) {
|
|
291
|
+
if (!decisions) return service;
|
|
292
|
+
|
|
293
|
+
const enhancedService = { ...service };
|
|
294
|
+
|
|
295
|
+
// Add decision constraints as metadata
|
|
296
|
+
enhancedService._decision_constraints = {};
|
|
297
|
+
|
|
298
|
+
// Naming conventions
|
|
299
|
+
if (decisions.naming_conventions) {
|
|
300
|
+
enhancedService._decision_constraints.naming = {
|
|
301
|
+
case_styles: decisions.naming_conventions.case_styles || {},
|
|
302
|
+
file_patterns: decisions.naming_conventions.file_patterns || {},
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Design patterns
|
|
307
|
+
if (decisions.design_patterns) {
|
|
308
|
+
enhancedService._decision_constraints.patterns = {
|
|
309
|
+
data_access: decisions.design_patterns.data_access,
|
|
310
|
+
dependency_injection: decisions.design_patterns.dependency_injection,
|
|
311
|
+
validation: decisions.design_patterns.validation,
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
// Check for service-specific patterns
|
|
315
|
+
const servicePattern = decisions.design_patterns.service_specific_patterns?.find(
|
|
316
|
+
p => p.service === service.name
|
|
317
|
+
);
|
|
318
|
+
if (servicePattern) {
|
|
319
|
+
enhancedService._decision_constraints.patterns.service_specific = servicePattern.pattern;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Error standards
|
|
324
|
+
if (decisions.error_response_standards) {
|
|
325
|
+
enhancedService._decision_constraints.errors = {
|
|
326
|
+
code_mapping: decisions.error_response_standards.error_code_mapping || {},
|
|
327
|
+
response_envelope: decisions.error_response_standards.response_envelope || {},
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Observability standards
|
|
332
|
+
if (decisions.observability_standards) {
|
|
333
|
+
enhancedService._decision_constraints.observability = {
|
|
334
|
+
logging: decisions.observability_standards.logging || {},
|
|
335
|
+
tracing: decisions.observability_standards.tracing || {},
|
|
336
|
+
metrics: decisions.observability_standards.metrics || {},
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return enhancedService;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Add a service to SDM (with strategy and decision injection)
|
|
345
|
+
*/
|
|
346
|
+
async addService(service) {
|
|
347
|
+
const sdm = await this.load();
|
|
348
|
+
|
|
349
|
+
// Apply global strategy constraints
|
|
350
|
+
const strategies = sdm.technical_strategies || {};
|
|
351
|
+
let enhancedService = this.applyStrategyConstraints(service, strategies);
|
|
352
|
+
|
|
353
|
+
// Apply decision registry constraints
|
|
354
|
+
const decisions = sdm.decision_registry || {};
|
|
355
|
+
enhancedService = this.applyDecisionConstraints(enhancedService, decisions);
|
|
356
|
+
|
|
357
|
+
// Check if service already exists
|
|
358
|
+
const existingIndex = sdm.services.findIndex(s => s.name === service.name);
|
|
359
|
+
|
|
360
|
+
if (existingIndex >= 0) {
|
|
361
|
+
// Update existing
|
|
362
|
+
sdm.services[existingIndex] = { ...sdm.services[existingIndex], ...enhancedService };
|
|
363
|
+
} else {
|
|
364
|
+
// Add new
|
|
365
|
+
sdm.services.push(enhancedService);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Update tech stack summary
|
|
369
|
+
this.updateTechStackSummary(sdm, enhancedService);
|
|
370
|
+
|
|
371
|
+
await this.save(sdm);
|
|
372
|
+
return sdm;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Add an entity to SDM
|
|
377
|
+
*/
|
|
378
|
+
async addEntity(entity) {
|
|
379
|
+
const sdm = await this.load();
|
|
380
|
+
|
|
381
|
+
const existingIndex = sdm.entities.findIndex(e => e.name === entity.name);
|
|
382
|
+
|
|
383
|
+
if (existingIndex >= 0) {
|
|
384
|
+
sdm.entities[existingIndex] = { ...sdm.entities[existingIndex], ...entity };
|
|
385
|
+
} else {
|
|
386
|
+
sdm.entities.push(entity);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
await this.save(sdm);
|
|
390
|
+
return sdm;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Add service communication pattern
|
|
395
|
+
*/
|
|
396
|
+
async addCommunication(from, to, protocol, pattern = 'sync') {
|
|
397
|
+
const sdm = await this.load();
|
|
398
|
+
|
|
399
|
+
const comm = { from, to, protocol, pattern };
|
|
400
|
+
|
|
401
|
+
// Remove duplicate if exists
|
|
402
|
+
sdm.communication = sdm.communication.filter(
|
|
403
|
+
c => !(c.from === from && c.to === to)
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
sdm.communication.push(comm);
|
|
407
|
+
|
|
408
|
+
await this.save(sdm);
|
|
409
|
+
return sdm;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Add Epic to Agile Blueprint
|
|
414
|
+
*/
|
|
415
|
+
async addEpic(epic) {
|
|
416
|
+
const sdm = await this.load();
|
|
417
|
+
|
|
418
|
+
if (!sdm.agile_blueprint) {
|
|
419
|
+
sdm.agile_blueprint = { epics: [], test_suite_status: {} };
|
|
420
|
+
}
|
|
421
|
+
if (!sdm.agile_blueprint.epics) {
|
|
422
|
+
sdm.agile_blueprint.epics = [];
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
sdm.agile_blueprint.epics.push(epic);
|
|
426
|
+
await this.save(sdm);
|
|
427
|
+
return sdm;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Add Story to Epic
|
|
432
|
+
*/
|
|
433
|
+
async addStory(epicId, story) {
|
|
434
|
+
const sdm = await this.load();
|
|
435
|
+
|
|
436
|
+
const epic = sdm.agile_blueprint?.epics?.find(e => e.id === epicId);
|
|
437
|
+
if (!epic) {
|
|
438
|
+
throw new Error(`Epic ${epicId} not found`);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (!epic.stories) epic.stories = [];
|
|
442
|
+
epic.stories.push(story);
|
|
443
|
+
|
|
444
|
+
await this.save(sdm);
|
|
445
|
+
return sdm;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Update task status
|
|
450
|
+
*/
|
|
451
|
+
async updateTaskStatus(storyId, taskId, status) {
|
|
452
|
+
const sdm = await this.load();
|
|
453
|
+
|
|
454
|
+
for (const epic of sdm.agile_blueprint?.epics || []) {
|
|
455
|
+
const story = epic.stories?.find(s => s.id === storyId);
|
|
456
|
+
if (story) {
|
|
457
|
+
const task = story.tasks?.find(t => t.id === taskId);
|
|
458
|
+
if (task) {
|
|
459
|
+
task.status = status;
|
|
460
|
+
await this.save(sdm);
|
|
461
|
+
return sdm;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
throw new Error(`Task ${taskId} not found in Story ${storyId}`);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Get rehydration context (formatted for agent consumption)
|
|
471
|
+
*/
|
|
472
|
+
async getRehydrationContext() {
|
|
473
|
+
const sdm = await this.load();
|
|
474
|
+
|
|
475
|
+
return {
|
|
476
|
+
summary: this.generateSummary(sdm),
|
|
477
|
+
services: sdm.services,
|
|
478
|
+
entities: sdm.entities,
|
|
479
|
+
communication: sdm.communication,
|
|
480
|
+
agile_blueprint: sdm.agile_blueprint,
|
|
481
|
+
fullMap: sdm,
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Generate human-readable summary
|
|
487
|
+
*/
|
|
488
|
+
generateSummary(sdm) {
|
|
489
|
+
const lines = [];
|
|
490
|
+
|
|
491
|
+
lines.push(`# ${sdm.metadata.project_name}`);
|
|
492
|
+
lines.push(`Architecture: ${sdm.metadata.architecture_type}`);
|
|
493
|
+
lines.push(`Last Updated: ${sdm.metadata.last_updated}`);
|
|
494
|
+
lines.push('');
|
|
495
|
+
lines.push('## Tech Stack');
|
|
496
|
+
lines.push(`Languages: ${sdm.tech_stack.languages.join(', ') || 'None'}`);
|
|
497
|
+
lines.push(`Frameworks: ${sdm.tech_stack.frameworks.join(', ') || 'None'}`);
|
|
498
|
+
lines.push(`Databases: ${sdm.tech_stack.databases.join(', ') || 'None'}`);
|
|
499
|
+
lines.push('');
|
|
500
|
+
lines.push(`## Services (${sdm.services.length})`);
|
|
501
|
+
sdm.services.forEach(s => {
|
|
502
|
+
lines.push(`- ${s.name}: ${s.scope}`);
|
|
503
|
+
lines.push(` Stack: ${s.tech_stack.language}/${s.tech_stack.framework}`);
|
|
504
|
+
});
|
|
505
|
+
lines.push('');
|
|
506
|
+
|
|
507
|
+
// Technical Strategies Summary
|
|
508
|
+
if (sdm.technical_strategies) {
|
|
509
|
+
const strategies = [];
|
|
510
|
+
if (sdm.technical_strategies.persistence?.model) {
|
|
511
|
+
strategies.push(`Persistence: ${sdm.technical_strategies.persistence.model}`);
|
|
512
|
+
}
|
|
513
|
+
if (sdm.technical_strategies.communication?.primary_pattern) {
|
|
514
|
+
strategies.push(`Communication: ${sdm.technical_strategies.communication.primary_pattern}`);
|
|
515
|
+
}
|
|
516
|
+
if (sdm.technical_strategies.state_management?.caching_layer) {
|
|
517
|
+
strategies.push(`Caching: ${sdm.technical_strategies.state_management.caching_layer}`);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (strategies.length > 0) {
|
|
521
|
+
lines.push(`## Active Strategies`);
|
|
522
|
+
strategies.forEach(s => lines.push(`- ${s}`));
|
|
523
|
+
lines.push('');
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Agile Blueprint Summary
|
|
528
|
+
if (sdm.agile_blueprint && sdm.agile_blueprint.epics && sdm.agile_blueprint.epics.length > 0) {
|
|
529
|
+
const totalStories = sdm.agile_blueprint.epics.reduce((acc, e) => acc + (e.stories?.length || 0), 0);
|
|
530
|
+
lines.push(`## Agile Blueprint`);
|
|
531
|
+
lines.push(`Epics: ${sdm.agile_blueprint.epics.length}`);
|
|
532
|
+
lines.push(`Stories: ${totalStories}`);
|
|
533
|
+
if (sdm.agile_blueprint.test_suite_status) {
|
|
534
|
+
lines.push(`Test Coverage: ${sdm.agile_blueprint.test_suite_status.coverage_percentage || 0}%`);
|
|
535
|
+
}
|
|
536
|
+
lines.push('');
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
return lines.join('\n');
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Check architectural alignment for existing services
|
|
544
|
+
* Returns services that need updates to match new strategy
|
|
545
|
+
*/
|
|
546
|
+
async checkStrategyAlignment(newStrategy, category) {
|
|
547
|
+
const sdm = await this.load();
|
|
548
|
+
const misalignedServices = [];
|
|
549
|
+
|
|
550
|
+
for (const service of sdm.services) {
|
|
551
|
+
const alignmentIssues = [];
|
|
552
|
+
|
|
553
|
+
// Check persistence alignment
|
|
554
|
+
if (category === 'persistence' && newStrategy.model === 'database_per_service') {
|
|
555
|
+
if (!service.dedicated_database) {
|
|
556
|
+
alignmentIssues.push('Missing dedicated database');
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Check communication alignment
|
|
561
|
+
if (category === 'communication' && newStrategy.primary_pattern === 'asynchronous_events') {
|
|
562
|
+
if (!service.event_boundaries) {
|
|
563
|
+
alignmentIssues.push('Missing event boundaries (produces/consumes)');
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Check state management alignment
|
|
568
|
+
if (category === 'state_management' && newStrategy.caching_layer === 'distributed_redis') {
|
|
569
|
+
if (!service.dependencies?.includes('redis')) {
|
|
570
|
+
alignmentIssues.push('Missing Redis dependency for distributed cache');
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
if (alignmentIssues.length > 0) {
|
|
575
|
+
misalignedServices.push({
|
|
576
|
+
service: service.name,
|
|
577
|
+
issues: alignmentIssues,
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
return misalignedServices;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Apply strategy to all existing services
|
|
587
|
+
*/
|
|
588
|
+
async realignServicesToStrategy() {
|
|
589
|
+
const sdm = await this.load();
|
|
590
|
+
const strategies = sdm.technical_strategies;
|
|
591
|
+
|
|
592
|
+
for (let i = 0; i < sdm.services.length; i++) {
|
|
593
|
+
sdm.services[i] = this.applyStrategyConstraints(sdm.services[i], strategies);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
await this.save(sdm);
|
|
597
|
+
return sdm;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Perform architectural audit - detect drift between SDM and codebase
|
|
602
|
+
*/
|
|
603
|
+
async performAudit() {
|
|
604
|
+
const sdm = await this.load();
|
|
605
|
+
const codebase = await this.scanCodebase();
|
|
606
|
+
|
|
607
|
+
const audit = {
|
|
608
|
+
timestamp: new Date().toISOString(),
|
|
609
|
+
summary: {
|
|
610
|
+
total_services: sdm.services.length,
|
|
611
|
+
implemented_services: 0,
|
|
612
|
+
total_entities: sdm.entities.length,
|
|
613
|
+
implemented_entities: 0,
|
|
614
|
+
total_stories: 0,
|
|
615
|
+
tested_stories: 0,
|
|
616
|
+
strategy_compliance_percentage: 100,
|
|
617
|
+
},
|
|
618
|
+
unimplemented: {
|
|
619
|
+
services: [],
|
|
620
|
+
entities: [],
|
|
621
|
+
stories: [],
|
|
622
|
+
},
|
|
623
|
+
unmanaged: {
|
|
624
|
+
services: [],
|
|
625
|
+
entities: [],
|
|
626
|
+
routes: [],
|
|
627
|
+
},
|
|
628
|
+
strategy_violations: [],
|
|
629
|
+
drift_percentage: 0,
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
// Check services
|
|
633
|
+
for (const service of sdm.services) {
|
|
634
|
+
const implemented = codebase.services.some(s =>
|
|
635
|
+
s.name === service.name || s.name === service.name.replace('-service', '')
|
|
636
|
+
);
|
|
637
|
+
|
|
638
|
+
if (implemented) {
|
|
639
|
+
audit.summary.implemented_services++;
|
|
640
|
+
|
|
641
|
+
// Check strategy compliance
|
|
642
|
+
const violations = this.checkServiceStrategyCompliance(service, sdm.technical_strategies, codebase);
|
|
643
|
+
audit.strategy_violations.push(...violations);
|
|
644
|
+
} else {
|
|
645
|
+
audit.unimplemented.services.push({
|
|
646
|
+
name: service.name,
|
|
647
|
+
scope: service.scope,
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// Check entities
|
|
653
|
+
for (const entity of sdm.entities) {
|
|
654
|
+
const implemented = codebase.entities.some(e =>
|
|
655
|
+
e.name === entity.name || e.table_name === entity.table_name
|
|
656
|
+
);
|
|
657
|
+
|
|
658
|
+
if (implemented) {
|
|
659
|
+
audit.summary.implemented_entities++;
|
|
660
|
+
} else {
|
|
661
|
+
audit.unimplemented.entities.push({
|
|
662
|
+
name: entity.name,
|
|
663
|
+
table_name: entity.table_name,
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Check stories and tests
|
|
669
|
+
if (sdm.agile_blueprint && sdm.agile_blueprint.epics) {
|
|
670
|
+
for (const epic of sdm.agile_blueprint.epics) {
|
|
671
|
+
for (const story of epic.stories || []) {
|
|
672
|
+
audit.summary.total_stories++;
|
|
673
|
+
|
|
674
|
+
const testFile = `tests/contracts/${story.id.toLowerCase()}-standard.test.js`;
|
|
675
|
+
const testExists = codebase.tests.includes(testFile);
|
|
676
|
+
|
|
677
|
+
if (testExists) {
|
|
678
|
+
audit.summary.tested_stories++;
|
|
679
|
+
} else {
|
|
680
|
+
audit.unimplemented.stories.push({
|
|
681
|
+
id: story.id,
|
|
682
|
+
name: story.name,
|
|
683
|
+
epic: epic.id,
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Check unmanaged (code not in SDM)
|
|
691
|
+
for (const codeService of codebase.services) {
|
|
692
|
+
const inSDM = sdm.services.some(s =>
|
|
693
|
+
s.name === codeService.name || `${codeService.name}-service` === s.name
|
|
694
|
+
);
|
|
695
|
+
|
|
696
|
+
if (!inSDM) {
|
|
697
|
+
audit.unmanaged.services.push(codeService);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
for (const codeEntity of codebase.entities) {
|
|
702
|
+
const inSDM = sdm.entities.some(e =>
|
|
703
|
+
e.name === codeEntity.name || e.table_name === codeEntity.table_name
|
|
704
|
+
);
|
|
705
|
+
|
|
706
|
+
if (!inSDM) {
|
|
707
|
+
audit.unmanaged.entities.push(codeEntity);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// Calculate drift percentage
|
|
712
|
+
const totalComponents =
|
|
713
|
+
audit.summary.total_services +
|
|
714
|
+
audit.summary.total_entities +
|
|
715
|
+
audit.summary.total_stories;
|
|
716
|
+
|
|
717
|
+
const driftComponents =
|
|
718
|
+
audit.unimplemented.services.length +
|
|
719
|
+
audit.unimplemented.entities.length +
|
|
720
|
+
audit.unimplemented.stories.length +
|
|
721
|
+
audit.unmanaged.services.length +
|
|
722
|
+
audit.unmanaged.entities.length +
|
|
723
|
+
audit.strategy_violations.length;
|
|
724
|
+
|
|
725
|
+
audit.drift_percentage = totalComponents > 0
|
|
726
|
+
? Math.round((driftComponents / totalComponents) * 100)
|
|
727
|
+
: 0;
|
|
728
|
+
|
|
729
|
+
// Calculate strategy compliance
|
|
730
|
+
const totalViolations = audit.strategy_violations.length;
|
|
731
|
+
const totalServices = sdm.services.length;
|
|
732
|
+
audit.summary.strategy_compliance_percentage = totalServices > 0
|
|
733
|
+
? Math.round(((totalServices - totalViolations) / totalServices) * 100)
|
|
734
|
+
: 100;
|
|
735
|
+
|
|
736
|
+
return audit;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* Scan codebase for actual implementation
|
|
741
|
+
*/
|
|
742
|
+
async scanCodebase() {
|
|
743
|
+
const fs = await import('fs');
|
|
744
|
+
const path = await import('path');
|
|
745
|
+
|
|
746
|
+
const codebase = {
|
|
747
|
+
services: [],
|
|
748
|
+
entities: [],
|
|
749
|
+
routes: [],
|
|
750
|
+
tests: [],
|
|
751
|
+
};
|
|
752
|
+
|
|
753
|
+
// Scan for services
|
|
754
|
+
const serviceDirs = ['services', 'cmd', 'src/services', 'apps'];
|
|
755
|
+
for (const dir of serviceDirs) {
|
|
756
|
+
const fullPath = path.join(this.projectRoot, dir);
|
|
757
|
+
if (fs.existsSync(fullPath)) {
|
|
758
|
+
const items = fs.readdirSync(fullPath, { withFileTypes: true });
|
|
759
|
+
for (const item of items) {
|
|
760
|
+
if (item.isDirectory()) {
|
|
761
|
+
codebase.services.push({
|
|
762
|
+
name: item.name,
|
|
763
|
+
path: path.join(dir, item.name),
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// Scan for entities/models
|
|
771
|
+
const entityDirs = ['models', 'entities', 'src/models', 'src/entities'];
|
|
772
|
+
for (const dir of entityDirs) {
|
|
773
|
+
const fullPath = path.join(this.projectRoot, dir);
|
|
774
|
+
if (fs.existsSync(fullPath)) {
|
|
775
|
+
const items = fs.readdirSync(fullPath);
|
|
776
|
+
for (const item of items) {
|
|
777
|
+
if (item.match(/\.(ts|js|go|py)$/)) {
|
|
778
|
+
const name = item.replace(/\.(ts|js|go|py)$/, '');
|
|
779
|
+
codebase.entities.push({
|
|
780
|
+
name,
|
|
781
|
+
file: path.join(dir, item),
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// Scan for tests
|
|
789
|
+
const testDirs = ['tests', '__tests__', 'test'];
|
|
790
|
+
for (const dir of testDirs) {
|
|
791
|
+
const fullPath = path.join(this.projectRoot, dir);
|
|
792
|
+
if (fs.existsSync(fullPath)) {
|
|
793
|
+
this.scanTestsRecursive(fullPath, dir, codebase.tests);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
return codebase;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* Recursively scan test directories
|
|
802
|
+
*/
|
|
803
|
+
scanTestsRecursive(dir, relativePath, tests) {
|
|
804
|
+
const fs = require('fs');
|
|
805
|
+
const path = require('path');
|
|
806
|
+
|
|
807
|
+
const items = fs.readdirSync(dir, { withFileTypes: true });
|
|
808
|
+
for (const item of items) {
|
|
809
|
+
const fullPath = path.join(dir, item.name);
|
|
810
|
+
const relPath = path.join(relativePath, item.name);
|
|
811
|
+
|
|
812
|
+
if (item.isDirectory()) {
|
|
813
|
+
this.scanTestsRecursive(fullPath, relPath, tests);
|
|
814
|
+
} else if (item.name.match(/\.(test|spec)\.(ts|js)$/) || item.name.match(/_test\.go$/)) {
|
|
815
|
+
tests.push(relPath);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
/**
|
|
821
|
+
* Check service compliance with decision registry
|
|
822
|
+
*/
|
|
823
|
+
checkServiceDecisionCompliance(service, decisions, codebase) {
|
|
824
|
+
const violations = [];
|
|
825
|
+
|
|
826
|
+
if (!decisions) return violations;
|
|
827
|
+
|
|
828
|
+
// Check naming conventions (would need code analysis - placeholder)
|
|
829
|
+
// In real implementation, would analyze actual code files
|
|
830
|
+
|
|
831
|
+
// Check design patterns compliance
|
|
832
|
+
if (decisions.design_patterns?.data_access && service._decision_constraints) {
|
|
833
|
+
const expectedPattern = decisions.design_patterns.data_access;
|
|
834
|
+
// Placeholder: Would analyze code to verify Repository pattern is used
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
return violations;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* Check service compliance with active strategies
|
|
842
|
+
*/
|
|
843
|
+
checkServiceStrategyCompliance(service, strategies, codebase) {
|
|
844
|
+
const violations = [];
|
|
845
|
+
|
|
846
|
+
if (!strategies) return violations;
|
|
847
|
+
|
|
848
|
+
// Check persistence strategy
|
|
849
|
+
if (strategies.persistence?.model === 'database_per_service') {
|
|
850
|
+
if (!service.dedicated_database) {
|
|
851
|
+
violations.push({
|
|
852
|
+
service: service.name,
|
|
853
|
+
strategy: 'persistence',
|
|
854
|
+
violation: 'Missing dedicated database',
|
|
855
|
+
expected: 'database_per_service',
|
|
856
|
+
actual: 'no dedicated_database config',
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// Check communication strategy
|
|
862
|
+
if (strategies.communication?.primary_pattern === 'asynchronous_events') {
|
|
863
|
+
if (!service.event_boundaries) {
|
|
864
|
+
violations.push({
|
|
865
|
+
service: service.name,
|
|
866
|
+
strategy: 'communication',
|
|
867
|
+
violation: 'Missing event boundaries',
|
|
868
|
+
expected: 'asynchronous_events',
|
|
869
|
+
actual: 'no event_boundaries config',
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// Check security strategy
|
|
875
|
+
if (strategies.security?.authentication === 'jwt_centralized') {
|
|
876
|
+
if (service.name !== 'auth-service' && !service.dependencies?.includes('auth-service')) {
|
|
877
|
+
violations.push({
|
|
878
|
+
service: service.name,
|
|
879
|
+
strategy: 'security',
|
|
880
|
+
violation: 'Missing auth-service dependency',
|
|
881
|
+
expected: 'jwt_centralized',
|
|
882
|
+
actual: 'no auth-service dependency',
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// Check resilience strategy
|
|
888
|
+
if (strategies.resilience?.circuit_breaker === true) {
|
|
889
|
+
if (!service.resilience?.circuit_breaker) {
|
|
890
|
+
violations.push({
|
|
891
|
+
service: service.name,
|
|
892
|
+
strategy: 'resilience',
|
|
893
|
+
violation: 'Missing circuit breaker config',
|
|
894
|
+
expected: 'circuit_breaker: true',
|
|
895
|
+
actual: 'no circuit_breaker config',
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
return violations;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/**
|
|
904
|
+
* Sync SDM to match codebase state
|
|
905
|
+
*/
|
|
906
|
+
async syncToCodebase() {
|
|
907
|
+
const sdm = await this.load();
|
|
908
|
+
const codebase = await this.scanCodebase();
|
|
909
|
+
const changes = {
|
|
910
|
+
added: { services: 0, entities: 0 },
|
|
911
|
+
removed: { services: 0, entities: 0 },
|
|
912
|
+
};
|
|
913
|
+
|
|
914
|
+
// Add unmanaged services
|
|
915
|
+
for (const codeService of codebase.services) {
|
|
916
|
+
const inSDM = sdm.services.some(s =>
|
|
917
|
+
s.name === codeService.name || `${codeService.name}-service` === s.name
|
|
918
|
+
);
|
|
919
|
+
|
|
920
|
+
if (!inSDM) {
|
|
921
|
+
sdm.services.push({
|
|
922
|
+
name: `${codeService.name}-service`,
|
|
923
|
+
scope: `Auto-discovered from ${codeService.path}`,
|
|
924
|
+
tech_stack: {
|
|
925
|
+
language: sdm.tech_stack.languages[0] || 'Unknown',
|
|
926
|
+
framework: sdm.tech_stack.frameworks[0] || 'Unknown',
|
|
927
|
+
},
|
|
928
|
+
_discovered: true,
|
|
929
|
+
});
|
|
930
|
+
changes.added.services++;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// Add unmanaged entities
|
|
935
|
+
for (const codeEntity of codebase.entities) {
|
|
936
|
+
const inSDM = sdm.entities.some(e =>
|
|
937
|
+
e.name === codeEntity.name
|
|
938
|
+
);
|
|
939
|
+
|
|
940
|
+
if (!inSDM) {
|
|
941
|
+
sdm.entities.push({
|
|
942
|
+
name: codeEntity.name,
|
|
943
|
+
table_name: codeEntity.name.toLowerCase() + 's',
|
|
944
|
+
attributes: [],
|
|
945
|
+
_discovered: true,
|
|
946
|
+
});
|
|
947
|
+
changes.added.entities++;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// Mark unimplemented as "planned" instead of removing
|
|
952
|
+
for (const service of sdm.services) {
|
|
953
|
+
const implemented = codebase.services.some(s =>
|
|
954
|
+
s.name === service.name || s.name === service.name.replace('-service', '')
|
|
955
|
+
);
|
|
956
|
+
|
|
957
|
+
if (!implemented && !service._status) {
|
|
958
|
+
service._status = 'planned';
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
await this.save(sdm);
|
|
963
|
+
return changes;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
/**
|
|
967
|
+
* Update tech stack summary from service
|
|
968
|
+
*/
|
|
969
|
+
updateTechStackSummary(sdm, service) {
|
|
970
|
+
const { language, framework, database } = service.tech_stack;
|
|
971
|
+
|
|
972
|
+
if (language && !sdm.tech_stack.languages.includes(language)) {
|
|
973
|
+
sdm.tech_stack.languages.push(language);
|
|
974
|
+
}
|
|
975
|
+
if (framework && !sdm.tech_stack.frameworks.includes(framework)) {
|
|
976
|
+
sdm.tech_stack.frameworks.push(framework);
|
|
977
|
+
}
|
|
978
|
+
if (database && !sdm.tech_stack.databases.includes(database)) {
|
|
979
|
+
sdm.tech_stack.databases.push(database);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}
|