@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
- // Skip empty types for performance (but only if optimization is enabled)
1343
- if (useOptimization && this.verbCountsByType[i] === 0) {
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
- const type = TypeUtils.getVerbFromIndex(i);
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 (every 100 saves)
2173
- if (this.nounCountsByType[typeIndex] % 100 === 0) {
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
- if (this.verbCountsByType[typeIndex] % 100 === 0) {
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
  /**
@@ -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
- // Remove from all caches
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
- // Clear parent cache for the entity
274
- const cached = this.pathCache.get(normalizedPath);
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.8",
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",