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.
|
|
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
|
+
};
|