aios-core
Version:
Synkra AIOS: AI-Orchestrated System for Full Stack Development - Core Framework
376 lines (335 loc) • 10.7 kB
JavaScript
/**
* Health Check System - Main Entry Point (HCS-2)
*
* Provides comprehensive health checking capabilities for AIOS projects.
* Supports 5 domains: Project Coherence, Local Environment, Repository Health,
* Deployment Environment, and Service Integration.
*
* NOTE (INS-4.8): This is a SEPARATE system from `core/doctor/` (INS-4.1).
* The primary diagnostic interface is `aios doctor` (bin/aios.js → core/doctor/).
* The agent-facing task `*health-check` now delegates to `aios doctor --json`
* and no longer calls this module. This module is preserved for potential
* programmatic use but is NOT the primary health check mechanism.
*
* @module aios-core/health-check
* @version 1.0.0
* @story HCS-2 - Health Check System Implementation
* @see core/doctor/ for the primary diagnostic system (15 checks)
*/
const HealthCheckEngine = require('./engine');
const { BaseCheck, CheckSeverity, CheckStatus } = require('./base-check');
const CheckRegistry = require('./check-registry');
const HealerManager = require('./healers');
const ReporterManager = require('./reporters');
/**
* Default configuration for health checks
*/
const DEFAULT_CONFIG = {
mode: 'quick',
domains: ['project', 'local', 'repository', 'deployment', 'services'],
autoFix: true,
autoFixTier: 1,
parallel: true,
cache: {
enabled: true,
ttl: 300000, // 5 minutes
},
performance: {
quickModeTimeout: 10000, // 10s
fullModeTimeout: 60000, // 60s
},
output: {
format: 'console',
verbose: false,
colors: true,
},
};
/**
* Main Health Check class
*
* Orchestrates health check execution, healing, and reporting.
*
* @class HealthCheck
* @example
* const healthCheck = new HealthCheck({ mode: 'full' });
* const results = await healthCheck.run();
* console.log(results.overall.score);
*/
class HealthCheck {
/**
* Create a new HealthCheck instance
* @param {Object} config - Configuration options
* @param {string} [config.mode='quick'] - Execution mode ('quick' or 'full')
* @param {string[]} [config.domains] - Domains to check
* @param {boolean} [config.autoFix=true] - Enable auto-fixing
* @param {number} [config.autoFixTier=1] - Maximum tier for auto-fix (1-3)
* @param {boolean} [config.parallel=true] - Run checks in parallel
* @param {Object} [config.cache] - Cache configuration
* @param {Object} [config.performance] - Performance settings
* @param {Object} [config.output] - Output settings
*/
constructor(config = {}) {
this.config = { ...DEFAULT_CONFIG, ...config };
this.engine = new HealthCheckEngine(this.config);
this.registry = new CheckRegistry();
this.healers = new HealerManager(this.config);
this.reporters = new ReporterManager(this.config);
this.results = null;
this.lastRun = null;
}
/**
* Run health checks
*
* @param {Object} options - Run options (overrides config)
* @param {string} [options.mode] - Execution mode
* @param {string} [options.domain] - Specific domain to check ('all' for all domains)
* @param {boolean} [options.autoFix] - Enable auto-fixing
* @param {boolean} [options.verbose] - Enable verbose output
* @returns {Promise<HealthCheckResults>} Health check results
*/
async run(options = {}) {
const runConfig = {
...this.config,
...options,
};
const startTime = Date.now();
try {
// Select checks based on mode and domain
const selectedChecks = this.selectChecks(runConfig);
// Run all checks
const checkResults = await this.engine.runChecks(selectedChecks, runConfig);
// Apply auto-fixes if enabled
let healingResults = [];
if (runConfig.autoFix) {
healingResults = await this.healers.applyFixes(checkResults, runConfig.autoFixTier);
}
// Calculate scores
const scores = this.calculateScores(checkResults);
// Generate report
const report = await this.reporters.generate(checkResults, scores, healingResults, runConfig);
// Store results
this.results = {
timestamp: new Date().toISOString(),
version: '1.0.0',
mode: runConfig.mode,
duration: `${Date.now() - startTime}ms`,
overall: scores.overall,
domains: scores.domains,
checks: checkResults,
autoFixed: healingResults.filter((h) => h.success),
techDebt: this.extractTechDebt(checkResults),
report,
};
this.lastRun = Date.now();
return this.results;
} catch (error) {
throw new Error(`Health check failed: ${error.message}`);
}
}
/**
* Select checks based on mode and domain
* @private
* @param {Object} config - Run configuration
* @returns {BaseCheck[]} Selected checks
*/
selectChecks(config) {
const { mode, domain, domains } = config;
// Get all registered checks
let checks = this.registry.getAllChecks();
// Filter by domain
if (domain && domain !== 'all') {
checks = checks.filter((c) => c.domain === domain);
} else if (domains && domains.length > 0) {
checks = checks.filter((c) => domains.includes(c.domain));
}
// Filter by mode
if (mode === 'quick') {
// Quick mode: only CRITICAL and HIGH severity
checks = checks.filter(
(c) => c.severity === CheckSeverity.CRITICAL || c.severity === CheckSeverity.HIGH,
);
}
// Sort by priority (CRITICAL first)
checks.sort((a, b) => {
const priorityOrder = {
[CheckSeverity.CRITICAL]: 0,
[CheckSeverity.HIGH]: 1,
[CheckSeverity.MEDIUM]: 2,
[CheckSeverity.LOW]: 3,
[CheckSeverity.INFO]: 4,
};
return priorityOrder[a.severity] - priorityOrder[b.severity];
});
return checks;
}
/**
* Calculate health scores from check results
* @private
* @param {Object[]} checkResults - Array of check results
* @returns {Object} Scores object with overall and per-domain scores
*/
calculateScores(checkResults) {
const weights = {
[CheckSeverity.CRITICAL]: 25,
[CheckSeverity.HIGH]: 15,
[CheckSeverity.MEDIUM]: 7,
[CheckSeverity.LOW]: 3,
[CheckSeverity.INFO]: 0,
};
// Calculate overall score
let maxPenalty = 0;
let actualPenalty = 0;
for (const result of checkResults) {
const weight = weights[result.severity] || 0;
maxPenalty += weight;
if (result.status !== CheckStatus.PASS) {
actualPenalty += weight;
}
}
const overallScore =
maxPenalty > 0 ? Math.round(100 - (actualPenalty / maxPenalty) * 100) : 100;
// Calculate per-domain scores
const domains = {};
const domainResults = this.groupByDomain(checkResults);
for (const [domain, results] of Object.entries(domainResults)) {
let domainMaxPenalty = 0;
let domainActualPenalty = 0;
for (const result of results) {
const weight = weights[result.severity] || 0;
domainMaxPenalty += weight;
if (result.status !== CheckStatus.PASS) {
domainActualPenalty += weight;
}
}
const domainScore =
domainMaxPenalty > 0
? Math.round(100 - (domainActualPenalty / domainMaxPenalty) * 100)
: 100;
const passedCount = results.filter((r) => r.status === CheckStatus.PASS).length;
const failedCount = results.filter(
(r) => r.status === CheckStatus.FAIL || r.status === CheckStatus.ERROR,
).length;
const warningCount = results.filter((r) => r.status === CheckStatus.WARNING).length;
domains[domain] = {
score: domainScore,
status: this.scoreToStatus(domainScore),
checks: results.map((r) => ({
name: r.name,
status: r.status,
severity: r.severity,
message: r.message,
})),
summary: {
total: results.length,
passed: passedCount,
failed: failedCount,
warnings: warningCount,
},
};
}
return {
overall: {
score: overallScore,
status: this.scoreToStatus(overallScore),
issuesCount: checkResults.filter((r) => r.status !== CheckStatus.PASS).length,
autoFixedCount: 0, // Will be updated after healing
},
domains,
};
}
/**
* Convert score to status string
* @private
* @param {number} score - Score (0-100)
* @returns {string} Status string
*/
scoreToStatus(score) {
if (score >= 90) return 'healthy';
if (score >= 70) return 'degraded';
if (score >= 50) return 'warning';
return 'critical';
}
/**
* Group check results by domain
* @private
* @param {Object[]} checkResults - Array of check results
* @returns {Object} Results grouped by domain
*/
groupByDomain(checkResults) {
return checkResults.reduce((acc, result) => {
const domain = result.domain || 'unknown';
if (!acc[domain]) {
acc[domain] = [];
}
acc[domain].push(result);
return acc;
}, {});
}
/**
* Extract technical debt items from check results
* @private
* @param {Object[]} checkResults - Array of check results
* @returns {Object[]} Technical debt items
*/
extractTechDebt(checkResults) {
return checkResults
.filter(
(r) =>
r.status === CheckStatus.WARNING ||
(r.severity === CheckSeverity.LOW && r.status !== CheckStatus.PASS),
)
.map((r) => ({
id: r.checkId,
name: r.name,
domain: r.domain,
severity: r.severity,
description: r.message,
recommendation: r.recommendation || 'Address when possible',
firstDetected: new Date().toISOString(),
}));
}
/**
* Get available domains
* @returns {string[]} Array of domain names
*/
getDomains() {
return ['project', 'local', 'repository', 'deployment', 'services'];
}
/**
* Get check count by domain
* @returns {Object} Object with domain -> count mapping
*/
getCheckCounts() {
const counts = {};
for (const domain of this.getDomains()) {
counts[domain] = this.registry.getChecksByDomain(domain).length;
}
return counts;
}
/**
* Clear cached results
*/
clearCache() {
this.engine.clearCache();
this.results = null;
}
/**
* Get last run results
* @returns {HealthCheckResults|null} Last run results or null
*/
getLastResults() {
return this.results;
}
}
// Export main class and utilities
module.exports = {
HealthCheck,
HealthCheckEngine,
BaseCheck,
CheckSeverity,
CheckStatus,
CheckRegistry,
HealerManager,
ReporterManager,
DEFAULT_CONFIG,
};