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.
- package/README.md +216 -0
- package/bin/cli.js +193 -0
- package/bin/demo-html.js +186 -0
- package/bin/simple-test.js +79 -0
- package/lib/checks/external-specs-urls.js +484 -0
- package/lib/checks/gitignore.js +350 -0
- package/lib/checks/package-json.js +518 -0
- package/lib/checks/spec-files.js +263 -0
- package/lib/checks/specsjson.js +361 -0
- package/lib/file-opener.js +127 -0
- package/lib/formatters.js +176 -0
- package/lib/health-check-orchestrator.js +413 -0
- package/lib/health-check-registry.js +396 -0
- package/lib/health-check-utils.js +234 -0
- package/lib/health-checker.js +145 -0
- package/lib/html-formatter.js +626 -0
- package/lib/index.js +123 -0
- package/lib/providers.js +184 -0
- package/lib/web.js +70 -0
- package/package.json +91 -0
|
@@ -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
|
+
}
|