vibecodingmachine-core 2026.1.3-2209 → 2026.1.23-1010

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 (32) hide show
  1. package/__tests__/provider-manager-fallback.test.js +43 -0
  2. package/__tests__/provider-manager-rate-limit.test.js +61 -0
  3. package/package.json +1 -1
  4. package/src/compliance/compliance-manager.js +5 -2
  5. package/src/database/migrations.js +135 -12
  6. package/src/database/user-database-client.js +63 -8
  7. package/src/database/user-schema.js +7 -0
  8. package/src/health-tracking/__tests__/ide-health-tracker.test.js +420 -0
  9. package/src/health-tracking/__tests__/interaction-recorder.test.js +392 -0
  10. package/src/health-tracking/errors.js +50 -0
  11. package/src/health-tracking/health-reporter.js +331 -0
  12. package/src/health-tracking/ide-health-tracker.js +446 -0
  13. package/src/health-tracking/interaction-recorder.js +161 -0
  14. package/src/health-tracking/json-storage.js +276 -0
  15. package/src/health-tracking/storage-interface.js +63 -0
  16. package/src/health-tracking/validators.js +277 -0
  17. package/src/ide-integration/applescript-manager.cjs +1062 -4
  18. package/src/ide-integration/applescript-manager.js +560 -11
  19. package/src/ide-integration/provider-manager.cjs +158 -28
  20. package/src/ide-integration/quota-detector.cjs +339 -16
  21. package/src/ide-integration/quota-detector.js +6 -1
  22. package/src/index.cjs +32 -1
  23. package/src/index.js +16 -0
  24. package/src/localization/translations/en.js +13 -1
  25. package/src/localization/translations/es.js +12 -0
  26. package/src/utils/admin-utils.js +33 -0
  27. package/src/utils/error-reporter.js +12 -4
  28. package/src/utils/requirement-helpers.js +34 -4
  29. package/src/utils/requirements-parser.js +3 -3
  30. package/tests/health-tracking/health-reporter.test.js +329 -0
  31. package/tests/health-tracking/ide-health-tracker.test.js +368 -0
  32. package/tests/health-tracking/interaction-recorder.test.js +309 -0
@@ -0,0 +1,276 @@
1
+ /**
2
+ * JSON File Storage Adapter
3
+ *
4
+ * Implements StorageInterface with atomic writes and backup handling
5
+ * for IDE health data persistence.
6
+ *
7
+ * @module json-storage
8
+ */
9
+
10
+ const fs = require('fs-extra');
11
+ const path = require('path');
12
+ const os = require('os');
13
+ const { StorageInterface } = require('./storage-interface');
14
+ const { FileSystemError, ValidationError } = require('./errors');
15
+ const { validateHealthData } = require('./validators');
16
+
17
+ /**
18
+ * Default storage file location
19
+ */
20
+ const DEFAULT_STORAGE_FILE = path.join(
21
+ os.homedir(),
22
+ '.config',
23
+ 'vibecodingmachine',
24
+ 'ide-health.json'
25
+ );
26
+
27
+ /**
28
+ * Default health data structure
29
+ */
30
+ const DEFAULT_HEALTH_DATA = {
31
+ version: '1.0.0',
32
+ lastUpdated: new Date().toISOString(),
33
+ ides: {},
34
+ timeoutConfig: {
35
+ mode: 'fixed',
36
+ defaultTimeout: 1800000, // 30 minutes
37
+ bufferPercentage: 0.4, // 40%
38
+ minSamplesForAdaptive: 10,
39
+ ewmaAlpha: 0.3,
40
+ },
41
+ defaultRequirement: null,
42
+ };
43
+
44
+ /**
45
+ * JSON file storage adapter with atomic writes
46
+ * @implements {StorageInterface}
47
+ */
48
+ class JSONStorage extends StorageInterface {
49
+ /**
50
+ * @param {Object} options - Configuration options
51
+ * @param {string} [options.storageFile] - Path to storage file
52
+ */
53
+ constructor(options = {}) {
54
+ super();
55
+ this.storageFile = options.storageFile || DEFAULT_STORAGE_FILE;
56
+ this.backupFile = `${this.storageFile}.bak`;
57
+ }
58
+
59
+ /**
60
+ * Read data from storage
61
+ * @returns {Promise<Object>} The stored data object
62
+ * @throws {FileSystemError} If read operation fails
63
+ */
64
+ async read() {
65
+ try {
66
+ // Ensure storage directory exists
67
+ await fs.ensureDir(path.dirname(this.storageFile));
68
+
69
+ // Check if storage file exists
70
+ const fileExists = await this.exists();
71
+
72
+ if (!fileExists) {
73
+ // Return default data structure if file doesn't exist
74
+ return JSON.parse(JSON.stringify(DEFAULT_HEALTH_DATA));
75
+ }
76
+
77
+ // Read file content
78
+ const content = await fs.readFile(this.storageFile, 'utf8');
79
+
80
+ // Parse JSON
81
+ let data;
82
+ try {
83
+ data = JSON.parse(content);
84
+ } catch (parseError) {
85
+ // Attempt to restore from backup on parse error
86
+ console.warn(`Failed to parse health data, attempting backup restore: ${parseError.message}`);
87
+ const backupExists = await fs.pathExists(this.backupFile);
88
+
89
+ if (backupExists) {
90
+ await this.restore(this.backupFile);
91
+ const backupContent = await fs.readFile(this.storageFile, 'utf8');
92
+ data = JSON.parse(backupContent);
93
+ } else {
94
+ throw new FileSystemError(
95
+ 'Failed to parse health data and no backup available',
96
+ this.storageFile,
97
+ 'read',
98
+ parseError
99
+ );
100
+ }
101
+ }
102
+
103
+ // Validate data structure
104
+ try {
105
+ validateHealthData(data);
106
+ } catch (validationError) {
107
+ throw new FileSystemError(
108
+ `Health data validation failed: ${validationError.message}`,
109
+ this.storageFile,
110
+ 'read',
111
+ validationError
112
+ );
113
+ }
114
+
115
+ // Run migrations if needed
116
+ data = await this._runMigrations(data);
117
+
118
+ return data;
119
+ } catch (error) {
120
+ if (error instanceof FileSystemError) {
121
+ throw error;
122
+ }
123
+ throw new FileSystemError(
124
+ `Failed to read health data: ${error.message}`,
125
+ this.storageFile,
126
+ 'read',
127
+ error
128
+ );
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Write data to storage atomically
134
+ * @param {Object} data - The data to write
135
+ * @returns {Promise<void>}
136
+ * @throws {FileSystemError} If write operation fails
137
+ * @throws {ValidationError} If data is invalid
138
+ */
139
+ async write(data) {
140
+ try {
141
+ // Validate data before writing
142
+ validateHealthData(data);
143
+
144
+ // Update lastUpdated timestamp
145
+ data.lastUpdated = new Date().toISOString();
146
+
147
+ // Ensure storage directory exists
148
+ await fs.ensureDir(path.dirname(this.storageFile));
149
+
150
+ // Create backup if file exists
151
+ const fileExists = await this.exists();
152
+ if (fileExists) {
153
+ await this.backup();
154
+ }
155
+
156
+ // Atomic write: write to temp file first
157
+ const tempFile = `${this.storageFile}.tmp`;
158
+ await fs.writeJson(tempFile, data, { spaces: 2 });
159
+
160
+ // Rename temp file to target (atomic operation)
161
+ await fs.rename(tempFile, this.storageFile);
162
+ } catch (error) {
163
+ if (error instanceof ValidationError) {
164
+ throw error;
165
+ }
166
+ throw new FileSystemError(
167
+ `Failed to write health data: ${error.message}`,
168
+ this.storageFile,
169
+ 'write',
170
+ error
171
+ );
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Check if storage file exists
177
+ * @returns {Promise<boolean>} True if storage exists
178
+ */
179
+ async exists() {
180
+ return fs.pathExists(this.storageFile);
181
+ }
182
+
183
+ /**
184
+ * Create backup of current storage
185
+ * @returns {Promise<string>} Path to backup file
186
+ * @throws {FileSystemError} If backup creation fails
187
+ */
188
+ async backup() {
189
+ try {
190
+ const fileExists = await this.exists();
191
+
192
+ if (!fileExists) {
193
+ return null;
194
+ }
195
+
196
+ await fs.copy(this.storageFile, this.backupFile, { overwrite: true });
197
+ return this.backupFile;
198
+ } catch (error) {
199
+ throw new FileSystemError(
200
+ `Failed to create backup: ${error.message}`,
201
+ this.storageFile,
202
+ 'backup',
203
+ error
204
+ );
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Restore from backup file
210
+ * @param {string} backupPath - Path to backup file
211
+ * @returns {Promise<void>}
212
+ * @throws {FileSystemError} If restore operation fails
213
+ */
214
+ async restore(backupPath) {
215
+ try {
216
+ const backupExists = await fs.pathExists(backupPath);
217
+
218
+ if (!backupExists) {
219
+ throw new FileSystemError(
220
+ 'Backup file does not exist',
221
+ backupPath,
222
+ 'restore'
223
+ );
224
+ }
225
+
226
+ await fs.copy(backupPath, this.storageFile, { overwrite: true });
227
+ } catch (error) {
228
+ if (error instanceof FileSystemError) {
229
+ throw error;
230
+ }
231
+ throw new FileSystemError(
232
+ `Failed to restore from backup: ${error.message}`,
233
+ backupPath,
234
+ 'restore',
235
+ error
236
+ );
237
+ }
238
+ }
239
+
240
+ /**
241
+ * Run migrations on data if schema version changed
242
+ * @private
243
+ * @param {Object} data - The data to migrate
244
+ * @returns {Promise<Object>} Migrated data
245
+ */
246
+ async _runMigrations(data) {
247
+ // No migrations needed yet - schema is at version 1.0.0
248
+ // Future migrations will be added here when schema changes
249
+
250
+ // Example migration pattern:
251
+ // if (data.version === '1.0.0') {
252
+ // data = this._migrateFrom1_0_0To1_1_0(data);
253
+ // data.version = '1.1.0';
254
+ // }
255
+
256
+ return data;
257
+ }
258
+
259
+ /**
260
+ * Get default health data structure
261
+ * @returns {Object} Default health data
262
+ */
263
+ static getDefaultData() {
264
+ return JSON.parse(JSON.stringify(DEFAULT_HEALTH_DATA));
265
+ }
266
+
267
+ /**
268
+ * Get default storage file path
269
+ * @returns {string} Default storage file path
270
+ */
271
+ static getDefaultStorageFile() {
272
+ return DEFAULT_STORAGE_FILE;
273
+ }
274
+ }
275
+
276
+ module.exports = { JSONStorage, DEFAULT_STORAGE_FILE, DEFAULT_HEALTH_DATA };
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Storage Interface for IDE Health Data
3
+ *
4
+ * Abstract interface for persisting health tracking data.
5
+ * Implementations must provide atomic read/write operations.
6
+ *
7
+ * @module storage-interface
8
+ */
9
+
10
+ /**
11
+ * Base storage interface that all storage adapters must implement
12
+ * @interface StorageInterface
13
+ */
14
+ class StorageInterface {
15
+ /**
16
+ * Read data from storage
17
+ * @returns {Promise<Object>} The stored data object
18
+ * @throws {FileSystemError} If read operation fails
19
+ */
20
+ async read() {
21
+ throw new Error('Method not implemented: read()');
22
+ }
23
+
24
+ /**
25
+ * Write data to storage atomically
26
+ * @param {Object} data - The data to write
27
+ * @returns {Promise<void>}
28
+ * @throws {FileSystemError} If write operation fails
29
+ * @throws {ValidationError} If data is invalid
30
+ */
31
+ async write(data) {
32
+ throw new Error('Method not implemented: write()');
33
+ }
34
+
35
+ /**
36
+ * Check if storage file exists
37
+ * @returns {Promise<boolean>} True if storage exists
38
+ */
39
+ async exists() {
40
+ throw new Error('Method not implemented: exists()');
41
+ }
42
+
43
+ /**
44
+ * Create backup of current storage
45
+ * @returns {Promise<string>} Path to backup file
46
+ * @throws {FileSystemError} If backup creation fails
47
+ */
48
+ async backup() {
49
+ throw new Error('Method not implemented: backup()');
50
+ }
51
+
52
+ /**
53
+ * Restore from backup file
54
+ * @param {string} backupPath - Path to backup file
55
+ * @returns {Promise<void>}
56
+ * @throws {FileSystemError} If restore operation fails
57
+ */
58
+ async restore(backupPath) {
59
+ throw new Error('Method not implemented: restore()');
60
+ }
61
+ }
62
+
63
+ module.exports = { StorageInterface };
@@ -0,0 +1,277 @@
1
+ /**
2
+ * Validation Utilities for IDE Health Data
3
+ *
4
+ * @module validators
5
+ */
6
+
7
+ const { ValidationError } = require('./errors');
8
+
9
+ /**
10
+ * Validates IDE health data structure according to schema
11
+ * @param {Object} data - The health data to validate
12
+ * @throws {ValidationError} If validation fails
13
+ */
14
+ function validateHealthData(data) {
15
+ if (!data || typeof data !== 'object') {
16
+ throw new ValidationError('Health data must be an object', 'data', data);
17
+ }
18
+
19
+ // Validate version
20
+ if (!data.version || typeof data.version !== 'string') {
21
+ throw new ValidationError('Version must be a string', 'version', data.version);
22
+ }
23
+
24
+ // Validate lastUpdated
25
+ if (!data.lastUpdated || typeof data.lastUpdated !== 'string') {
26
+ throw new ValidationError('lastUpdated must be an ISO 8601 string', 'lastUpdated', data.lastUpdated);
27
+ }
28
+
29
+ // Validate ides object
30
+ if (!data.ides || typeof data.ides !== 'object') {
31
+ throw new ValidationError('ides must be an object', 'ides', data.ides);
32
+ }
33
+
34
+ // Validate each IDE record
35
+ for (const [ideId, ideRecord] of Object.entries(data.ides)) {
36
+ validateIDEHealthRecord(ideRecord, ideId);
37
+ }
38
+
39
+ // Validate timeoutConfig
40
+ if (data.timeoutConfig) {
41
+ validateTimeoutConfiguration(data.timeoutConfig);
42
+ }
43
+
44
+ // Validate defaultRequirement (optional)
45
+ if (data.defaultRequirement !== null && data.defaultRequirement !== undefined) {
46
+ validateDefaultRequirement(data.defaultRequirement);
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Validates IDE health record structure
52
+ * @param {Object} record - The IDE health record to validate
53
+ * @param {string} ideId - IDE identifier for error messages
54
+ * @throws {ValidationError} If validation fails
55
+ */
56
+ function validateIDEHealthRecord(record, ideId) {
57
+ if (!record || typeof record !== 'object') {
58
+ throw new ValidationError(`IDE health record for ${ideId} must be an object`, 'record', record);
59
+ }
60
+
61
+ // Validate successCount
62
+ if (typeof record.successCount !== 'number' || record.successCount < 0) {
63
+ throw new ValidationError(`successCount must be >= 0 for ${ideId}`, 'successCount', record.successCount);
64
+ }
65
+
66
+ // Validate failureCount
67
+ if (typeof record.failureCount !== 'number' || record.failureCount < 0) {
68
+ throw new ValidationError(`failureCount must be >= 0 for ${ideId}`, 'failureCount', record.failureCount);
69
+ }
70
+
71
+ // Validate responseTimes array
72
+ if (!Array.isArray(record.responseTimes)) {
73
+ throw new ValidationError(`responseTimes must be an array for ${ideId}`, 'responseTimes', record.responseTimes);
74
+ }
75
+
76
+ if (record.responseTimes.length > 50) {
77
+ throw new ValidationError(`responseTimes array exceeds max 50 entries for ${ideId}`, 'responseTimes.length', record.responseTimes.length);
78
+ }
79
+
80
+ // Validate ewma
81
+ if (typeof record.ewma !== 'number' || record.ewma < 0) {
82
+ throw new ValidationError(`ewma must be >= 0 for ${ideId}`, 'ewma', record.ewma);
83
+ }
84
+
85
+ // Validate lastSuccess (nullable)
86
+ if (record.lastSuccess !== null && typeof record.lastSuccess !== 'string') {
87
+ throw new ValidationError(`lastSuccess must be null or ISO 8601 string for ${ideId}`, 'lastSuccess', record.lastSuccess);
88
+ }
89
+
90
+ // Validate lastFailure (nullable)
91
+ if (record.lastFailure !== null && typeof record.lastFailure !== 'string') {
92
+ throw new ValidationError(`lastFailure must be null or ISO 8601 string for ${ideId}`, 'lastFailure', record.lastFailure);
93
+ }
94
+
95
+ // Validate consecutiveFailures
96
+ if (typeof record.consecutiveFailures !== 'number' || record.consecutiveFailures < 0) {
97
+ throw new ValidationError(`consecutiveFailures must be >= 0 for ${ideId}`, 'consecutiveFailures', record.consecutiveFailures);
98
+ }
99
+
100
+ // Validate currentTimeout (5 minutes minimum = 300000ms)
101
+ if (typeof record.currentTimeout !== 'number' || record.currentTimeout < 300000) {
102
+ throw new ValidationError(`currentTimeout must be >= 300000 (5 minutes) for ${ideId}`, 'currentTimeout', record.currentTimeout);
103
+ }
104
+
105
+ // Validate interactions array
106
+ if (!Array.isArray(record.interactions)) {
107
+ throw new ValidationError(`interactions must be an array for ${ideId}`, 'interactions', record.interactions);
108
+ }
109
+
110
+ if (record.interactions.length > 100) {
111
+ throw new ValidationError(`interactions array exceeds max 100 entries for ${ideId}`, 'interactions.length', record.interactions.length);
112
+ }
113
+
114
+ // Validate each interaction
115
+ record.interactions.forEach((interaction, index) => {
116
+ validateInteractionRecord(interaction, `${ideId}[${index}]`);
117
+ });
118
+ }
119
+
120
+ /**
121
+ * Validates interaction record structure
122
+ * @param {Object} interaction - The interaction record to validate
123
+ * @param {string} context - Context for error messages
124
+ * @throws {ValidationError} If validation fails
125
+ */
126
+ function validateInteractionRecord(interaction, context) {
127
+ if (!interaction || typeof interaction !== 'object') {
128
+ throw new ValidationError(`Interaction at ${context} must be an object`, 'interaction', interaction);
129
+ }
130
+
131
+ // Validate timestamp
132
+ if (!interaction.timestamp || typeof interaction.timestamp !== 'string') {
133
+ throw new ValidationError(`timestamp must be ISO 8601 string at ${context}`, 'timestamp', interaction.timestamp);
134
+ }
135
+
136
+ // Validate outcome
137
+ const validOutcomes = ['success', 'failure', 'quota'];
138
+ if (!validOutcomes.includes(interaction.outcome)) {
139
+ throw new ValidationError(`outcome must be one of ${validOutcomes.join(', ')} at ${context}`, 'outcome', interaction.outcome);
140
+ }
141
+
142
+ // Validate responseTime (nullable)
143
+ if (interaction.responseTime !== null && (typeof interaction.responseTime !== 'number' || interaction.responseTime < 0)) {
144
+ throw new ValidationError(`responseTime must be null or >= 0 at ${context}`, 'responseTime', interaction.responseTime);
145
+ }
146
+
147
+ // Validate timeoutUsed
148
+ if (typeof interaction.timeoutUsed !== 'number' || interaction.timeoutUsed < 0) {
149
+ throw new ValidationError(`timeoutUsed must be >= 0 at ${context}`, 'timeoutUsed', interaction.timeoutUsed);
150
+ }
151
+
152
+ // Validate continuationPromptsDetected
153
+ if (typeof interaction.continuationPromptsDetected !== 'number' || interaction.continuationPromptsDetected < 0) {
154
+ throw new ValidationError(`continuationPromptsDetected must be >= 0 at ${context}`, 'continuationPromptsDetected', interaction.continuationPromptsDetected);
155
+ }
156
+
157
+ // Validate requirementId (nullable)
158
+ if (interaction.requirementId !== null && typeof interaction.requirementId !== 'string') {
159
+ throw new ValidationError(`requirementId must be null or string at ${context}`, 'requirementId', interaction.requirementId);
160
+ }
161
+
162
+ // Validate errorMessage (nullable)
163
+ if (interaction.errorMessage !== null && typeof interaction.errorMessage !== 'string') {
164
+ throw new ValidationError(`errorMessage must be null or string at ${context}`, 'errorMessage', interaction.errorMessage);
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Validates timeout configuration structure
170
+ * @param {Object} config - The timeout configuration to validate
171
+ * @throws {ValidationError} If validation fails
172
+ */
173
+ function validateTimeoutConfiguration(config) {
174
+ if (!config || typeof config !== 'object') {
175
+ throw new ValidationError('Timeout configuration must be an object', 'timeoutConfig', config);
176
+ }
177
+
178
+ // Validate mode
179
+ const validModes = ['adaptive', 'fixed'];
180
+ if (!validModes.includes(config.mode)) {
181
+ throw new ValidationError(`mode must be one of ${validModes.join(', ')}`, 'mode', config.mode);
182
+ }
183
+
184
+ // Validate defaultTimeout (5 minutes minimum)
185
+ if (typeof config.defaultTimeout !== 'number' || config.defaultTimeout < 300000) {
186
+ throw new ValidationError('defaultTimeout must be >= 300000 (5 minutes)', 'defaultTimeout', config.defaultTimeout);
187
+ }
188
+
189
+ // Validate bufferPercentage (20% to 100%)
190
+ if (typeof config.bufferPercentage !== 'number' || config.bufferPercentage < 0.2 || config.bufferPercentage > 1.0) {
191
+ throw new ValidationError('bufferPercentage must be between 0.2 and 1.0', 'bufferPercentage', config.bufferPercentage);
192
+ }
193
+
194
+ // Validate minSamplesForAdaptive (5 to 50)
195
+ if (typeof config.minSamplesForAdaptive !== 'number' || config.minSamplesForAdaptive < 5 || config.minSamplesForAdaptive > 50) {
196
+ throw new ValidationError('minSamplesForAdaptive must be between 5 and 50', 'minSamplesForAdaptive', config.minSamplesForAdaptive);
197
+ }
198
+
199
+ // Validate ewmaAlpha (0 to 1, exclusive)
200
+ if (typeof config.ewmaAlpha !== 'number' || config.ewmaAlpha <= 0 || config.ewmaAlpha >= 1) {
201
+ throw new ValidationError('ewmaAlpha must be between 0 and 1 (exclusive)', 'ewmaAlpha', config.ewmaAlpha);
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Validates default requirement structure
207
+ * @param {Object} defaultReq - The default requirement to validate
208
+ * @throws {ValidationError} If validation fails
209
+ */
210
+ function validateDefaultRequirement(defaultReq) {
211
+ if (!defaultReq || typeof defaultReq !== 'object') {
212
+ throw new ValidationError('Default requirement must be an object', 'defaultRequirement', defaultReq);
213
+ }
214
+
215
+ // Validate text (non-empty)
216
+ if (typeof defaultReq.text !== 'string' || defaultReq.text.trim().length === 0) {
217
+ throw new ValidationError('Default requirement text must be non-empty string', 'text', defaultReq.text);
218
+ }
219
+
220
+ // Validate maxIterations (nullable, 0 = unlimited)
221
+ if (defaultReq.maxIterations !== null && (typeof defaultReq.maxIterations !== 'number' || defaultReq.maxIterations < 0)) {
222
+ throw new ValidationError('maxIterations must be null or >= 0', 'maxIterations', defaultReq.maxIterations);
223
+ }
224
+
225
+ // Validate currentIteration
226
+ if (typeof defaultReq.currentIteration !== 'number' || defaultReq.currentIteration < 0) {
227
+ throw new ValidationError('currentIteration must be >= 0', 'currentIteration', defaultReq.currentIteration);
228
+ }
229
+
230
+ // Validate consecutiveFailures
231
+ if (typeof defaultReq.consecutiveFailures !== 'number' || defaultReq.consecutiveFailures < 0) {
232
+ throw new ValidationError('consecutiveFailures must be >= 0', 'consecutiveFailures', defaultReq.consecutiveFailures);
233
+ }
234
+
235
+ // Validate status
236
+ const validStatuses = ['pending', 'active', 'paused', 'completed', 'stopped'];
237
+ if (!validStatuses.includes(defaultReq.status)) {
238
+ throw new ValidationError(`status must be one of ${validStatuses.join(', ')}`, 'status', defaultReq.status);
239
+ }
240
+
241
+ // Validate lastProcessed (nullable)
242
+ if (defaultReq.lastProcessed !== null && typeof defaultReq.lastProcessed !== 'string') {
243
+ throw new ValidationError('lastProcessed must be null or ISO 8601 string', 'lastProcessed', defaultReq.lastProcessed);
244
+ }
245
+ }
246
+
247
+ /**
248
+ * Validates IDE identifier
249
+ * @param {string} ideId - IDE identifier to validate
250
+ * @throws {ValidationError} If validation fails
251
+ */
252
+ function validateIdeId(ideId) {
253
+ if (!ideId || typeof ideId !== 'string' || ideId.trim().length === 0) {
254
+ throw new ValidationError('IDE identifier must be non-empty string', 'ideId', ideId);
255
+ }
256
+ }
257
+
258
+ /**
259
+ * Validates response time value
260
+ * @param {number} responseTime - Response time in milliseconds
261
+ * @throws {ValidationError} If validation fails
262
+ */
263
+ function validateResponseTime(responseTime) {
264
+ if (typeof responseTime !== 'number' || responseTime < 0) {
265
+ throw new ValidationError('Response time must be >= 0', 'responseTime', responseTime);
266
+ }
267
+ }
268
+
269
+ module.exports = {
270
+ validateHealthData,
271
+ validateIDEHealthRecord,
272
+ validateInteractionRecord,
273
+ validateTimeoutConfiguration,
274
+ validateDefaultRequirement,
275
+ validateIdeId,
276
+ validateResponseTime,
277
+ };