@soulcraft/brainy 5.3.2 → 5.3.4

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/dist/brainy.js CHANGED
@@ -2031,9 +2031,11 @@ export class Brainy {
2031
2031
  // Get current state statistics
2032
2032
  const entityCount = await this.getNounCount();
2033
2033
  const relationshipCount = await this.getVerbCount();
2034
+ // v5.3.4: Import NULL_HASH constant
2035
+ const { NULL_HASH } = await import('./storage/cow/constants.js');
2034
2036
  // Build commit object using builder pattern
2035
2037
  const builder = CommitBuilder.create(blobStorage)
2036
- .tree('0000000000000000000000000000000000000000000000000000000000000000') // Empty tree hash for now
2038
+ .tree(NULL_HASH) // Empty tree hash (sentinel value)
2037
2039
  .message(options?.message || 'Snapshot commit')
2038
2040
  .author(options?.author || 'unknown')
2039
2041
  .timestamp(Date.now())
@@ -222,7 +222,9 @@ export class BaseStorage extends BaseStorageAdapter {
222
222
  const mainRef = await this.refManager.getRef('main');
223
223
  if (!mainRef) {
224
224
  // Create initial commit with empty tree
225
- const emptyTreeHash = '0000000000000000000000000000000000000000000000000000000000000000';
225
+ // v5.3.4: Use NULL_HASH constant instead of hardcoded string
226
+ const { NULL_HASH } = await import('./cow/constants.js');
227
+ const emptyTreeHash = NULL_HASH;
226
228
  // Import CommitBuilder
227
229
  const { CommitBuilder } = await import('./cow/CommitObject.js');
228
230
  // Create initial commit object
@@ -14,6 +14,7 @@
14
14
  * @module storage/cow/BlobStorage
15
15
  */
16
16
  import { createHash } from 'crypto';
17
+ import { NULL_HASH, isNullHash } from './constants.js';
17
18
  /**
18
19
  * State-of-the-art content-addressable blob storage
19
20
  *
@@ -132,10 +133,12 @@ export class BlobStorage {
132
133
  }
133
134
  else {
134
135
  // Small blob: single write
135
- await this.adapter.put(`blob:${hash}`, finalData);
136
+ const prefix = options.type || 'blob';
137
+ await this.adapter.put(`${prefix}:${hash}`, finalData);
136
138
  }
137
139
  // Write metadata
138
- await this.adapter.put(`blob-meta:${hash}`, Buffer.from(JSON.stringify(metadata)));
140
+ const prefix = options.type || 'blob';
141
+ await this.adapter.put(`${prefix}-meta:${hash}`, Buffer.from(JSON.stringify(metadata)));
139
142
  // Update cache (write-through)
140
143
  this.addToCache(hash, data, metadata);
141
144
  // Update stats
@@ -160,6 +163,15 @@ export class BlobStorage {
160
163
  * @returns Blob data
161
164
  */
162
165
  async read(hash, options = {}) {
166
+ // v5.3.4 fix: Guard against NULL hash (sentinel value)
167
+ // NULL_HASH ('0000...0000') is used as a sentinel for "no parent" or "empty tree"
168
+ // It should NEVER be read as actual blob data
169
+ if (isNullHash(hash)) {
170
+ throw new Error(`Cannot read NULL hash (${NULL_HASH}): ` +
171
+ `This is a sentinel value indicating "no parent commit" or "empty tree". ` +
172
+ `If you're seeing this error from CommitObject.walk(), there's a bug in commit traversal logic. ` +
173
+ `If you're seeing this from TreeObject operations, there's a bug in tree handling.`);
174
+ }
163
175
  // Check cache first
164
176
  if (!options.skipCache) {
165
177
  const cached = this.getFromCache(hash);
@@ -169,17 +181,27 @@ export class BlobStorage {
169
181
  }
170
182
  this.stats.cacheMisses++;
171
183
  }
172
- // Read from storage
173
- const data = await this.adapter.get(`blob:${hash}`);
174
- if (!data) {
175
- throw new Error(`Blob not found: ${hash}`);
184
+ // Try to read metadata to determine type (for backward compatibility)
185
+ // Try commit, tree, then blob prefixes
186
+ let prefix = null;
187
+ let metadataBuffer;
188
+ let metadata;
189
+ for (const tryPrefix of ['commit', 'tree', 'blob']) {
190
+ metadataBuffer = await this.adapter.get(`${tryPrefix}-meta:${hash}`);
191
+ if (metadataBuffer) {
192
+ prefix = tryPrefix;
193
+ metadata = JSON.parse(metadataBuffer.toString());
194
+ break;
195
+ }
176
196
  }
177
- // Read metadata
178
- const metadataBuffer = await this.adapter.get(`blob-meta:${hash}`);
179
- if (!metadataBuffer) {
197
+ if (!prefix || !metadata) {
180
198
  throw new Error(`Blob metadata not found: ${hash}`);
181
199
  }
182
- const metadata = JSON.parse(metadataBuffer.toString());
200
+ // Read from storage using determined prefix
201
+ const data = await this.adapter.get(`${prefix}:${hash}`);
202
+ if (!data) {
203
+ throw new Error(`Blob not found: ${hash}`);
204
+ }
183
205
  // Decompress if needed
184
206
  let finalData = data;
185
207
  if (metadata.compression === 'zstd' && !options.skipDecompression) {
@@ -209,9 +231,14 @@ export class BlobStorage {
209
231
  if (this.cache.has(hash)) {
210
232
  return true;
211
233
  }
212
- // Check storage
213
- const exists = await this.adapter.get(`blob:${hash}`);
214
- return exists !== undefined;
234
+ // Check storage - try all prefixes for backward compatibility
235
+ for (const prefix of ['commit', 'tree', 'blob']) {
236
+ const exists = await this.adapter.get(`${prefix}:${hash}`);
237
+ if (exists !== undefined) {
238
+ return true;
239
+ }
240
+ }
241
+ return false;
215
242
  }
216
243
  /**
217
244
  * Delete a blob from storage
@@ -230,10 +257,19 @@ export class BlobStorage {
230
257
  if (refCount > 0) {
231
258
  return;
232
259
  }
260
+ // Determine prefix by checking which one exists
261
+ let prefix = 'blob';
262
+ for (const tryPrefix of ['commit', 'tree', 'blob']) {
263
+ const exists = await this.adapter.get(`${tryPrefix}:${hash}`);
264
+ if (exists !== undefined) {
265
+ prefix = tryPrefix;
266
+ break;
267
+ }
268
+ }
233
269
  // Delete blob data
234
- await this.adapter.delete(`blob:${hash}`);
270
+ await this.adapter.delete(`${prefix}:${hash}`);
235
271
  // Delete metadata
236
- await this.adapter.delete(`blob-meta:${hash}`);
272
+ await this.adapter.delete(`${prefix}-meta:${hash}`);
237
273
  // Remove from cache
238
274
  this.removeFromCache(hash);
239
275
  // Update stats
@@ -246,11 +282,15 @@ export class BlobStorage {
246
282
  * @returns Blob metadata
247
283
  */
248
284
  async getMetadata(hash) {
249
- const data = await this.adapter.get(`blob-meta:${hash}`);
250
- if (!data) {
251
- return undefined;
285
+ // Try to read metadata with type-aware prefix (backward compatible)
286
+ // Try commit, tree, then blob prefixes
287
+ for (const prefix of ['commit', 'tree', 'blob']) {
288
+ const data = await this.adapter.get(`${prefix}-meta:${hash}`);
289
+ if (data) {
290
+ return JSON.parse(data.toString());
291
+ }
252
292
  }
253
- return JSON.parse(data.toString());
293
+ return undefined;
254
294
  }
255
295
  /**
256
296
  * Batch write multiple blobs in parallel
@@ -277,8 +317,16 @@ export class BlobStorage {
277
317
  * @returns Array of blob hashes
278
318
  */
279
319
  async listBlobs() {
280
- const keys = await this.adapter.list('blob:');
281
- return keys.map((key) => key.replace(/^blob:/, ''));
320
+ // List all types of blobs
321
+ const hashes = new Set();
322
+ for (const prefix of ['commit', 'tree', 'blob']) {
323
+ const keys = await this.adapter.list(`${prefix}:`);
324
+ keys.forEach((key) => {
325
+ const hash = key.replace(new RegExp(`^${prefix}:`), '');
326
+ hashes.add(hash);
327
+ });
328
+ }
329
+ return Array.from(hashes);
282
330
  }
283
331
  /**
284
332
  * Get storage statistics
@@ -349,7 +397,8 @@ export class BlobStorage {
349
397
  async writeMultipart(hash, data, metadata) {
350
398
  // For now, just write as single blob
351
399
  // TODO: Implement actual multipart upload for S3/R2/GCS
352
- await this.adapter.put(`blob:${hash}`, data);
400
+ const prefix = metadata.type || 'blob';
401
+ await this.adapter.put(`${prefix}:${hash}`, data);
353
402
  }
354
403
  /**
355
404
  * Increment reference count for a blob
@@ -360,7 +409,8 @@ export class BlobStorage {
360
409
  throw new Error(`Cannot increment ref count, blob not found: ${hash}`);
361
410
  }
362
411
  metadata.refCount++;
363
- await this.adapter.put(`blob-meta:${hash}`, Buffer.from(JSON.stringify(metadata)));
412
+ const prefix = metadata.type || 'blob';
413
+ await this.adapter.put(`${prefix}-meta:${hash}`, Buffer.from(JSON.stringify(metadata)));
364
414
  return metadata.refCount;
365
415
  }
366
416
  /**
@@ -372,7 +422,8 @@ export class BlobStorage {
372
422
  return 0;
373
423
  }
374
424
  metadata.refCount = Math.max(0, metadata.refCount - 1);
375
- await this.adapter.put(`blob-meta:${hash}`, Buffer.from(JSON.stringify(metadata)));
425
+ const prefix = metadata.type || 'blob';
426
+ await this.adapter.put(`${prefix}-meta:${hash}`, Buffer.from(JSON.stringify(metadata)));
376
427
  return metadata.refCount;
377
428
  }
378
429
  /**
@@ -15,6 +15,7 @@
15
15
  * @module storage/cow/CommitObject
16
16
  */
17
17
  import { BlobStorage } from './BlobStorage.js';
18
+ import { isNullHash } from './constants.js';
18
19
  /**
19
20
  * CommitBuilder: Fluent API for building commit objects
20
21
  *
@@ -269,7 +270,10 @@ export class CommitObject {
269
270
  static async *walk(blobStorage, startHash, options) {
270
271
  let currentHash = startHash;
271
272
  let depth = 0;
272
- while (currentHash) {
273
+ // v5.3.4 fix: Guard against NULL hash (sentinel for "no parent")
274
+ // The initial commit has parent = null or NULL_HASH ('0000...0000')
275
+ // We must stop walking when we reach it, not try to read it
276
+ while (currentHash && !isNullHash(currentHash)) {
273
277
  // Check max depth
274
278
  if (options?.maxDepth && depth >= options.maxDepth) {
275
279
  break;
@@ -291,7 +295,7 @@ export class CommitObject {
291
295
  if (options?.stopAt && currentHash === options.stopAt) {
292
296
  break;
293
297
  }
294
- // Move to parent
298
+ // Move to parent (can be null or NULL_HASH for initial commit)
295
299
  currentHash = commit.parent;
296
300
  depth++;
297
301
  }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * COW Storage Constants
3
+ *
4
+ * Sentinel values and utilities for Copy-On-Write storage system.
5
+ *
6
+ * @module storage/cow/constants
7
+ */
8
+ /**
9
+ * NULL_HASH - Sentinel value for "no parent commit" or "empty tree"
10
+ *
11
+ * In Git-like COW systems, we need a way to represent:
12
+ * - Initial commit (has no parent)
13
+ * - Empty tree (contains no files)
14
+ *
15
+ * We use a 64-character zero hash as a sentinel value.
16
+ * This should NEVER be used as an actual content hash.
17
+ *
18
+ * @constant
19
+ * @example
20
+ * ```typescript
21
+ * const builder = CommitBuilder.create(storage)
22
+ * .tree(NULL_HASH) // Empty tree
23
+ * .parent(null) // No parent (use null, not NULL_HASH)
24
+ * .build()
25
+ * ```
26
+ */
27
+ export declare const NULL_HASH = "0000000000000000000000000000000000000000000000000000000000000000";
28
+ /**
29
+ * Check if a hash is the NULL sentinel value
30
+ *
31
+ * @param hash - Hash to check (can be string or null)
32
+ * @returns true if hash is null or NULL_HASH
33
+ *
34
+ * @example
35
+ * ```typescript
36
+ * if (isNullHash(commit.parent)) {
37
+ * console.log('This is the initial commit')
38
+ * }
39
+ * ```
40
+ */
41
+ export declare function isNullHash(hash: string | null | undefined): boolean;
42
+ /**
43
+ * Check if a hash is valid (non-null, non-empty, proper format)
44
+ *
45
+ * @param hash - Hash to check
46
+ * @returns true if hash is a valid SHA-256 hash
47
+ */
48
+ export declare function isValidHash(hash: string | null | undefined): boolean;
@@ -0,0 +1,57 @@
1
+ /**
2
+ * COW Storage Constants
3
+ *
4
+ * Sentinel values and utilities for Copy-On-Write storage system.
5
+ *
6
+ * @module storage/cow/constants
7
+ */
8
+ /**
9
+ * NULL_HASH - Sentinel value for "no parent commit" or "empty tree"
10
+ *
11
+ * In Git-like COW systems, we need a way to represent:
12
+ * - Initial commit (has no parent)
13
+ * - Empty tree (contains no files)
14
+ *
15
+ * We use a 64-character zero hash as a sentinel value.
16
+ * This should NEVER be used as an actual content hash.
17
+ *
18
+ * @constant
19
+ * @example
20
+ * ```typescript
21
+ * const builder = CommitBuilder.create(storage)
22
+ * .tree(NULL_HASH) // Empty tree
23
+ * .parent(null) // No parent (use null, not NULL_HASH)
24
+ * .build()
25
+ * ```
26
+ */
27
+ export const NULL_HASH = '0000000000000000000000000000000000000000000000000000000000000000';
28
+ /**
29
+ * Check if a hash is the NULL sentinel value
30
+ *
31
+ * @param hash - Hash to check (can be string or null)
32
+ * @returns true if hash is null or NULL_HASH
33
+ *
34
+ * @example
35
+ * ```typescript
36
+ * if (isNullHash(commit.parent)) {
37
+ * console.log('This is the initial commit')
38
+ * }
39
+ * ```
40
+ */
41
+ export function isNullHash(hash) {
42
+ return hash === null || hash === undefined || hash === NULL_HASH;
43
+ }
44
+ /**
45
+ * Check if a hash is valid (non-null, non-empty, proper format)
46
+ *
47
+ * @param hash - Hash to check
48
+ * @returns true if hash is a valid SHA-256 hash
49
+ */
50
+ export function isValidHash(hash) {
51
+ if (isNullHash(hash)) {
52
+ return false;
53
+ }
54
+ // SHA-256 hash must be exactly 64 hexadecimal characters
55
+ return typeof hash === 'string' && /^[a-f0-9]{64}$/.test(hash);
56
+ }
57
+ //# sourceMappingURL=constants.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soulcraft/brainy",
3
- "version": "5.3.2",
3
+ "version": "5.3.4",
4
4
  "description": "Universal Knowledge Protocol™ - World's first Triple Intelligence database unifying vector, graph, and document search in one API. 31 nouns × 40 verbs for infinite expressiveness.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",