spec-up-t-healthcheck 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.
@@ -0,0 +1,413 @@
1
+ /**
2
+ * @fileoverview Health check orchestrator module
3
+ *
4
+ * This module coordinates the execution of multiple health checks, manages
5
+ * their sequencing, and generates comprehensive reports. It provides the
6
+ * main interface for running health check suites and handles error recovery.
7
+ *
8
+ * @author spec-up-t-healthcheck
9
+ */
10
+
11
+ import { calculateSummary } from './health-check-utils.js';
12
+ import { globalRegistry } from './health-check-registry.js';
13
+
14
+ /**
15
+ * @typedef {Object} HealthCheckOptions
16
+ * @property {string[]} [checks] - Specific check IDs to run (runs all if not specified)
17
+ * @property {string[]} [categories] - Categories of checks to run
18
+ * @property {boolean} [continueOnError=true] - Whether to continue running checks after failures
19
+ * @property {number} [timeout=30000] - Timeout for individual checks in milliseconds
20
+ * @property {boolean} [parallel=false] - Whether to run checks in parallel (ignores dependencies)
21
+ * @property {Object} [checkOptions={}] - Options to pass to individual health checks
22
+ */
23
+
24
+ /**
25
+ * @typedef {Object} ExecutionContext
26
+ * @property {import('./providers.js').Provider} provider - The provider instance
27
+ * @property {HealthCheckOptions} options - Execution options
28
+ * @property {Map<string, import('./health-check-utils.js').HealthCheckResult>} results - Results map
29
+ * @property {string[]} failures - IDs of failed checks
30
+ * @property {Date} startTime - When execution started
31
+ */
32
+
33
+ /**
34
+ * Health check orchestrator that coordinates running multiple health checks.
35
+ *
36
+ * This class manages the execution flow of health checks, handles dependencies,
37
+ * timeouts, error recovery, and report generation. It provides both sequential
38
+ * and parallel execution modes.
39
+ */
40
+ export class HealthCheckOrchestrator {
41
+ constructor(registry = globalRegistry) {
42
+ this.registry = registry;
43
+ this.defaultOptions = {
44
+ continueOnError: true,
45
+ timeout: 30000,
46
+ parallel: false,
47
+ checkOptions: {}
48
+ };
49
+ }
50
+
51
+ /**
52
+ * Runs a comprehensive health check suite on a specification repository.
53
+ *
54
+ * This is the main entry point for performing health checks. It orchestrates multiple
55
+ * individual checks, aggregates results, and provides detailed reporting with summary
56
+ * statistics. The function supports configurable check selection and provides comprehensive
57
+ * error handling.
58
+ *
59
+ * @param {import('./providers.js').Provider} provider - The provider instance for repository access
60
+ * @param {HealthCheckOptions} [options={}] - Configuration options for the health check run
61
+ * @returns {Promise<import('./health-check-utils.js').HealthCheckReport>} Complete health check report with results and summary
62
+ *
63
+ * @example
64
+ * ```javascript
65
+ * const orchestrator = new HealthCheckOrchestrator();
66
+ * const provider = createLocalProvider('/path/to/repo');
67
+ *
68
+ * // Run all default checks
69
+ * const report = await orchestrator.runHealthChecks(provider);
70
+ *
71
+ * // Run specific checks only
72
+ * const customReport = await orchestrator.runHealthChecks(provider, {
73
+ * checks: ['package-json']
74
+ * });
75
+ *
76
+ * // Run checks by category
77
+ * const categoryReport = await orchestrator.runHealthChecks(provider, {
78
+ * categories: ['configuration', 'content']
79
+ * });
80
+ *
81
+ * console.log(`Health score: ${report.summary.score}%`);
82
+ * console.log(`${report.summary.passed}/${report.summary.total} checks passed`);
83
+ * ```
84
+ */
85
+ async runHealthChecks(provider, options = {}) {
86
+ // Ensure the registry has discovered available checks
87
+ if (!this.registry.autoDiscovered) {
88
+ await this.registry.autoDiscover();
89
+ }
90
+
91
+ const mergedOptions = { ...this.defaultOptions, ...options };
92
+ const context = this.createExecutionContext(provider, mergedOptions);
93
+
94
+ try {
95
+ const checksToRun = this.selectChecksToRun(mergedOptions);
96
+
97
+ if (checksToRun.length === 0) {
98
+ return this.createEmptyReport(context);
99
+ }
100
+
101
+ // Execute checks based on execution mode
102
+ if (mergedOptions.parallel) {
103
+ await this.runChecksInParallel(context, checksToRun);
104
+ } else {
105
+ await this.runChecksSequentially(context, checksToRun);
106
+ }
107
+
108
+ return this.generateReport(context);
109
+
110
+ } catch (error) {
111
+ return this.createErrorReport(context, error);
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Creates an execution context for tracking check execution state.
117
+ *
118
+ * @param {import('./providers.js').Provider} provider - The provider instance
119
+ * @param {HealthCheckOptions} options - Execution options
120
+ * @returns {ExecutionContext} The execution context
121
+ * @private
122
+ */
123
+ createExecutionContext(provider, options) {
124
+ return {
125
+ provider,
126
+ options,
127
+ results: new Map(),
128
+ failures: [],
129
+ startTime: new Date()
130
+ };
131
+ }
132
+
133
+ /**
134
+ * Selects which health checks to run based on options.
135
+ *
136
+ * @param {HealthCheckOptions} options - Execution options
137
+ * @returns {string[]} Array of check IDs to run
138
+ * @private
139
+ */
140
+ selectChecksToRun(options) {
141
+ if (options.checks && options.checks.length > 0) {
142
+ // Validate requested checks exist
143
+ const validChecks = options.checks.filter(id => this.registry.has(id));
144
+ if (validChecks.length !== options.checks.length) {
145
+ const missing = options.checks.filter(id => !this.registry.has(id));
146
+ console.warn(`Requested health checks not found: ${missing.join(', ')}`);
147
+ }
148
+ return validChecks;
149
+ }
150
+
151
+ if (options.categories && options.categories.length > 0) {
152
+ const checksByCategory = [];
153
+ for (const category of options.categories) {
154
+ const categoryChecks = this.registry.getByCategory(category);
155
+ checksByCategory.push(...categoryChecks.map(check => check.id));
156
+ }
157
+ return [...new Set(checksByCategory)]; // Remove duplicates
158
+ }
159
+
160
+ // Return all available checks in execution order
161
+ const orderedChecks = this.registry.getExecutionOrder();
162
+ return orderedChecks.map(check => check.id);
163
+ }
164
+
165
+ /**
166
+ * Runs health checks sequentially, respecting dependencies and priority.
167
+ *
168
+ * @param {ExecutionContext} context - Execution context
169
+ * @param {string[]} checkIds - Array of check IDs to run
170
+ * @private
171
+ */
172
+ async runChecksSequentially(context, checkIds) {
173
+ const orderedChecks = this.registry.getExecutionOrder(checkIds);
174
+
175
+ for (const checkMetadata of orderedChecks) {
176
+ if (!context.options.continueOnError && context.failures.length > 0) {
177
+ break; // Stop on first failure if configured
178
+ }
179
+
180
+ try {
181
+ const result = await this.executeWithTimeout(
182
+ checkMetadata.id,
183
+ context.provider,
184
+ context.options.timeout
185
+ );
186
+
187
+ context.results.set(checkMetadata.id, result);
188
+
189
+ if (result.status === 'fail') {
190
+ context.failures.push(checkMetadata.id);
191
+ }
192
+
193
+ } catch (error) {
194
+ const errorResult = this.createCheckErrorResult(checkMetadata.id, error);
195
+ context.results.set(checkMetadata.id, errorResult);
196
+ context.failures.push(checkMetadata.id);
197
+ }
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Runs health checks in parallel for faster execution.
203
+ *
204
+ * @param {ExecutionContext} context - Execution context
205
+ * @param {string[]} checkIds - Array of check IDs to run
206
+ * @private
207
+ */
208
+ async runChecksInParallel(context, checkIds) {
209
+ const checkPromises = checkIds.map(async (checkId) => {
210
+ try {
211
+ const result = await this.executeWithTimeout(
212
+ checkId,
213
+ context.provider,
214
+ context.options.timeout
215
+ );
216
+ return { checkId, result, error: null };
217
+ } catch (error) {
218
+ return { checkId, result: null, error };
219
+ }
220
+ });
221
+
222
+ const results = await Promise.allSettled(checkPromises);
223
+
224
+ for (const promiseResult of results) {
225
+ if (promiseResult.status === 'fulfilled') {
226
+ const { checkId, result, error } = promiseResult.value;
227
+
228
+ if (error) {
229
+ const errorResult = this.createCheckErrorResult(checkId, error);
230
+ context.results.set(checkId, errorResult);
231
+ context.failures.push(checkId);
232
+ } else {
233
+ context.results.set(checkId, result);
234
+ if (result.status === 'fail') {
235
+ context.failures.push(checkId);
236
+ }
237
+ }
238
+ } else {
239
+ // This shouldn't happen with our current implementation, but handle it
240
+ console.error('Unexpected promise rejection in parallel execution:', promiseResult.reason);
241
+ }
242
+ }
243
+ }
244
+
245
+ /**
246
+ * Executes a health check with timeout protection.
247
+ *
248
+ * @param {string} checkId - The health check ID
249
+ * @param {import('./providers.js').Provider} provider - The provider instance
250
+ * @param {number} timeout - Timeout in milliseconds
251
+ * @returns {Promise<import('./health-check-utils.js').HealthCheckResult>} The check result
252
+ * @private
253
+ */
254
+ async executeWithTimeout(checkId, provider, timeout) {
255
+ return new Promise(async (resolve, reject) => {
256
+ const timeoutHandle = setTimeout(() => {
257
+ reject(new Error(`Health check '${checkId}' timed out after ${timeout}ms`));
258
+ }, timeout);
259
+
260
+ try {
261
+ const result = await this.registry.execute(checkId, provider);
262
+ clearTimeout(timeoutHandle);
263
+ resolve(result);
264
+ } catch (error) {
265
+ clearTimeout(timeoutHandle);
266
+ reject(error);
267
+ }
268
+ });
269
+ }
270
+
271
+ /**
272
+ * Creates an error result for a health check that failed to execute.
273
+ *
274
+ * @param {string} checkId - The health check ID
275
+ * @param {Error} error - The error that occurred
276
+ * @returns {import('./health-check-utils.js').HealthCheckResult} Error result
277
+ * @private
278
+ */
279
+ createCheckErrorResult(checkId, error) {
280
+ return {
281
+ check: checkId,
282
+ status: 'fail',
283
+ message: `Health check execution failed: ${error.message}`,
284
+ timestamp: new Date().toISOString(),
285
+ details: {
286
+ error: error.message,
287
+ executionError: true
288
+ }
289
+ };
290
+ }
291
+
292
+ /**
293
+ * Generates the final health check report.
294
+ *
295
+ * @param {ExecutionContext} context - Execution context
296
+ * @returns {import('./health-check-utils.js').HealthCheckReport} The complete report
297
+ * @private
298
+ */
299
+ generateReport(context) {
300
+ const results = Array.from(context.results.values());
301
+ const summary = calculateSummary(results);
302
+ const executionTime = Date.now() - context.startTime.getTime();
303
+
304
+ return {
305
+ results,
306
+ summary: {
307
+ ...summary,
308
+ executionTimeMs: executionTime,
309
+ executionDate: context.startTime.toISOString()
310
+ },
311
+ timestamp: new Date().toISOString(),
312
+ provider: {
313
+ type: context.provider.type,
314
+ ...(context.provider.repoPath && { repoPath: context.provider.repoPath })
315
+ }
316
+ };
317
+ }
318
+
319
+ /**
320
+ * Creates an empty report when no checks are selected.
321
+ *
322
+ * @param {ExecutionContext} context - Execution context
323
+ * @returns {import('./health-check-utils.js').HealthCheckReport} Empty report
324
+ * @private
325
+ */
326
+ createEmptyReport(context) {
327
+ return {
328
+ results: [],
329
+ summary: {
330
+ total: 0,
331
+ passed: 0,
332
+ failed: 0,
333
+ warnings: 0,
334
+ skipped: 0,
335
+ score: 0,
336
+ hasErrors: false,
337
+ hasWarnings: false,
338
+ executionTimeMs: 0
339
+ },
340
+ timestamp: new Date().toISOString(),
341
+ provider: {
342
+ type: context.provider.type,
343
+ ...(context.provider.repoPath && { repoPath: context.provider.repoPath })
344
+ }
345
+ };
346
+ }
347
+
348
+ /**
349
+ * Creates an error report when orchestration fails.
350
+ *
351
+ * @param {ExecutionContext} context - Execution context
352
+ * @param {Error} error - The orchestration error
353
+ * @returns {import('./health-check-utils.js').HealthCheckReport} Error report
354
+ * @private
355
+ */
356
+ createErrorReport(context, error) {
357
+ const results = Array.from(context.results.values());
358
+ const summary = calculateSummary(results);
359
+
360
+ return {
361
+ results,
362
+ summary: {
363
+ ...summary,
364
+ orchestrationError: error.message
365
+ },
366
+ timestamp: new Date().toISOString(),
367
+ provider: {
368
+ type: context.provider.type,
369
+ ...(context.provider.repoPath && { repoPath: context.provider.repoPath })
370
+ },
371
+ error: {
372
+ message: error.message,
373
+ type: 'orchestration'
374
+ }
375
+ };
376
+ }
377
+
378
+ /**
379
+ * Gets available health checks with their metadata.
380
+ *
381
+ * @returns {Object[]} Array of available health check metadata
382
+ */
383
+ getAvailableChecks() {
384
+ return this.registry.getAllIds().map(id => {
385
+ const metadata = this.registry.get(id);
386
+ return {
387
+ id: metadata.id,
388
+ name: metadata.name,
389
+ description: metadata.description,
390
+ category: metadata.category,
391
+ enabled: metadata.enabled,
392
+ priority: metadata.priority
393
+ };
394
+ });
395
+ }
396
+ }
397
+
398
+ /**
399
+ * Global orchestrator instance for convenient access.
400
+ * @type {HealthCheckOrchestrator}
401
+ */
402
+ export const globalOrchestrator = new HealthCheckOrchestrator();
403
+
404
+ /**
405
+ * Convenience function to run health checks using the global orchestrator.
406
+ *
407
+ * @param {import('./providers.js').Provider} provider - The provider instance
408
+ * @param {HealthCheckOptions} [options={}] - Execution options
409
+ * @returns {Promise<import('./health-check-utils.js').HealthCheckReport>} The health check report
410
+ */
411
+ export async function runHealthChecks(provider, options = {}) {
412
+ return globalOrchestrator.runHealthChecks(provider, options);
413
+ }