@soulcraft/brainy 6.2.8 → 6.2.9
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.
|
@@ -1276,6 +1276,42 @@ export class BaseStorage extends BaseStorageAdapter {
|
|
|
1276
1276
|
nextCursor
|
|
1277
1277
|
};
|
|
1278
1278
|
}
|
|
1279
|
+
// v6.2.9: Fast path for SINGLE sourceId + verbType combo (common VFS pattern)
|
|
1280
|
+
// This avoids the slow type-iteration fallback for VFS operations
|
|
1281
|
+
// NOTE: Only use fast path for single sourceId to avoid incomplete results
|
|
1282
|
+
const isSingleSourceId = options.filter.sourceId &&
|
|
1283
|
+
!Array.isArray(options.filter.sourceId);
|
|
1284
|
+
if (isSingleSourceId &&
|
|
1285
|
+
options.filter.verbType &&
|
|
1286
|
+
!options.filter.targetId &&
|
|
1287
|
+
!options.filter.service &&
|
|
1288
|
+
!options.filter.metadata) {
|
|
1289
|
+
const sourceId = options.filter.sourceId;
|
|
1290
|
+
const verbTypes = Array.isArray(options.filter.verbType)
|
|
1291
|
+
? options.filter.verbType
|
|
1292
|
+
: [options.filter.verbType];
|
|
1293
|
+
prodLog.debug(`[BaseStorage] getVerbs: Using fast path for sourceId=${sourceId}, verbTypes=${verbTypes.join(',')}`);
|
|
1294
|
+
// Get verbs by source (uses GraphAdjacencyIndex if available)
|
|
1295
|
+
const verbsBySource = await this.getVerbsBySource_internal(sourceId);
|
|
1296
|
+
// Filter by verbType in memory (fast - usually small number of verbs per source)
|
|
1297
|
+
const filtered = verbsBySource.filter(v => verbTypes.includes(v.verb));
|
|
1298
|
+
// Apply pagination
|
|
1299
|
+
const paginatedVerbs = filtered.slice(offset, offset + limit);
|
|
1300
|
+
const hasMore = offset + limit < filtered.length;
|
|
1301
|
+
// Set next cursor if there are more items
|
|
1302
|
+
let nextCursor = undefined;
|
|
1303
|
+
if (hasMore && paginatedVerbs.length > 0) {
|
|
1304
|
+
const lastItem = paginatedVerbs[paginatedVerbs.length - 1];
|
|
1305
|
+
nextCursor = lastItem.id;
|
|
1306
|
+
}
|
|
1307
|
+
prodLog.debug(`[BaseStorage] getVerbs: Fast path returned ${filtered.length} verbs (${paginatedVerbs.length} after pagination)`);
|
|
1308
|
+
return {
|
|
1309
|
+
items: paginatedVerbs,
|
|
1310
|
+
totalCount: filtered.length,
|
|
1311
|
+
hasMore,
|
|
1312
|
+
nextCursor
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1279
1315
|
}
|
|
1280
1316
|
// For more complex filtering or no filtering, use a paginated approach
|
|
1281
1317
|
// that avoids loading all verbs into memory at once
|
|
@@ -1336,14 +1372,32 @@ export class BaseStorage extends BaseStorageAdapter {
|
|
|
1336
1372
|
// Only use type-skipping optimization if counts are non-zero (reliable)
|
|
1337
1373
|
const totalVerbCountFromArray = this.verbCountsByType.reduce((sum, c) => sum + c, 0);
|
|
1338
1374
|
const useOptimization = totalVerbCountFromArray > 0;
|
|
1375
|
+
// v6.2.9 BUG FIX: Pre-compute requested verb types to avoid skipping them
|
|
1376
|
+
// When a specific verbType filter is provided, we MUST check that type
|
|
1377
|
+
// even if verbCountsByType shows 0 (counts can be stale after restart)
|
|
1378
|
+
const requestedVerbTypes = options?.filter?.verbType;
|
|
1379
|
+
const requestedVerbTypesSet = requestedVerbTypes
|
|
1380
|
+
? new Set(Array.isArray(requestedVerbTypes) ? requestedVerbTypes : [requestedVerbTypes])
|
|
1381
|
+
: null;
|
|
1339
1382
|
// Iterate through all 127 verb types (Stage 3 CANONICAL) with early termination
|
|
1340
1383
|
// OPTIMIZATION: Skip types with zero count (only if counts are reliable)
|
|
1341
1384
|
for (let i = 0; i < VERB_TYPE_COUNT && collectedVerbs.length < targetCount; i++) {
|
|
1342
|
-
|
|
1343
|
-
|
|
1385
|
+
const type = TypeUtils.getVerbFromIndex(i);
|
|
1386
|
+
// v6.2.9 FIX: Never skip a type that's explicitly requested in the filter
|
|
1387
|
+
// This fixes VFS bug where Contains relationships were skipped after restart
|
|
1388
|
+
// when verbCountsByType[Contains] was 0 due to stale statistics
|
|
1389
|
+
const isRequestedType = requestedVerbTypesSet?.has(type) ?? false;
|
|
1390
|
+
const countIsZero = this.verbCountsByType[i] === 0;
|
|
1391
|
+
// Skip empty types for performance (but only if optimization is enabled AND not requested)
|
|
1392
|
+
if (useOptimization && countIsZero && !isRequestedType) {
|
|
1344
1393
|
continue;
|
|
1345
1394
|
}
|
|
1346
|
-
|
|
1395
|
+
// v6.2.9: Log when we DON'T skip a requested type that would have been skipped
|
|
1396
|
+
// This helps diagnose stale statistics issues in production
|
|
1397
|
+
if (useOptimization && countIsZero && isRequestedType) {
|
|
1398
|
+
prodLog.debug(`[BaseStorage] getVerbs: NOT skipping type=${type} despite count=0 (type was explicitly requested). ` +
|
|
1399
|
+
`Statistics may be stale - consider running rebuildTypeCounts().`);
|
|
1400
|
+
}
|
|
1347
1401
|
try {
|
|
1348
1402
|
const verbsOfType = await this.getVerbsByType_internal(type);
|
|
1349
1403
|
// Apply filtering inline (memory efficient)
|
|
@@ -2169,8 +2223,11 @@ export class BaseStorage extends BaseStorageAdapter {
|
|
|
2169
2223
|
this.nounCountsByType[typeIndex]++;
|
|
2170
2224
|
// COW-aware write (v5.0.1): Use COW helper for branch isolation
|
|
2171
2225
|
await this.writeObjectToBranch(path, noun);
|
|
2172
|
-
// Periodically save statistics
|
|
2173
|
-
|
|
2226
|
+
// Periodically save statistics
|
|
2227
|
+
// v6.2.9: Also save on first noun of each type to ensure low-count types are tracked
|
|
2228
|
+
const shouldSave = this.nounCountsByType[typeIndex] === 1 || // First noun of type
|
|
2229
|
+
this.nounCountsByType[typeIndex] % 100 === 0; // Every 100th
|
|
2230
|
+
if (shouldSave) {
|
|
2174
2231
|
await this.saveTypeStatistics();
|
|
2175
2232
|
}
|
|
2176
2233
|
}
|
|
@@ -2277,7 +2334,11 @@ export class BaseStorage extends BaseStorageAdapter {
|
|
|
2277
2334
|
prodLog.warn(`[BaseStorage] graphIndex is null, cannot update index for verb ${verb.id}`);
|
|
2278
2335
|
}
|
|
2279
2336
|
// Periodically save statistics
|
|
2280
|
-
|
|
2337
|
+
// v6.2.9: Also save on first verb of each type to ensure low-count types are tracked
|
|
2338
|
+
// This prevents stale statistics after restart for types with < 100 verbs (common for VFS)
|
|
2339
|
+
const shouldSave = this.verbCountsByType[typeIndex] === 1 || // First verb of type
|
|
2340
|
+
this.verbCountsByType[typeIndex] % 100 === 0; // Every 100th
|
|
2341
|
+
if (shouldSave) {
|
|
2281
2342
|
await this.saveTypeStatistics();
|
|
2282
2343
|
}
|
|
2283
2344
|
}
|
|
@@ -92,6 +92,13 @@ export declare class UnifiedCache {
|
|
|
92
92
|
* Delete specific item from cache
|
|
93
93
|
*/
|
|
94
94
|
delete(key: string): boolean;
|
|
95
|
+
/**
|
|
96
|
+
* Delete all items with keys starting with the given prefix
|
|
97
|
+
* v6.2.9: Added for VFS cache invalidation (fixes stale parent ID bug)
|
|
98
|
+
* @param prefix - The key prefix to match
|
|
99
|
+
* @returns Number of items deleted
|
|
100
|
+
*/
|
|
101
|
+
deleteByPrefix(prefix: string): number;
|
|
95
102
|
/**
|
|
96
103
|
* Clear cache or specific type
|
|
97
104
|
*/
|
|
@@ -304,6 +304,23 @@ export class UnifiedCache {
|
|
|
304
304
|
}
|
|
305
305
|
return false;
|
|
306
306
|
}
|
|
307
|
+
/**
|
|
308
|
+
* Delete all items with keys starting with the given prefix
|
|
309
|
+
* v6.2.9: Added for VFS cache invalidation (fixes stale parent ID bug)
|
|
310
|
+
* @param prefix - The key prefix to match
|
|
311
|
+
* @returns Number of items deleted
|
|
312
|
+
*/
|
|
313
|
+
deleteByPrefix(prefix) {
|
|
314
|
+
let deleted = 0;
|
|
315
|
+
for (const [key, item] of this.cache) {
|
|
316
|
+
if (key.startsWith(prefix)) {
|
|
317
|
+
this.currentSize -= item.size;
|
|
318
|
+
this.cache.delete(key);
|
|
319
|
+
deleted++;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return deleted;
|
|
323
|
+
}
|
|
307
324
|
/**
|
|
308
325
|
* Clear cache or specific type
|
|
309
326
|
*/
|
|
@@ -64,6 +64,8 @@ export declare class PathResolver {
|
|
|
64
64
|
createPath(path: string, entityId: string): Promise<void>;
|
|
65
65
|
/**
|
|
66
66
|
* Invalidate cache entries for a path and its children
|
|
67
|
+
* v6.2.9 FIX: Also invalidates UnifiedCache to prevent stale entity IDs
|
|
68
|
+
* This fixes the "Source entity not found" bug after delete+recreate operations
|
|
67
69
|
*/
|
|
68
70
|
invalidatePath(path: string, recursive?: boolean): void;
|
|
69
71
|
/**
|
package/dist/vfs/PathResolver.js
CHANGED
|
@@ -254,26 +254,38 @@ export class PathResolver {
|
|
|
254
254
|
}
|
|
255
255
|
/**
|
|
256
256
|
* Invalidate cache entries for a path and its children
|
|
257
|
+
* v6.2.9 FIX: Also invalidates UnifiedCache to prevent stale entity IDs
|
|
258
|
+
* This fixes the "Source entity not found" bug after delete+recreate operations
|
|
257
259
|
*/
|
|
258
260
|
invalidatePath(path, recursive = false) {
|
|
259
261
|
const normalizedPath = this.normalizePath(path);
|
|
260
|
-
//
|
|
262
|
+
// v6.2.9 FIX: Clear parent cache BEFORE deleting from pathCache
|
|
263
|
+
// (we need the entityId from the cache entry)
|
|
264
|
+
const cached = this.pathCache.get(normalizedPath);
|
|
265
|
+
if (cached) {
|
|
266
|
+
this.parentCache.delete(cached.entityId);
|
|
267
|
+
}
|
|
268
|
+
// Remove from local caches
|
|
261
269
|
this.pathCache.delete(normalizedPath);
|
|
262
270
|
this.hotPaths.delete(normalizedPath);
|
|
271
|
+
// v6.2.9 CRITICAL FIX: Also invalidate UnifiedCache (global LRU cache)
|
|
272
|
+
// This was missing before, causing stale entity IDs to be returned after delete
|
|
273
|
+
const cacheKey = `vfs:path:${normalizedPath}`;
|
|
274
|
+
getGlobalCache().delete(cacheKey);
|
|
263
275
|
if (recursive) {
|
|
264
276
|
// Remove all paths that start with this path
|
|
265
277
|
const prefix = normalizedPath.endsWith('/') ? normalizedPath : normalizedPath + '/';
|
|
266
|
-
for (const [cachedPath] of this.pathCache) {
|
|
278
|
+
for (const [cachedPath, entry] of this.pathCache) {
|
|
267
279
|
if (cachedPath.startsWith(prefix)) {
|
|
268
280
|
this.pathCache.delete(cachedPath);
|
|
269
281
|
this.hotPaths.delete(cachedPath);
|
|
282
|
+
// v6.2.9: Also clear parent cache for this entry
|
|
283
|
+
this.parentCache.delete(entry.entityId);
|
|
270
284
|
}
|
|
271
285
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
if (cached) {
|
|
276
|
-
this.parentCache.delete(cached.entityId);
|
|
286
|
+
// v6.2.9 CRITICAL FIX: Also invalidate UnifiedCache entries with this prefix
|
|
287
|
+
const globalCachePrefix = `vfs:path:${prefix}`;
|
|
288
|
+
getGlobalCache().deleteByPrefix(globalCachePrefix);
|
|
277
289
|
}
|
|
278
290
|
}
|
|
279
291
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@soulcraft/brainy",
|
|
3
|
-
"version": "6.2.
|
|
3
|
+
"version": "6.2.9",
|
|
4
4
|
"description": "Universal Knowledge Protocol™ - World's first Triple Intelligence database unifying vector, graph, and document search in one API. Stage 3 CANONICAL: 42 nouns × 127 verbs covering 96-97% of all human knowledge.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.js",
|