@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(
|
|
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
|
-
|
|
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
|
-
|
|
136
|
+
const prefix = options.type || 'blob';
|
|
137
|
+
await this.adapter.put(`${prefix}:${hash}`, finalData);
|
|
136
138
|
}
|
|
137
139
|
// Write metadata
|
|
138
|
-
|
|
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
|
-
//
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
214
|
-
|
|
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(
|
|
270
|
+
await this.adapter.delete(`${prefix}:${hash}`);
|
|
235
271
|
// Delete metadata
|
|
236
|
-
await this.adapter.delete(
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
|
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
|
-
|
|
281
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|