musubi-sdd 5.6.2 → 5.7.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "musubi-sdd",
3
- "version": "5.6.2",
3
+ "version": "5.7.2",
4
4
  "description": "Ultimate Specification Driven Development Tool with 27 Agents for 7 AI Coding Platforms + MCP Integration (Claude Code, GitHub Copilot, Cursor, Gemini CLI, Windsurf, Codex, Qwen Code)",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/index.js CHANGED
@@ -85,6 +85,9 @@ const { ReleaseManager } = require('./monitoring/release-manager');
85
85
  const { SteeringValidator } = require('./steering/steering-validator');
86
86
  const { SteeringAutoUpdate } = require('./steering/steering-auto-update');
87
87
 
88
+ // Performance (Phase 6)
89
+ const performance = require('./performance');
90
+
88
91
  module.exports = {
89
92
  // Analyzers
90
93
  LargeProjectAnalyzer,
@@ -148,4 +151,7 @@ module.exports = {
148
151
  // Steering
149
152
  SteeringValidator,
150
153
  SteeringAutoUpdate,
154
+
155
+ // Performance (Phase 6)
156
+ performance,
151
157
  };
@@ -0,0 +1,471 @@
1
+ /**
2
+ * MUSUBI Cache Manager
3
+ *
4
+ * Caching layer for performance optimization.
5
+ * Phase 6 P0: Performance Optimization
6
+ *
7
+ * Features:
8
+ * - In-memory LRU cache
9
+ * - TTL-based expiration
10
+ * - Cache statistics
11
+ * - Namespace isolation
12
+ * - Cache-aside pattern support
13
+ */
14
+
15
+ /**
16
+ * Cache entry structure
17
+ */
18
+ class CacheEntry {
19
+ constructor(value, ttl = null) {
20
+ this.value = value;
21
+ this.createdAt = Date.now();
22
+ this.accessedAt = Date.now();
23
+ this.accessCount = 0;
24
+ this.ttl = ttl;
25
+ }
26
+
27
+ isExpired() {
28
+ if (this.ttl === null) return false;
29
+ return Date.now() - this.createdAt > this.ttl;
30
+ }
31
+
32
+ touch() {
33
+ this.accessedAt = Date.now();
34
+ this.accessCount++;
35
+ }
36
+ }
37
+
38
+ /**
39
+ * LRU Cache implementation
40
+ */
41
+ class LRUCache {
42
+ constructor(maxSize = 1000) {
43
+ this.maxSize = maxSize;
44
+ this.cache = new Map();
45
+ }
46
+
47
+ get(key) {
48
+ const entry = this.cache.get(key);
49
+ if (!entry) return undefined;
50
+
51
+ if (entry.isExpired()) {
52
+ this.cache.delete(key);
53
+ return undefined;
54
+ }
55
+
56
+ // Move to end (most recently used)
57
+ this.cache.delete(key);
58
+ this.cache.set(key, entry);
59
+ entry.touch();
60
+
61
+ return entry.value;
62
+ }
63
+
64
+ set(key, value, ttl = null) {
65
+ // Remove if exists to update position
66
+ this.cache.delete(key);
67
+
68
+ // Evict oldest if at capacity
69
+ if (this.cache.size >= this.maxSize) {
70
+ const oldestKey = this.cache.keys().next().value;
71
+ this.cache.delete(oldestKey);
72
+ }
73
+
74
+ this.cache.set(key, new CacheEntry(value, ttl));
75
+ }
76
+
77
+ has(key) {
78
+ const entry = this.cache.get(key);
79
+ if (!entry) return false;
80
+
81
+ if (entry.isExpired()) {
82
+ this.cache.delete(key);
83
+ return false;
84
+ }
85
+
86
+ return true;
87
+ }
88
+
89
+ delete(key) {
90
+ return this.cache.delete(key);
91
+ }
92
+
93
+ clear() {
94
+ this.cache.clear();
95
+ }
96
+
97
+ get size() {
98
+ return this.cache.size;
99
+ }
100
+
101
+ /**
102
+ * Get cache statistics
103
+ */
104
+ getStats() {
105
+ let totalAccess = 0;
106
+ let expiredCount = 0;
107
+ const now = Date.now();
108
+ let oldestEntry = now;
109
+
110
+ for (const [, entry] of this.cache) {
111
+ totalAccess += entry.accessCount;
112
+ if (entry.isExpired()) expiredCount++;
113
+ if (entry.createdAt < oldestEntry) oldestEntry = entry.createdAt;
114
+ }
115
+
116
+ return {
117
+ size: this.cache.size,
118
+ maxSize: this.maxSize,
119
+ totalAccess,
120
+ expiredCount,
121
+ oldestEntryAge: now - oldestEntry,
122
+ };
123
+ }
124
+
125
+ /**
126
+ * Clean up expired entries
127
+ */
128
+ cleanup() {
129
+ let cleaned = 0;
130
+ for (const [key, entry] of this.cache) {
131
+ if (entry.isExpired()) {
132
+ this.cache.delete(key);
133
+ cleaned++;
134
+ }
135
+ }
136
+ return cleaned;
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Cache Manager with namespaces
142
+ */
143
+ class CacheManager {
144
+ constructor(options = {}) {
145
+ this.options = {
146
+ defaultTTL: options.defaultTTL || 5 * 60 * 1000, // 5 minutes
147
+ maxSize: options.maxSize || 1000,
148
+ cleanupInterval: options.cleanupInterval || 60 * 1000, // 1 minute
149
+ enableStats: options.enableStats !== false,
150
+ ...options,
151
+ };
152
+
153
+ this.namespaces = new Map();
154
+ this.stats = {
155
+ hits: 0,
156
+ misses: 0,
157
+ sets: 0,
158
+ deletes: 0,
159
+ };
160
+
161
+ // Start cleanup timer if interval is set
162
+ if (this.options.cleanupInterval > 0) {
163
+ this.cleanupTimer = setInterval(() => {
164
+ this.cleanupAll();
165
+ }, this.options.cleanupInterval);
166
+
167
+ // Don't prevent process exit
168
+ if (this.cleanupTimer.unref) {
169
+ this.cleanupTimer.unref();
170
+ }
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Get or create a namespace
176
+ * @private
177
+ */
178
+ _getNamespace(namespace) {
179
+ if (!this.namespaces.has(namespace)) {
180
+ this.namespaces.set(namespace, new LRUCache(this.options.maxSize));
181
+ }
182
+ return this.namespaces.get(namespace);
183
+ }
184
+
185
+ /**
186
+ * Generate cache key
187
+ * @private
188
+ */
189
+ _makeKey(namespace, key) {
190
+ return `${namespace}:${typeof key === 'object' ? JSON.stringify(key) : key}`;
191
+ }
192
+
193
+ /**
194
+ * Get a value from cache
195
+ * @param {string} namespace - Cache namespace
196
+ * @param {string|Object} key - Cache key
197
+ * @returns {*} - Cached value or undefined
198
+ */
199
+ get(namespace, key) {
200
+ const cache = this._getNamespace(namespace);
201
+ const value = cache.get(this._makeKey(namespace, key));
202
+
203
+ if (this.options.enableStats) {
204
+ if (value !== undefined) {
205
+ this.stats.hits++;
206
+ } else {
207
+ this.stats.misses++;
208
+ }
209
+ }
210
+
211
+ return value;
212
+ }
213
+
214
+ /**
215
+ * Set a value in cache
216
+ * @param {string} namespace - Cache namespace
217
+ * @param {string|Object} key - Cache key
218
+ * @param {*} value - Value to cache
219
+ * @param {number} [ttl] - Time to live in ms
220
+ */
221
+ set(namespace, key, value, ttl = null) {
222
+ const cache = this._getNamespace(namespace);
223
+ cache.set(this._makeKey(namespace, key), value, ttl || this.options.defaultTTL);
224
+
225
+ if (this.options.enableStats) {
226
+ this.stats.sets++;
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Check if a key exists in cache
232
+ * @param {string} namespace - Cache namespace
233
+ * @param {string|Object} key - Cache key
234
+ * @returns {boolean}
235
+ */
236
+ has(namespace, key) {
237
+ const cache = this._getNamespace(namespace);
238
+ return cache.has(this._makeKey(namespace, key));
239
+ }
240
+
241
+ /**
242
+ * Delete a key from cache
243
+ * @param {string} namespace - Cache namespace
244
+ * @param {string|Object} key - Cache key
245
+ * @returns {boolean}
246
+ */
247
+ delete(namespace, key) {
248
+ const cache = this._getNamespace(namespace);
249
+ const result = cache.delete(this._makeKey(namespace, key));
250
+
251
+ if (this.options.enableStats && result) {
252
+ this.stats.deletes++;
253
+ }
254
+
255
+ return result;
256
+ }
257
+
258
+ /**
259
+ * Clear a namespace or all namespaces
260
+ * @param {string} [namespace] - Optional namespace to clear
261
+ */
262
+ clear(namespace) {
263
+ if (namespace) {
264
+ const cache = this.namespaces.get(namespace);
265
+ if (cache) {
266
+ cache.clear();
267
+ }
268
+ } else {
269
+ for (const cache of this.namespaces.values()) {
270
+ cache.clear();
271
+ }
272
+ }
273
+ }
274
+
275
+ /**
276
+ * Get or set pattern (cache-aside)
277
+ * @param {string} namespace - Cache namespace
278
+ * @param {string|Object} key - Cache key
279
+ * @param {Function} fetchFn - Function to fetch value if not cached
280
+ * @param {number} [ttl] - Time to live in ms
281
+ * @returns {Promise<*>} - Cached or fetched value
282
+ */
283
+ async getOrSet(namespace, key, fetchFn, ttl = null) {
284
+ const cached = this.get(namespace, key);
285
+ if (cached !== undefined) {
286
+ return cached;
287
+ }
288
+
289
+ const value = await fetchFn();
290
+ this.set(namespace, key, value, ttl);
291
+ return value;
292
+ }
293
+
294
+ /**
295
+ * Synchronous get or set
296
+ * @param {string} namespace - Cache namespace
297
+ * @param {string|Object} key - Cache key
298
+ * @param {Function} fetchFn - Function to fetch value if not cached
299
+ * @param {number} [ttl] - Time to live in ms
300
+ * @returns {*} - Cached or fetched value
301
+ */
302
+ getOrSetSync(namespace, key, fetchFn, ttl = null) {
303
+ const cached = this.get(namespace, key);
304
+ if (cached !== undefined) {
305
+ return cached;
306
+ }
307
+
308
+ const value = fetchFn();
309
+ this.set(namespace, key, value, ttl);
310
+ return value;
311
+ }
312
+
313
+ /**
314
+ * Memoize a function
315
+ * @param {string} namespace - Cache namespace
316
+ * @param {Function} fn - Function to memoize
317
+ * @param {Function} [keyFn] - Function to generate cache key from args
318
+ * @param {number} [ttl] - Time to live in ms
319
+ * @returns {Function} - Memoized function
320
+ */
321
+ memoize(namespace, fn, keyFn = null, ttl = null) {
322
+ const cache = this;
323
+ const generateKey = keyFn || ((...args) => JSON.stringify(args));
324
+
325
+ return function (...args) {
326
+ const key = generateKey(...args);
327
+ return cache.getOrSetSync(namespace, key, () => fn.apply(this, args), ttl);
328
+ };
329
+ }
330
+
331
+ /**
332
+ * Memoize an async function
333
+ * @param {string} namespace - Cache namespace
334
+ * @param {Function} fn - Async function to memoize
335
+ * @param {Function} [keyFn] - Function to generate cache key from args
336
+ * @param {number} [ttl] - Time to live in ms
337
+ * @returns {Function} - Memoized async function
338
+ */
339
+ memoizeAsync(namespace, fn, keyFn = null, ttl = null) {
340
+ const cache = this;
341
+ const generateKey = keyFn || ((...args) => JSON.stringify(args));
342
+
343
+ return async function (...args) {
344
+ const key = generateKey(...args);
345
+ return cache.getOrSet(namespace, key, () => fn.apply(this, args), ttl);
346
+ };
347
+ }
348
+
349
+ /**
350
+ * Clean up expired entries in all namespaces
351
+ */
352
+ cleanupAll() {
353
+ let totalCleaned = 0;
354
+ for (const cache of this.namespaces.values()) {
355
+ totalCleaned += cache.cleanup();
356
+ }
357
+ return totalCleaned;
358
+ }
359
+
360
+ /**
361
+ * Get cache statistics
362
+ */
363
+ getStats() {
364
+ const namespaceStats = {};
365
+ let totalSize = 0;
366
+
367
+ for (const [name, cache] of this.namespaces) {
368
+ const stats = cache.getStats();
369
+ namespaceStats[name] = stats;
370
+ totalSize += stats.size;
371
+ }
372
+
373
+ const hitRate =
374
+ this.stats.hits + this.stats.misses > 0
375
+ ? (this.stats.hits / (this.stats.hits + this.stats.misses)) * 100
376
+ : 0;
377
+
378
+ return {
379
+ global: {
380
+ ...this.stats,
381
+ hitRate: hitRate.toFixed(2) + '%',
382
+ totalSize,
383
+ namespaceCount: this.namespaces.size,
384
+ },
385
+ namespaces: namespaceStats,
386
+ };
387
+ }
388
+
389
+ /**
390
+ * Destroy the cache manager
391
+ */
392
+ destroy() {
393
+ if (this.cleanupTimer) {
394
+ clearInterval(this.cleanupTimer);
395
+ this.cleanupTimer = null;
396
+ }
397
+ this.clear();
398
+ }
399
+ }
400
+
401
+ /**
402
+ * Request coalescing for duplicate requests
403
+ */
404
+ class RequestCoalescer {
405
+ constructor() {
406
+ this.pending = new Map();
407
+ }
408
+
409
+ /**
410
+ * Coalesce duplicate requests
411
+ * @param {string} key - Request key
412
+ * @param {Function} fetchFn - Async function to fetch data
413
+ * @returns {Promise<*>} - Result
414
+ */
415
+ async coalesce(key, fetchFn) {
416
+ // If there's already a pending request, wait for it
417
+ if (this.pending.has(key)) {
418
+ return this.pending.get(key);
419
+ }
420
+
421
+ // Create new promise for this request
422
+ const promise = fetchFn()
423
+ .then(result => {
424
+ this.pending.delete(key);
425
+ return result;
426
+ })
427
+ .catch(error => {
428
+ this.pending.delete(key);
429
+ throw error;
430
+ });
431
+
432
+ this.pending.set(key, promise);
433
+ return promise;
434
+ }
435
+
436
+ /**
437
+ * Get number of pending requests
438
+ */
439
+ get pendingCount() {
440
+ return this.pending.size;
441
+ }
442
+
443
+ /**
444
+ * Clear all pending requests (use with caution)
445
+ */
446
+ clear() {
447
+ this.pending.clear();
448
+ }
449
+ }
450
+
451
+ // Singleton instance
452
+ const defaultCacheManager = new CacheManager();
453
+
454
+ // Cache namespaces for MUSUBI
455
+ const CacheNamespace = {
456
+ ANALYSIS: 'analysis', // Analysis results
457
+ STEERING: 'steering', // Steering file contents
458
+ LLM: 'llm', // LLM responses
459
+ CODEGRAPH: 'codegraph', // CodeGraph queries
460
+ TEMPLATES: 'templates', // Template contents
461
+ VALIDATION: 'validation', // Validation results
462
+ };
463
+
464
+ module.exports = {
465
+ CacheEntry,
466
+ LRUCache,
467
+ CacheManager,
468
+ RequestCoalescer,
469
+ CacheNamespace,
470
+ defaultCacheManager,
471
+ };