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,396 @@
1
+ /**
2
+ * @fileoverview Health check registry system
3
+ *
4
+ * This module provides a registry system for discovering, registering, and managing
5
+ * health check modules. It enables dynamic loading of health checks and provides
6
+ * a centralized way to manage available checks without hardcoding dependencies.
7
+ *
8
+ * @author spec-up-t-healthcheck
9
+ */
10
+
11
+ import { isValidHealthCheckResult } from './health-check-utils.js';
12
+
13
+ /**
14
+ * @typedef {Object} HealthCheckMetadata
15
+ * @property {string} id - Unique identifier for the health check
16
+ * @property {string} name - Human-readable name
17
+ * @property {string} description - Description of what the check validates
18
+ * @property {function} checkFunction - The actual health check function
19
+ * @property {string} [category='general'] - Category for grouping checks
20
+ * @property {number} [priority=100] - Execution priority (lower = higher priority)
21
+ * @property {string[]} [dependencies=[]] - IDs of checks that must run before this one
22
+ * @property {boolean} [enabled=true] - Whether the check is enabled by default
23
+ */
24
+
25
+ /**
26
+ * Registry for managing health check modules.
27
+ * This class provides a centralized system for registering, discovering,
28
+ * and executing health checks in a modular fashion.
29
+ */
30
+ export class HealthCheckRegistry {
31
+ constructor() {
32
+ /** @type {Map<string, HealthCheckMetadata>} */
33
+ this.checks = new Map();
34
+
35
+ /** @type {Set<string>} */
36
+ this.categories = new Set(['general']);
37
+
38
+ /** @type {boolean} */
39
+ this.autoDiscovered = false;
40
+ }
41
+
42
+ /**
43
+ * Registers a health check with the registry.
44
+ *
45
+ * @param {HealthCheckMetadata} metadata - The health check metadata
46
+ * @throws {Error} If the check metadata is invalid or ID already exists
47
+ *
48
+ * @example
49
+ * ```javascript
50
+ * registry.register({
51
+ * id: 'my-check',
52
+ * name: 'My Custom Check',
53
+ * description: 'Validates something important',
54
+ * checkFunction: async (provider) => { ... }
55
+ * });
56
+ * ```
57
+ */
58
+ register(metadata) {
59
+ this.validateMetadata(metadata);
60
+
61
+ if (this.checks.has(metadata.id)) {
62
+ throw new Error(`Health check with ID '${metadata.id}' is already registered`);
63
+ }
64
+
65
+ // Set defaults for optional fields
66
+ const fullMetadata = {
67
+ category: 'general',
68
+ priority: 100,
69
+ dependencies: [],
70
+ enabled: true,
71
+ ...metadata
72
+ };
73
+
74
+ this.checks.set(metadata.id, fullMetadata);
75
+ this.categories.add(fullMetadata.category);
76
+ }
77
+
78
+ /**
79
+ * Validates health check metadata structure.
80
+ *
81
+ * @param {HealthCheckMetadata} metadata - The metadata to validate
82
+ * @throws {Error} If metadata is invalid
83
+ * @private
84
+ */
85
+ validateMetadata(metadata) {
86
+ if (!metadata || typeof metadata !== 'object') {
87
+ throw new Error('Health check metadata must be an object');
88
+ }
89
+
90
+ const requiredFields = ['id', 'name', 'description', 'checkFunction'];
91
+ for (const field of requiredFields) {
92
+ if (!metadata[field]) {
93
+ throw new Error(`Health check metadata missing required field: ${field}`);
94
+ }
95
+ }
96
+
97
+ if (typeof metadata.id !== 'string' || metadata.id.trim() === '') {
98
+ throw new Error('Health check ID must be a non-empty string');
99
+ }
100
+
101
+ if (typeof metadata.checkFunction !== 'function') {
102
+ throw new Error('Health check function must be a function');
103
+ }
104
+
105
+ // Validate optional fields if present
106
+ if (metadata.priority !== undefined && (typeof metadata.priority !== 'number' || metadata.priority < 0)) {
107
+ throw new Error('Health check priority must be a non-negative number');
108
+ }
109
+
110
+ if (metadata.dependencies && !Array.isArray(metadata.dependencies)) {
111
+ throw new Error('Health check dependencies must be an array');
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Unregisters a health check from the registry.
117
+ *
118
+ * @param {string} id - The ID of the health check to unregister
119
+ * @returns {boolean} True if the check was removed, false if it wasn't found
120
+ */
121
+ unregister(id) {
122
+ return this.checks.delete(id);
123
+ }
124
+
125
+ /**
126
+ * Gets metadata for a specific health check.
127
+ *
128
+ * @param {string} id - The ID of the health check
129
+ * @returns {HealthCheckMetadata|undefined} The metadata or undefined if not found
130
+ */
131
+ get(id) {
132
+ return this.checks.get(id);
133
+ }
134
+
135
+ /**
136
+ * Gets all registered health check IDs.
137
+ *
138
+ * @returns {string[]} Array of health check IDs
139
+ */
140
+ getAllIds() {
141
+ return Array.from(this.checks.keys());
142
+ }
143
+
144
+ /**
145
+ * Gets health checks filtered by category.
146
+ *
147
+ * @param {string} category - The category to filter by
148
+ * @returns {HealthCheckMetadata[]} Array of health checks in the category
149
+ */
150
+ getByCategory(category) {
151
+ return Array.from(this.checks.values()).filter(check => check.category === category);
152
+ }
153
+
154
+ /**
155
+ * Gets all available categories.
156
+ *
157
+ * @returns {string[]} Array of category names
158
+ */
159
+ getCategories() {
160
+ return Array.from(this.categories);
161
+ }
162
+
163
+ /**
164
+ * Checks if a health check is registered.
165
+ *
166
+ * @param {string} id - The ID to check
167
+ * @returns {boolean} True if the check is registered
168
+ */
169
+ has(id) {
170
+ return this.checks.has(id);
171
+ }
172
+
173
+ /**
174
+ * Gets health checks sorted by priority and dependencies.
175
+ *
176
+ * This method returns checks in an order that respects dependencies
177
+ * and priority settings, ensuring checks run in the correct sequence.
178
+ *
179
+ * @param {string[]} [requestedIds] - Specific check IDs to include (optional)
180
+ * @returns {HealthCheckMetadata[]} Ordered array of health checks
181
+ */
182
+ getExecutionOrder(requestedIds) {
183
+ const availableChecks = requestedIds
184
+ ? requestedIds.map(id => this.get(id)).filter(Boolean)
185
+ : Array.from(this.checks.values());
186
+
187
+ // Filter only enabled checks
188
+ const enabledChecks = availableChecks.filter(check => check.enabled);
189
+
190
+ // Sort by priority, then by ID for consistent ordering
191
+ return enabledChecks.sort((a, b) => {
192
+ if (a.priority !== b.priority) {
193
+ return a.priority - b.priority;
194
+ }
195
+ return a.id.localeCompare(b.id);
196
+ });
197
+ }
198
+
199
+ /**
200
+ * Executes a specific health check.
201
+ *
202
+ * @param {string} id - The ID of the health check to execute
203
+ * @param {import('./providers.js').Provider} provider - The provider instance
204
+ * @returns {Promise<import('./health-check-utils.js').HealthCheckResult>} The check result
205
+ * @throws {Error} If the check is not registered or execution fails
206
+ */
207
+ async execute(id, provider) {
208
+ const metadata = this.get(id);
209
+ if (!metadata) {
210
+ throw new Error(`Health check '${id}' is not registered`);
211
+ }
212
+
213
+ if (!metadata.enabled) {
214
+ throw new Error(`Health check '${id}' is disabled`);
215
+ }
216
+
217
+ try {
218
+ const result = await metadata.checkFunction(provider);
219
+
220
+ // Validate the result structure
221
+ if (!isValidHealthCheckResult(result)) {
222
+ throw new Error(`Health check '${id}' returned invalid result structure`);
223
+ }
224
+
225
+ return result;
226
+ } catch (error) {
227
+ throw new Error(`Failed to execute health check '${id}': ${error.message}`);
228
+ }
229
+ }
230
+
231
+ /**
232
+ * Automatically discovers and registers health checks from the checks directory.
233
+ *
234
+ * This method dynamically imports health check modules and registers them
235
+ * with the registry. It's designed to work with the standard module structure.
236
+ */
237
+ async autoDiscover() {
238
+ if (this.autoDiscovered) {
239
+ return; // Avoid duplicate discovery
240
+ }
241
+
242
+ try {
243
+ // Import the built-in health checks
244
+ const packageJsonModule = await import('./checks/package-json.js');
245
+ const specFilesModule = await import('./checks/spec-files.js');
246
+ const specsJsonModule = await import('./checks/specsjson.js');
247
+ const externalSpecsUrlsModule = await import('./checks/external-specs-urls.js');
248
+ const gitignoreModule = await import('./checks/gitignore.js');
249
+
250
+ // Register package.json check
251
+ if (packageJsonModule.checkPackageJson && packageJsonModule.CHECK_ID) {
252
+ this.register({
253
+ id: packageJsonModule.CHECK_ID,
254
+ name: packageJsonModule.CHECK_NAME || 'Package.json Check',
255
+ description: packageJsonModule.CHECK_DESCRIPTION || 'Validates package.json file',
256
+ checkFunction: packageJsonModule.checkPackageJson,
257
+ category: 'configuration',
258
+ priority: 10 // High priority for configuration checks
259
+ });
260
+ }
261
+
262
+ // Register spec files check
263
+ if (specFilesModule.checkSpecFiles && specFilesModule.CHECK_ID) {
264
+ this.register({
265
+ id: specFilesModule.CHECK_ID,
266
+ name: specFilesModule.CHECK_NAME || 'Specification Files Check',
267
+ description: specFilesModule.CHECK_DESCRIPTION || 'Discovers specification files',
268
+ checkFunction: specFilesModule.checkSpecFiles,
269
+ category: 'content',
270
+ priority: 20 // Lower priority, content checks can run after configuration
271
+ });
272
+ }
273
+
274
+ // Register specs.json check
275
+ if (specsJsonModule.checkSpecsJson && specsJsonModule.CHECK_ID) {
276
+ this.register({
277
+ id: specsJsonModule.CHECK_ID,
278
+ name: specsJsonModule.CHECK_NAME || 'Specs.json Check',
279
+ description: specsJsonModule.CHECK_DESCRIPTION || 'Validates specs.json file',
280
+ checkFunction: specsJsonModule.checkSpecsJson,
281
+ category: 'configuration',
282
+ priority: 15 // Between package-json and spec-files
283
+ });
284
+ }
285
+
286
+ // Register external specs URLs check
287
+ if (externalSpecsUrlsModule.checkExternalSpecsUrls && externalSpecsUrlsModule.CHECK_ID) {
288
+ this.register({
289
+ id: externalSpecsUrlsModule.CHECK_ID,
290
+ name: externalSpecsUrlsModule.CHECK_NAME || 'External Specs URL Validation',
291
+ description: externalSpecsUrlsModule.CHECK_DESCRIPTION || 'Validates external specification URLs',
292
+ checkFunction: externalSpecsUrlsModule.checkExternalSpecsUrls,
293
+ category: 'external-references',
294
+ priority: 30 // Run after specs.json is validated
295
+ });
296
+ }
297
+
298
+ // Register .gitignore check
299
+ if (gitignoreModule.checkGitignore && gitignoreModule.CHECK_ID) {
300
+ this.register({
301
+ id: gitignoreModule.CHECK_ID,
302
+ name: gitignoreModule.CHECK_NAME || '.gitignore Validation',
303
+ description: gitignoreModule.CHECK_DESCRIPTION || 'Validates .gitignore file',
304
+ checkFunction: gitignoreModule.checkGitignore,
305
+ category: 'configuration',
306
+ priority: 12 // After package.json, before specs.json
307
+ });
308
+ }
309
+
310
+ this.autoDiscovered = true;
311
+ } catch (error) {
312
+ console.warn('Failed to auto-discover some health checks:', error.message);
313
+ }
314
+ }
315
+
316
+ /**
317
+ * Gets a summary of the registry state.
318
+ *
319
+ * @returns {Object} Summary information about registered checks
320
+ */
321
+ getSummary() {
322
+ const checks = Array.from(this.checks.values());
323
+
324
+ return {
325
+ totalChecks: checks.length,
326
+ enabledChecks: checks.filter(c => c.enabled).length,
327
+ disabledChecks: checks.filter(c => !c.enabled).length,
328
+ categories: this.getCategories(),
329
+ checksByCategory: Object.fromEntries(
330
+ this.getCategories().map(cat => [cat, this.getByCategory(cat).length])
331
+ )
332
+ };
333
+ }
334
+
335
+ /**
336
+ * Enables or disables a health check.
337
+ *
338
+ * @param {string} id - The ID of the health check
339
+ * @param {boolean} enabled - Whether to enable or disable the check
340
+ * @returns {boolean} True if the check was found and updated
341
+ */
342
+ setEnabled(id, enabled) {
343
+ const metadata = this.get(id);
344
+ if (metadata) {
345
+ metadata.enabled = enabled;
346
+ return true;
347
+ }
348
+ return false;
349
+ }
350
+
351
+ /**
352
+ * Clears all registered health checks.
353
+ *
354
+ * This method is primarily useful for testing or when reinitializing
355
+ * the registry with a different set of checks.
356
+ */
357
+ clear() {
358
+ this.checks.clear();
359
+ this.categories.clear();
360
+ this.categories.add('general');
361
+ this.autoDiscovered = false;
362
+ }
363
+ }
364
+
365
+ /**
366
+ * Global registry instance for convenient access.
367
+ * Most applications should use this singleton instance.
368
+ * @type {HealthCheckRegistry}
369
+ */
370
+ export const globalRegistry = new HealthCheckRegistry();
371
+
372
+ /**
373
+ * Convenience function to register a health check with the global registry.
374
+ *
375
+ * @param {HealthCheckMetadata} metadata - The health check metadata
376
+ */
377
+ export function registerHealthCheck(metadata) {
378
+ globalRegistry.register(metadata);
379
+ }
380
+
381
+ /**
382
+ * Convenience function to get a health check from the global registry.
383
+ *
384
+ * @param {string} id - The health check ID
385
+ * @returns {HealthCheckMetadata|undefined} The health check metadata
386
+ */
387
+ export function getHealthCheck(id) {
388
+ return globalRegistry.get(id);
389
+ }
390
+
391
+ /**
392
+ * Convenience function to auto-discover health checks using the global registry.
393
+ */
394
+ export async function autoDiscoverHealthChecks() {
395
+ await globalRegistry.autoDiscover();
396
+ }
@@ -0,0 +1,234 @@
1
+ /**
2
+ * @fileoverview Health check utilities and common types for spec-up-t-healthcheck
3
+ *
4
+ * This module provides shared utilities, type definitions, and helper functions
5
+ * used across all health check modules. It ensures consistency in health check
6
+ * result formatting and provides a centralized location for common functionality.
7
+ *
8
+ * @author spec-up-t-healthcheck
9
+ */
10
+
11
+ /**
12
+ * @typedef {Object} HealthCheckResult
13
+ * @property {string} check - The name/identifier of the health check
14
+ * @property {'pass'|'fail'|'warn'|'skip'} status - The result status
15
+ * @property {string} message - Human-readable result message
16
+ * @property {string} timestamp - ISO timestamp when the check was performed
17
+ * @property {Object} [details={}] - Additional details about the check result
18
+ */
19
+
20
+ /**
21
+ * @typedef {Object} HealthCheckSummary
22
+ * @property {number} total - Total number of checks performed
23
+ * @property {number} passed - Number of checks that passed
24
+ * @property {number} failed - Number of checks that failed
25
+ * @property {number} warnings - Number of checks with warnings
26
+ * @property {number} skipped - Number of checks that were skipped
27
+ * @property {number} score - Overall health score as a percentage (0-100)
28
+ * @property {boolean} hasErrors - Whether any checks failed
29
+ * @property {boolean} hasWarnings - Whether any checks had warnings
30
+ */
31
+
32
+ /**
33
+ * @typedef {Object} HealthCheckReport
34
+ * @property {HealthCheckResult[]} results - Array of individual check results
35
+ * @property {HealthCheckSummary} summary - Aggregated summary of all check results
36
+ * @property {string} timestamp - ISO timestamp when the report was generated
37
+ * @property {Object} provider - Information about the provider used for checks
38
+ * @property {string} provider.type - The type of provider ('local', 'remote', etc.)
39
+ * @property {string} [provider.repoPath] - The repository path (for local providers)
40
+ */
41
+
42
+ /**
43
+ * @typedef {function(import('./providers.js').Provider): Promise<HealthCheckResult>} HealthCheckFunction
44
+ * @description A function that performs a health check using a provider and returns a result
45
+ */
46
+
47
+ /**
48
+ * Valid status values for health check results.
49
+ * @type {readonly string[]}
50
+ */
51
+ export const HEALTH_CHECK_STATUSES = Object.freeze(['pass', 'fail', 'warn', 'skip']);
52
+
53
+ /**
54
+ * Creates a standardized health check result object.
55
+ *
56
+ * This utility function ensures consistent structure across all health check results,
57
+ * automatically adding timestamps and providing a standard format for reporting.
58
+ * The function validates input parameters to maintain data integrity.
59
+ *
60
+ * @param {string} check - The identifier/name of the health check being performed
61
+ * @param {'pass'|'fail'|'warn'|'skip'} status - The status of the health check
62
+ * @param {string} message - A human-readable message describing the result
63
+ * @param {Object} [details={}] - Optional additional details about the check
64
+ * @returns {HealthCheckResult} A standardized health check result object
65
+ * @throws {Error} If check name or message is empty, or status is invalid
66
+ *
67
+ * @example
68
+ * ```javascript
69
+ * const result = createHealthCheckResult(
70
+ * 'package-json',
71
+ * 'pass',
72
+ * 'package.json is valid',
73
+ * { packageData: { name: 'my-spec', version: '1.0.0' } }
74
+ * );
75
+ * ```
76
+ */
77
+ export function createHealthCheckResult(check, status, message, details = {}) {
78
+ // Input validation to ensure data integrity
79
+ if (typeof check !== 'string' || check.trim() === '') {
80
+ throw new Error('Check name must be a non-empty string');
81
+ }
82
+
83
+ if (typeof message !== 'string' || message.trim() === '') {
84
+ throw new Error('Message must be a non-empty string');
85
+ }
86
+
87
+ if (!HEALTH_CHECK_STATUSES.includes(status)) {
88
+ throw new Error(`Status must be one of: ${HEALTH_CHECK_STATUSES.join(', ')}`);
89
+ }
90
+
91
+ if (details !== null && typeof details !== 'object') {
92
+ throw new Error('Details must be an object or null');
93
+ }
94
+
95
+ return {
96
+ check: check.trim(),
97
+ status,
98
+ message: message.trim(),
99
+ timestamp: new Date().toISOString(),
100
+ details: details || {}
101
+ };
102
+ }
103
+
104
+ /**
105
+ * Calculates summary statistics from an array of health check results.
106
+ *
107
+ * This function aggregates individual health check results into a comprehensive
108
+ * summary that includes counts, percentages, and boolean flags for quick
109
+ * assessment of overall repository health.
110
+ *
111
+ * @param {HealthCheckResult[]} results - Array of health check results to summarize
112
+ * @returns {HealthCheckSummary} Aggregated summary statistics
113
+ *
114
+ * @example
115
+ * ```javascript
116
+ * const results = [
117
+ * createHealthCheckResult('check1', 'pass', 'All good'),
118
+ * createHealthCheckResult('check2', 'fail', 'Found issue')
119
+ * ];
120
+ * const summary = calculateSummary(results);
121
+ * console.log(summary.score); // 50 (50% passed)
122
+ * ```
123
+ */
124
+ export function calculateSummary(results) {
125
+ if (!Array.isArray(results)) {
126
+ throw new Error('Results must be an array');
127
+ }
128
+
129
+ const summary = {
130
+ total: results.length,
131
+ passed: results.filter(r => r.status === 'pass').length,
132
+ failed: results.filter(r => r.status === 'fail').length,
133
+ warnings: results.filter(r => r.status === 'warn').length,
134
+ skipped: results.filter(r => r.status === 'skip').length
135
+ };
136
+
137
+ // Calculate health score as percentage of passed checks
138
+ summary.score = summary.total > 0 ? Math.round((summary.passed / summary.total) * 100) : 0;
139
+
140
+ // Boolean flags for quick status assessment
141
+ summary.hasErrors = summary.failed > 0;
142
+ summary.hasWarnings = summary.warnings > 0;
143
+
144
+ return summary;
145
+ }
146
+
147
+ /**
148
+ * Validates that an object conforms to the HealthCheckResult interface.
149
+ *
150
+ * This function performs runtime validation of health check result objects
151
+ * to ensure they meet the expected structure and data types. Useful for
152
+ * validating results from external or dynamic sources.
153
+ *
154
+ * @param {any} result - The object to validate
155
+ * @returns {boolean} True if the object is a valid HealthCheckResult
156
+ *
157
+ * @example
158
+ * ```javascript
159
+ * const result = { check: 'test', status: 'pass', message: 'OK', timestamp: '...' };
160
+ * if (isValidHealthCheckResult(result)) {
161
+ * // Safe to use as HealthCheckResult
162
+ * }
163
+ * ```
164
+ */
165
+ export function isValidHealthCheckResult(result) {
166
+ if (!result || typeof result !== 'object') {
167
+ return false;
168
+ }
169
+
170
+ const requiredFields = ['check', 'status', 'message', 'timestamp'];
171
+
172
+ // Check all required fields are present and have correct types
173
+ for (const field of requiredFields) {
174
+ if (!(field in result) || typeof result[field] !== 'string') {
175
+ return false;
176
+ }
177
+ }
178
+
179
+ // Validate status is a known value
180
+ if (!HEALTH_CHECK_STATUSES.includes(result.status)) {
181
+ return false;
182
+ }
183
+
184
+ // Validate timestamp is a valid ISO string
185
+ if (isNaN(Date.parse(result.timestamp))) {
186
+ return false;
187
+ }
188
+
189
+ // Details field is optional but must be an object if present
190
+ if ('details' in result && (result.details === null || typeof result.details !== 'object')) {
191
+ return false;
192
+ }
193
+
194
+ return true;
195
+ }
196
+
197
+ /**
198
+ * Creates a standardized error result for health checks that encounter exceptions.
199
+ *
200
+ * This utility function provides a consistent way to handle and report errors
201
+ * that occur during health check execution, ensuring proper error information
202
+ * is captured and formatted for reporting.
203
+ *
204
+ * @param {string} check - The identifier of the health check that encountered an error
205
+ * @param {Error|string} error - The error object or error message
206
+ * @param {Object} [additionalDetails={}] - Additional context about the error
207
+ * @returns {HealthCheckResult} A standardized error result
208
+ *
209
+ * @example
210
+ * ```javascript
211
+ * try {
212
+ * // Health check logic
213
+ * } catch (error) {
214
+ * return createErrorResult('my-check', error, { context: 'additional info' });
215
+ * }
216
+ * ```
217
+ */
218
+ export function createErrorResult(check, error, additionalDetails = {}) {
219
+ const errorMessage = error instanceof Error ? error.message : String(error);
220
+ const errorStack = error instanceof Error ? error.stack : undefined;
221
+
222
+ const details = {
223
+ error: errorMessage,
224
+ ...(errorStack && { stack: errorStack }),
225
+ ...additionalDetails
226
+ };
227
+
228
+ return createHealthCheckResult(
229
+ check,
230
+ 'fail',
231
+ `Error during health check: ${errorMessage}`,
232
+ details
233
+ );
234
+ }