musubi-sdd 3.0.1 → 3.5.1

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 (49) hide show
  1. package/bin/musubi-change.js +623 -10
  2. package/bin/musubi-orchestrate.js +456 -0
  3. package/bin/musubi-trace.js +393 -0
  4. package/package.json +3 -2
  5. package/src/analyzers/impact-analyzer.js +682 -0
  6. package/src/integrations/cicd.js +782 -0
  7. package/src/integrations/documentation.js +740 -0
  8. package/src/integrations/examples.js +789 -0
  9. package/src/integrations/index.js +23 -0
  10. package/src/integrations/platforms.js +929 -0
  11. package/src/managers/delta-spec.js +484 -0
  12. package/src/monitoring/incident-manager.js +890 -0
  13. package/src/monitoring/index.js +633 -0
  14. package/src/monitoring/observability.js +938 -0
  15. package/src/monitoring/release-manager.js +622 -0
  16. package/src/orchestration/index.js +168 -0
  17. package/src/orchestration/orchestration-engine.js +409 -0
  18. package/src/orchestration/pattern-registry.js +319 -0
  19. package/src/orchestration/patterns/auto.js +386 -0
  20. package/src/orchestration/patterns/group-chat.js +395 -0
  21. package/src/orchestration/patterns/human-in-loop.js +506 -0
  22. package/src/orchestration/patterns/nested.js +322 -0
  23. package/src/orchestration/patterns/sequential.js +278 -0
  24. package/src/orchestration/patterns/swarm.js +395 -0
  25. package/src/orchestration/workflow-orchestrator.js +738 -0
  26. package/src/reporters/coverage-report.js +452 -0
  27. package/src/reporters/traceability-matrix-report.js +684 -0
  28. package/src/steering/advanced-validation.js +812 -0
  29. package/src/steering/auto-updater.js +670 -0
  30. package/src/steering/index.js +119 -0
  31. package/src/steering/quality-metrics.js +650 -0
  32. package/src/steering/template-constraints.js +789 -0
  33. package/src/templates/agents/claude-code/skills/agent-assistant/SKILL.md +22 -0
  34. package/src/templates/agents/claude-code/skills/issue-resolver/SKILL.md +21 -0
  35. package/src/templates/agents/claude-code/skills/orchestrator/SKILL.md +90 -28
  36. package/src/templates/agents/claude-code/skills/project-manager/SKILL.md +32 -0
  37. package/src/templates/agents/claude-code/skills/site-reliability-engineer/SKILL.md +27 -0
  38. package/src/templates/agents/claude-code/skills/steering/SKILL.md +30 -0
  39. package/src/templates/agents/claude-code/skills/test-engineer/SKILL.md +21 -0
  40. package/src/templates/agents/claude-code/skills/ui-ux-designer/SKILL.md +27 -0
  41. package/src/templates/agents/codex/AGENTS.md +36 -1
  42. package/src/templates/agents/cursor/AGENTS.md +36 -1
  43. package/src/templates/agents/gemini-cli/GEMINI.md +36 -1
  44. package/src/templates/agents/github-copilot/AGENTS.md +65 -1
  45. package/src/templates/agents/qwen-code/QWEN.md +36 -1
  46. package/src/templates/agents/windsurf/AGENTS.md +36 -1
  47. package/src/templates/shared/delta-spec-template.md +246 -0
  48. package/src/validators/delta-format.js +474 -0
  49. package/src/validators/traceability-validator.js +561 -0
@@ -0,0 +1,650 @@
1
+ /**
2
+ * Quality Metrics Dashboard Module
3
+ *
4
+ * Provides:
5
+ * - Coverage metrics (code, test, documentation)
6
+ * - Compliance scoring (steering, constitutional)
7
+ * - Analytics and trends
8
+ * - Health indicators
9
+ */
10
+
11
+ const EventEmitter = require('events');
12
+ const path = require('path');
13
+
14
+ // Metric Categories
15
+ const MetricCategory = {
16
+ COVERAGE: 'coverage',
17
+ COMPLIANCE: 'compliance',
18
+ QUALITY: 'quality',
19
+ HEALTH: 'health',
20
+ TREND: 'trend'
21
+ };
22
+
23
+ // Health Status
24
+ const HealthStatus = {
25
+ HEALTHY: 'healthy',
26
+ WARNING: 'warning',
27
+ CRITICAL: 'critical',
28
+ UNKNOWN: 'unknown'
29
+ };
30
+
31
+ // Trend Direction
32
+ const TrendDirection = {
33
+ UP: 'up',
34
+ DOWN: 'down',
35
+ STABLE: 'stable',
36
+ UNKNOWN: 'unknown'
37
+ };
38
+
39
+ /**
40
+ * Base Metric
41
+ */
42
+ class Metric {
43
+ constructor(name, options = {}) {
44
+ this.name = name;
45
+ this.category = options.category || MetricCategory.QUALITY;
46
+ this.unit = options.unit || 'percent';
47
+ this.value = options.value || 0;
48
+ this.target = options.target || null;
49
+ this.min = options.min || 0;
50
+ this.max = options.max || 100;
51
+ this.timestamp = options.timestamp || new Date();
52
+ this.metadata = options.metadata || {};
53
+ }
54
+
55
+ update(value) {
56
+ this.value = Math.max(this.min, Math.min(this.max, value));
57
+ this.timestamp = new Date();
58
+ return this;
59
+ }
60
+
61
+ getProgress() {
62
+ if (this.target === null) return null;
63
+ return (this.value / this.target) * 100;
64
+ }
65
+
66
+ isOnTarget() {
67
+ if (this.target === null) return null;
68
+ return this.value >= this.target;
69
+ }
70
+
71
+ toJSON() {
72
+ return {
73
+ name: this.name,
74
+ category: this.category,
75
+ value: this.value,
76
+ unit: this.unit,
77
+ target: this.target,
78
+ progress: this.getProgress(),
79
+ onTarget: this.isOnTarget(),
80
+ timestamp: this.timestamp.toISOString()
81
+ };
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Coverage Metric
87
+ */
88
+ class CoverageMetric extends Metric {
89
+ constructor(name, options = {}) {
90
+ super(name, { ...options, category: MetricCategory.COVERAGE });
91
+ this.coveredItems = options.coveredItems || 0;
92
+ this.totalItems = options.totalItems || 0;
93
+ }
94
+
95
+ setCoverage(covered, total) {
96
+ this.coveredItems = covered;
97
+ this.totalItems = total;
98
+ this.value = total > 0 ? (covered / total) * 100 : 0;
99
+ this.timestamp = new Date();
100
+ return this;
101
+ }
102
+
103
+ toJSON() {
104
+ return {
105
+ ...super.toJSON(),
106
+ covered: this.coveredItems,
107
+ total: this.totalItems
108
+ };
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Compliance Metric
114
+ */
115
+ class ComplianceMetric extends Metric {
116
+ constructor(name, options = {}) {
117
+ super(name, { ...options, category: MetricCategory.COMPLIANCE });
118
+ this.rules = [];
119
+ this.violations = [];
120
+ }
121
+
122
+ addRule(rule) {
123
+ this.rules.push(rule);
124
+ return this;
125
+ }
126
+
127
+ addViolation(violation) {
128
+ this.violations.push({
129
+ ...violation,
130
+ timestamp: new Date()
131
+ });
132
+ this.recalculate();
133
+ return this;
134
+ }
135
+
136
+ clearViolations() {
137
+ this.violations = [];
138
+ this.recalculate();
139
+ return this;
140
+ }
141
+
142
+ recalculate() {
143
+ if (this.rules.length === 0) {
144
+ this.value = 100;
145
+ return;
146
+ }
147
+
148
+ const violatedRules = new Set(this.violations.map(v => v.rule));
149
+ const compliantCount = this.rules.length - violatedRules.size;
150
+ this.value = (compliantCount / this.rules.length) * 100;
151
+ this.timestamp = new Date();
152
+ }
153
+
154
+ toJSON() {
155
+ return {
156
+ ...super.toJSON(),
157
+ rules: this.rules.length,
158
+ violations: this.violations.length,
159
+ violatedRules: [...new Set(this.violations.map(v => v.rule))]
160
+ };
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Health Indicator
166
+ */
167
+ class HealthIndicator {
168
+ constructor(name, options = {}) {
169
+ this.name = name;
170
+ this.status = options.status || HealthStatus.UNKNOWN;
171
+ this.message = options.message || '';
172
+ this.lastCheck = options.lastCheck || null;
173
+ this.checkInterval = options.checkInterval || 60000; // 1 minute
174
+ this.checker = options.checker || (() => ({ status: HealthStatus.UNKNOWN }));
175
+ }
176
+
177
+ async check() {
178
+ try {
179
+ const result = await this.checker();
180
+ this.status = result.status;
181
+ this.message = result.message || '';
182
+ this.lastCheck = new Date();
183
+ return result;
184
+ } catch (error) {
185
+ this.status = HealthStatus.CRITICAL;
186
+ this.message = error.message;
187
+ this.lastCheck = new Date();
188
+ return { status: HealthStatus.CRITICAL, error };
189
+ }
190
+ }
191
+
192
+ isHealthy() {
193
+ return this.status === HealthStatus.HEALTHY;
194
+ }
195
+
196
+ needsCheck() {
197
+ if (!this.lastCheck) return true;
198
+ return Date.now() - this.lastCheck.getTime() > this.checkInterval;
199
+ }
200
+
201
+ toJSON() {
202
+ return {
203
+ name: this.name,
204
+ status: this.status,
205
+ message: this.message,
206
+ lastCheck: this.lastCheck?.toISOString(),
207
+ healthy: this.isHealthy()
208
+ };
209
+ }
210
+ }
211
+
212
+ /**
213
+ * Trend Analyzer
214
+ */
215
+ class TrendAnalyzer {
216
+ constructor(options = {}) {
217
+ this.dataPoints = [];
218
+ this.maxPoints = options.maxPoints || 100;
219
+ this.trendWindow = options.trendWindow || 7; // days
220
+ }
221
+
222
+ addDataPoint(value, timestamp = new Date()) {
223
+ this.dataPoints.push({ value, timestamp });
224
+
225
+ // Keep only maxPoints
226
+ if (this.dataPoints.length > this.maxPoints) {
227
+ this.dataPoints = this.dataPoints.slice(-this.maxPoints);
228
+ }
229
+
230
+ return this;
231
+ }
232
+
233
+ getTrend() {
234
+ if (this.dataPoints.length < 2) {
235
+ return { direction: TrendDirection.UNKNOWN, change: 0 };
236
+ }
237
+
238
+ const recent = this.dataPoints.slice(-this.trendWindow);
239
+ const first = recent[0].value;
240
+ const last = recent[recent.length - 1].value;
241
+ const change = last - first;
242
+ const percentChange = first > 0 ? (change / first) * 100 : 0;
243
+
244
+ let direction;
245
+ if (Math.abs(percentChange) < 1) {
246
+ direction = TrendDirection.STABLE;
247
+ } else if (change > 0) {
248
+ direction = TrendDirection.UP;
249
+ } else {
250
+ direction = TrendDirection.DOWN;
251
+ }
252
+
253
+ return {
254
+ direction,
255
+ change,
256
+ percentChange,
257
+ first,
258
+ last,
259
+ dataPoints: recent.length
260
+ };
261
+ }
262
+
263
+ getAverage() {
264
+ if (this.dataPoints.length === 0) return 0;
265
+ const sum = this.dataPoints.reduce((acc, dp) => acc + dp.value, 0);
266
+ return sum / this.dataPoints.length;
267
+ }
268
+
269
+ getMin() {
270
+ if (this.dataPoints.length === 0) return null;
271
+ return Math.min(...this.dataPoints.map(dp => dp.value));
272
+ }
273
+
274
+ getMax() {
275
+ if (this.dataPoints.length === 0) return null;
276
+ return Math.max(...this.dataPoints.map(dp => dp.value));
277
+ }
278
+
279
+ toJSON() {
280
+ return {
281
+ trend: this.getTrend(),
282
+ average: this.getAverage(),
283
+ min: this.getMin(),
284
+ max: this.getMax(),
285
+ dataPoints: this.dataPoints.length
286
+ };
287
+ }
288
+ }
289
+
290
+ /**
291
+ * Quality Score Calculator
292
+ */
293
+ class QualityScoreCalculator {
294
+ constructor(options = {}) {
295
+ this.weights = options.weights || {
296
+ coverage: 0.3,
297
+ compliance: 0.3,
298
+ health: 0.2,
299
+ trends: 0.2
300
+ };
301
+ }
302
+
303
+ calculate(metrics) {
304
+ const scores = {
305
+ coverage: this.calculateCoverageScore(metrics.coverage || []),
306
+ compliance: this.calculateComplianceScore(metrics.compliance || []),
307
+ health: this.calculateHealthScore(metrics.health || []),
308
+ trends: this.calculateTrendScore(metrics.trends || [])
309
+ };
310
+
311
+ const overall = Object.entries(this.weights).reduce((acc, [key, weight]) => {
312
+ return acc + (scores[key] * weight);
313
+ }, 0);
314
+
315
+ return {
316
+ overall: Math.round(overall * 100) / 100,
317
+ breakdown: scores,
318
+ grade: this.getGrade(overall),
319
+ weights: this.weights
320
+ };
321
+ }
322
+
323
+ calculateCoverageScore(coverageMetrics) {
324
+ if (coverageMetrics.length === 0) return 0;
325
+ const avg = coverageMetrics.reduce((acc, m) => acc + m.value, 0) / coverageMetrics.length;
326
+ return Math.min(100, avg);
327
+ }
328
+
329
+ calculateComplianceScore(complianceMetrics) {
330
+ if (complianceMetrics.length === 0) return 100;
331
+ const avg = complianceMetrics.reduce((acc, m) => acc + m.value, 0) / complianceMetrics.length;
332
+ return avg;
333
+ }
334
+
335
+ calculateHealthScore(healthIndicators) {
336
+ if (healthIndicators.length === 0) return 100;
337
+
338
+ const statusScores = {
339
+ [HealthStatus.HEALTHY]: 100,
340
+ [HealthStatus.WARNING]: 60,
341
+ [HealthStatus.CRITICAL]: 0,
342
+ [HealthStatus.UNKNOWN]: 50
343
+ };
344
+
345
+ const avg = healthIndicators.reduce((acc, h) => acc + statusScores[h.status], 0) / healthIndicators.length;
346
+ return avg;
347
+ }
348
+
349
+ calculateTrendScore(trends) {
350
+ if (trends.length === 0) return 50;
351
+
352
+ const directionScores = {
353
+ [TrendDirection.UP]: 100,
354
+ [TrendDirection.STABLE]: 70,
355
+ [TrendDirection.DOWN]: 30,
356
+ [TrendDirection.UNKNOWN]: 50
357
+ };
358
+
359
+ const avg = trends.reduce((acc, t) => acc + directionScores[t.direction], 0) / trends.length;
360
+ return avg;
361
+ }
362
+
363
+ getGrade(score) {
364
+ if (score >= 90) return 'A';
365
+ if (score >= 80) return 'B';
366
+ if (score >= 70) return 'C';
367
+ if (score >= 60) return 'D';
368
+ return 'F';
369
+ }
370
+ }
371
+
372
+ /**
373
+ * Quality Metrics Dashboard
374
+ */
375
+ class QualityMetricsDashboard extends EventEmitter {
376
+ constructor(options = {}) {
377
+ super();
378
+ this.name = options.name || 'Quality Dashboard';
379
+ this.metrics = new Map();
380
+ this.healthIndicators = new Map();
381
+ this.trendAnalyzers = new Map();
382
+ this.scoreCalculator = new QualityScoreCalculator(options.weights);
383
+ this.history = [];
384
+ this.maxHistory = options.maxHistory || 1000;
385
+ }
386
+
387
+ // Metric Management
388
+ addMetric(metric) {
389
+ this.metrics.set(metric.name, metric);
390
+ this.emit('metric:added', metric);
391
+ return this;
392
+ }
393
+
394
+ getMetric(name) {
395
+ return this.metrics.get(name);
396
+ }
397
+
398
+ updateMetric(name, value) {
399
+ const metric = this.metrics.get(name);
400
+ if (metric) {
401
+ metric.update(value);
402
+ this.recordHistory('metric', name, value);
403
+ this.emit('metric:updated', metric);
404
+
405
+ // Update trend if exists
406
+ const trend = this.trendAnalyzers.get(name);
407
+ if (trend) {
408
+ trend.addDataPoint(value);
409
+ }
410
+ }
411
+ return this;
412
+ }
413
+
414
+ getMetricsByCategory(category) {
415
+ return [...this.metrics.values()].filter(m => m.category === category);
416
+ }
417
+
418
+ // Health Indicators
419
+ addHealthIndicator(indicator) {
420
+ this.healthIndicators.set(indicator.name, indicator);
421
+ this.emit('health:added', indicator);
422
+ return this;
423
+ }
424
+
425
+ getHealthIndicator(name) {
426
+ return this.healthIndicators.get(name);
427
+ }
428
+
429
+ async checkHealth(name) {
430
+ const indicator = this.healthIndicators.get(name);
431
+ if (indicator) {
432
+ const result = await indicator.check();
433
+ this.emit('health:checked', { name, result });
434
+ return result;
435
+ }
436
+ return null;
437
+ }
438
+
439
+ async checkAllHealth() {
440
+ const results = {};
441
+ for (const [name, indicator] of this.healthIndicators) {
442
+ results[name] = await indicator.check();
443
+ }
444
+ return results;
445
+ }
446
+
447
+ // Trend Analysis
448
+ addTrendAnalyzer(name, options = {}) {
449
+ const analyzer = new TrendAnalyzer(options);
450
+ this.trendAnalyzers.set(name, analyzer);
451
+ return analyzer;
452
+ }
453
+
454
+ getTrendAnalyzer(name) {
455
+ return this.trendAnalyzers.get(name);
456
+ }
457
+
458
+ getTrends() {
459
+ const trends = {};
460
+ for (const [name, analyzer] of this.trendAnalyzers) {
461
+ trends[name] = analyzer.getTrend();
462
+ }
463
+ return trends;
464
+ }
465
+
466
+ // Score Calculation
467
+ calculateScore() {
468
+ const data = {
469
+ coverage: this.getMetricsByCategory(MetricCategory.COVERAGE),
470
+ compliance: this.getMetricsByCategory(MetricCategory.COMPLIANCE),
471
+ health: [...this.healthIndicators.values()],
472
+ trends: Object.values(this.getTrends())
473
+ };
474
+
475
+ const score = this.scoreCalculator.calculate(data);
476
+ this.emit('score:calculated', score);
477
+ return score;
478
+ }
479
+
480
+ // History
481
+ recordHistory(type, name, value) {
482
+ this.history.push({
483
+ type,
484
+ name,
485
+ value,
486
+ timestamp: new Date()
487
+ });
488
+
489
+ if (this.history.length > this.maxHistory) {
490
+ this.history = this.history.slice(-this.maxHistory);
491
+ }
492
+ }
493
+
494
+ getHistory(options = {}) {
495
+ let filtered = this.history;
496
+
497
+ if (options.type) {
498
+ filtered = filtered.filter(h => h.type === options.type);
499
+ }
500
+
501
+ if (options.name) {
502
+ filtered = filtered.filter(h => h.name === options.name);
503
+ }
504
+
505
+ if (options.since) {
506
+ const since = new Date(options.since);
507
+ filtered = filtered.filter(h => h.timestamp >= since);
508
+ }
509
+
510
+ if (options.limit) {
511
+ filtered = filtered.slice(-options.limit);
512
+ }
513
+
514
+ return filtered;
515
+ }
516
+
517
+ // Dashboard Generation
518
+ generateSummary() {
519
+ const score = this.calculateScore();
520
+ const metrics = [...this.metrics.values()].map(m => m.toJSON());
521
+ const health = [...this.healthIndicators.values()].map(h => h.toJSON());
522
+ const trends = this.getTrends();
523
+
524
+ return {
525
+ name: this.name,
526
+ generatedAt: new Date().toISOString(),
527
+ score,
528
+ metrics,
529
+ health,
530
+ trends,
531
+ summary: {
532
+ totalMetrics: metrics.length,
533
+ healthyIndicators: health.filter(h => h.healthy).length,
534
+ totalIndicators: health.length,
535
+ metricsOnTarget: metrics.filter(m => m.onTarget).length
536
+ }
537
+ };
538
+ }
539
+
540
+ generateMarkdownReport() {
541
+ const summary = this.generateSummary();
542
+ const lines = [
543
+ `# ${this.name}`,
544
+ '',
545
+ `Generated: ${summary.generatedAt}`,
546
+ '',
547
+ '## Overall Score',
548
+ '',
549
+ `**Grade: ${summary.score.grade}** (${summary.score.overall}%)`,
550
+ '',
551
+ '### Breakdown',
552
+ '',
553
+ `| Category | Score |`,
554
+ `|----------|-------|`,
555
+ `| Coverage | ${summary.score.breakdown.coverage.toFixed(1)}% |`,
556
+ `| Compliance | ${summary.score.breakdown.compliance.toFixed(1)}% |`,
557
+ `| Health | ${summary.score.breakdown.health.toFixed(1)}% |`,
558
+ `| Trends | ${summary.score.breakdown.trends.toFixed(1)}% |`,
559
+ '',
560
+ '## Metrics',
561
+ ''
562
+ ];
563
+
564
+ for (const metric of summary.metrics) {
565
+ const status = metric.onTarget ? '✅' : (metric.onTarget === false ? '⚠️' : '➖');
566
+ lines.push(`- ${status} **${metric.name}**: ${metric.value.toFixed(1)}${metric.unit === 'percent' ? '%' : ` ${metric.unit}`}`);
567
+ if (metric.target) {
568
+ lines.push(` - Target: ${metric.target}%`);
569
+ }
570
+ }
571
+
572
+ lines.push('', '## Health Indicators', '');
573
+
574
+ for (const indicator of summary.health) {
575
+ const icon = indicator.healthy ? '🟢' : (indicator.status === 'warning' ? '🟡' : '🔴');
576
+ lines.push(`- ${icon} **${indicator.name}**: ${indicator.status}`);
577
+ if (indicator.message) {
578
+ lines.push(` - ${indicator.message}`);
579
+ }
580
+ }
581
+
582
+ lines.push('', '## Trends', '');
583
+
584
+ for (const [name, trend] of Object.entries(summary.trends)) {
585
+ const icon = trend.direction === 'up' ? '📈' : (trend.direction === 'down' ? '📉' : '➡️');
586
+ lines.push(`- ${icon} **${name}**: ${trend.direction} (${trend.percentChange?.toFixed(1) || 0}%)`);
587
+ }
588
+
589
+ return lines.join('\n');
590
+ }
591
+
592
+ // Serialization
593
+ toJSON() {
594
+ return this.generateSummary();
595
+ }
596
+ }
597
+
598
+ /**
599
+ * Factory function
600
+ */
601
+ function createQualityDashboard(options = {}) {
602
+ const dashboard = new QualityMetricsDashboard(options);
603
+
604
+ // Add default metrics if specified
605
+ if (options.includeDefaults !== false) {
606
+ // Coverage metrics
607
+ dashboard.addMetric(new CoverageMetric('Code Coverage', { target: 80 }));
608
+ dashboard.addMetric(new CoverageMetric('Test Coverage', { target: 80 }));
609
+ dashboard.addMetric(new CoverageMetric('Documentation Coverage', { target: 70 }));
610
+
611
+ // Compliance metrics
612
+ const steering = new ComplianceMetric('Steering Compliance', { target: 100 });
613
+ steering.addRule('structure-defined');
614
+ steering.addRule('tech-defined');
615
+ steering.addRule('product-defined');
616
+ dashboard.addMetric(steering);
617
+
618
+ const constitution = new ComplianceMetric('Constitutional Compliance', { target: 100 });
619
+ constitution.addRule('article-1');
620
+ constitution.addRule('article-2');
621
+ constitution.addRule('article-3');
622
+ dashboard.addMetric(constitution);
623
+
624
+ // Trend analyzers
625
+ dashboard.addTrendAnalyzer('Code Coverage');
626
+ dashboard.addTrendAnalyzer('Test Coverage');
627
+ dashboard.addTrendAnalyzer('Steering Compliance');
628
+ }
629
+
630
+ return dashboard;
631
+ }
632
+
633
+ module.exports = {
634
+ // Constants
635
+ MetricCategory,
636
+ HealthStatus,
637
+ TrendDirection,
638
+
639
+ // Classes
640
+ Metric,
641
+ CoverageMetric,
642
+ ComplianceMetric,
643
+ HealthIndicator,
644
+ TrendAnalyzer,
645
+ QualityScoreCalculator,
646
+ QualityMetricsDashboard,
647
+
648
+ // Factory
649
+ createQualityDashboard
650
+ };