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.
- package/__tests__/provider-manager-fallback.test.js +43 -0
- package/__tests__/provider-manager-rate-limit.test.js +61 -0
- package/package.json +1 -1
- package/src/compliance/compliance-manager.js +5 -2
- package/src/database/migrations.js +135 -12
- package/src/database/user-database-client.js +63 -8
- package/src/database/user-schema.js +7 -0
- package/src/health-tracking/__tests__/ide-health-tracker.test.js +420 -0
- package/src/health-tracking/__tests__/interaction-recorder.test.js +392 -0
- package/src/health-tracking/errors.js +50 -0
- package/src/health-tracking/health-reporter.js +331 -0
- package/src/health-tracking/ide-health-tracker.js +446 -0
- package/src/health-tracking/interaction-recorder.js +161 -0
- package/src/health-tracking/json-storage.js +276 -0
- package/src/health-tracking/storage-interface.js +63 -0
- package/src/health-tracking/validators.js +277 -0
- package/src/ide-integration/applescript-manager.cjs +1062 -4
- package/src/ide-integration/applescript-manager.js +560 -11
- package/src/ide-integration/provider-manager.cjs +158 -28
- package/src/ide-integration/quota-detector.cjs +339 -16
- package/src/ide-integration/quota-detector.js +6 -1
- package/src/index.cjs +32 -1
- package/src/index.js +16 -0
- package/src/localization/translations/en.js +13 -1
- package/src/localization/translations/es.js +12 -0
- package/src/utils/admin-utils.js +33 -0
- package/src/utils/error-reporter.js +12 -4
- package/src/utils/requirement-helpers.js +34 -4
- package/src/utils/requirements-parser.js +3 -3
- package/tests/health-tracking/health-reporter.test.js +329 -0
- package/tests/health-tracking/ide-health-tracker.test.js +368 -0
- 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
|
+
};
|