@soulcraft/brainy 5.2.0 → 5.3.0

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
@@ -16,6 +16,7 @@ import { NaturalLanguageProcessor } from './neural/naturalLanguageProcessor.js';
16
16
  import { NeuralEntityExtractor } from './neural/entityExtractor.js';
17
17
  import { TripleIntelligenceSystem } from './triple/TripleIntelligenceSystem.js';
18
18
  import { VirtualFileSystem } from './vfs/VirtualFileSystem.js';
19
+ import { VersioningAPI } from './versioning/VersioningAPI.js';
19
20
  import { MetadataIndexManager } from './utils/metadataIndex.js';
20
21
  import { GraphAdjacencyIndex } from './graph/graphAdjacencyIndex.js';
21
22
  import { CommitBuilder } from './storage/cow/CommitObject.js';
@@ -1868,7 +1869,20 @@ export class Brainy {
1868
1869
  }
1869
1870
  const refManager = this.storage.refManager;
1870
1871
  const currentBranch = this.storage.currentBranch || 'main';
1871
- // Step 1: Copy storage ref (COW layer - instant!)
1872
+ // Step 1: Ensure initial commit exists (required for fork)
1873
+ const currentRef = await refManager.getRef(currentBranch);
1874
+ if (!currentRef) {
1875
+ // Auto-create initial commit if none exists
1876
+ await this.commit({
1877
+ message: `Initial commit on ${currentBranch}`,
1878
+ author: options?.author || 'Brainy',
1879
+ metadata: { timestamp: Date.now() }
1880
+ });
1881
+ if (!this.config.silent) {
1882
+ console.log(`📝 Auto-created initial commit on ${currentBranch} (required for fork)`);
1883
+ }
1884
+ }
1885
+ // Step 2: Copy storage ref (COW layer - instant!)
1872
1886
  await refManager.copyRef(currentBranch, branchName);
1873
1887
  // Step 2: Create new Brainy instance pointing to fork branch
1874
1888
  const forkConfig = {
@@ -2038,7 +2052,7 @@ export class Brainy {
2038
2052
  // Build and persist commit (returns hash directly)
2039
2053
  const commitHash = await builder.build();
2040
2054
  // Update branch ref to point to new commit
2041
- await refManager.setRef(`heads/${currentBranch}`, commitHash, {
2055
+ await refManager.setRef(currentBranch, commitHash, {
2042
2056
  author: options?.author || 'unknown',
2043
2057
  message: options?.message || 'Snapshot commit'
2044
2058
  });
@@ -2491,6 +2505,37 @@ export class Brainy {
2491
2505
  }
2492
2506
  return this._neural;
2493
2507
  }
2508
+ /**
2509
+ * Versioning API - Entity version control (v5.3.0)
2510
+ *
2511
+ * Provides entity-level versioning with:
2512
+ * - save() - Create version of entity
2513
+ * - restore() - Restore entity to specific version
2514
+ * - list() - List all versions of entity
2515
+ * - compare() - Deep diff between versions
2516
+ * - prune() - Remove old versions (retention policies)
2517
+ *
2518
+ * @example
2519
+ * ```typescript
2520
+ * // Save current state
2521
+ * const version = await brain.versions.save('user-123', { tag: 'v1.0' })
2522
+ *
2523
+ * // List versions
2524
+ * const versions = await brain.versions.list('user-123')
2525
+ *
2526
+ * // Restore to previous version
2527
+ * await brain.versions.restore('user-123', 5)
2528
+ *
2529
+ * // Compare versions
2530
+ * const diff = await brain.versions.compare('user-123', 2, 5)
2531
+ * ```
2532
+ */
2533
+ get versions() {
2534
+ if (!this._versions) {
2535
+ this._versions = new VersioningAPI(this);
2536
+ }
2537
+ return this._versions;
2538
+ }
2494
2539
  /**
2495
2540
  * Natural Language Processing API
2496
2541
  */
@@ -3711,12 +3756,19 @@ export class Brainy {
3711
3756
  const graphIndexSize = await this.graphIndex.size();
3712
3757
  const needsRebuild = metadataStats.totalEntries === 0 ||
3713
3758
  hnswIndexSize === 0 ||
3714
- graphIndexSize === 0 ||
3715
- this.config.disableAutoRebuild === false; // Explicitly enabled
3759
+ graphIndexSize === 0;
3716
3760
  if (!needsRebuild) {
3717
3761
  // All indexes already populated, no rebuild needed
3718
3762
  return;
3719
3763
  }
3764
+ // BUG FIX: If disableAutoRebuild is truthy, skip rebuild even if indexes are empty
3765
+ // Indexes will load lazily on first query
3766
+ if (this.config.disableAutoRebuild) {
3767
+ if (!this.config.silent) {
3768
+ console.log('⚡ Indexes empty but auto-rebuild disabled - using lazy loading');
3769
+ }
3770
+ return;
3771
+ }
3720
3772
  // Small dataset: Rebuild all indexes for best performance
3721
3773
  if (totalCount < AUTO_REBUILD_THRESHOLD || this.config.disableAutoRebuild === false) {
3722
3774
  if (!this.config.silent) {
@@ -0,0 +1,112 @@
1
+ /**
2
+ * VersionDiff - Deep Object Comparison for Entity Versions (v5.3.0)
3
+ *
4
+ * Provides deep diff between entity versions:
5
+ * - Field-level change detection
6
+ * - Nested object comparison
7
+ * - Array diffing
8
+ * - Type change detection
9
+ * - Human-readable diff output
10
+ *
11
+ * NO MOCKS - Production implementation
12
+ */
13
+ import type { NounMetadata } from '../coreTypes.js';
14
+ /**
15
+ * Types of changes in a diff
16
+ */
17
+ export type ChangeType = 'added' | 'removed' | 'modified' | 'type-changed';
18
+ /**
19
+ * A single field change in a diff
20
+ */
21
+ export interface FieldChange {
22
+ /** Path to the field (e.g., 'metadata.user.name') */
23
+ path: string;
24
+ /** Type of change */
25
+ type: ChangeType;
26
+ /** Old value (undefined for 'added') */
27
+ oldValue?: any;
28
+ /** New value (undefined for 'removed') */
29
+ newValue?: any;
30
+ /** Old type (for 'type-changed') */
31
+ oldType?: string;
32
+ /** New type (for 'type-changed') */
33
+ newType?: string;
34
+ }
35
+ /**
36
+ * Complete diff between two versions
37
+ */
38
+ export interface VersionDiff {
39
+ /** Entity ID being compared */
40
+ entityId: string;
41
+ /** From version number */
42
+ fromVersion: number;
43
+ /** To version number */
44
+ toVersion: number;
45
+ /** Fields that were added */
46
+ added: FieldChange[];
47
+ /** Fields that were removed */
48
+ removed: FieldChange[];
49
+ /** Fields that were modified */
50
+ modified: FieldChange[];
51
+ /** Fields whose type changed */
52
+ typeChanged: FieldChange[];
53
+ /** Total number of changes */
54
+ totalChanges: number;
55
+ /** Whether versions are identical */
56
+ identical: boolean;
57
+ }
58
+ /**
59
+ * Options for diff comparison
60
+ */
61
+ export interface DiffOptions {
62
+ /** Entity ID (for context in output) */
63
+ entityId: string;
64
+ /** From version number */
65
+ fromVersion: number;
66
+ /** To version number */
67
+ toVersion: number;
68
+ /** Ignore these fields in comparison */
69
+ ignoreFields?: string[];
70
+ /** Maximum depth for nested object comparison (default: 10) */
71
+ maxDepth?: number;
72
+ /** Include unchanged fields in output (default: false) */
73
+ includeUnchanged?: boolean;
74
+ }
75
+ /**
76
+ * Compare two entity versions and generate diff
77
+ *
78
+ * @param from Old version entity
79
+ * @param to New version entity
80
+ * @param options Diff options
81
+ * @returns Diff between versions
82
+ */
83
+ export declare function compareEntityVersions(from: NounMetadata, to: NounMetadata, options: DiffOptions): VersionDiff;
84
+ /**
85
+ * Format diff as human-readable string
86
+ *
87
+ * @param diff Diff to format
88
+ * @returns Formatted string
89
+ */
90
+ export declare function formatDiff(diff: VersionDiff): string;
91
+ /**
92
+ * Get summary statistics about a diff
93
+ */
94
+ export declare function getDiffStats(diff: VersionDiff): {
95
+ changedFields: number;
96
+ addedFields: number;
97
+ removedFields: number;
98
+ modifiedFields: number;
99
+ typeChangedFields: number;
100
+ };
101
+ /**
102
+ * Check if diff has any changes
103
+ */
104
+ export declare function hasChanges(diff: VersionDiff): boolean;
105
+ /**
106
+ * Get all changed field paths
107
+ */
108
+ export declare function getChangedPaths(diff: VersionDiff): string[];
109
+ /**
110
+ * Filter diff to only include specific paths
111
+ */
112
+ export declare function filterDiff(diff: VersionDiff, paths: string[]): VersionDiff;
@@ -0,0 +1,320 @@
1
+ /**
2
+ * VersionDiff - Deep Object Comparison for Entity Versions (v5.3.0)
3
+ *
4
+ * Provides deep diff between entity versions:
5
+ * - Field-level change detection
6
+ * - Nested object comparison
7
+ * - Array diffing
8
+ * - Type change detection
9
+ * - Human-readable diff output
10
+ *
11
+ * NO MOCKS - Production implementation
12
+ */
13
+ /**
14
+ * Compare two entity versions and generate diff
15
+ *
16
+ * @param from Old version entity
17
+ * @param to New version entity
18
+ * @param options Diff options
19
+ * @returns Diff between versions
20
+ */
21
+ export function compareEntityVersions(from, to, options) {
22
+ const added = [];
23
+ const removed = [];
24
+ const modified = [];
25
+ const typeChanged = [];
26
+ const ignoreFields = new Set(options.ignoreFields || []);
27
+ const maxDepth = options.maxDepth ?? 10;
28
+ // Compare objects recursively
29
+ compareObjects(from, to, '', added, removed, modified, typeChanged, ignoreFields, 0, maxDepth);
30
+ const totalChanges = added.length + removed.length + modified.length + typeChanged.length;
31
+ const identical = totalChanges === 0;
32
+ return {
33
+ entityId: options.entityId,
34
+ fromVersion: options.fromVersion,
35
+ toVersion: options.toVersion,
36
+ added,
37
+ removed,
38
+ modified,
39
+ typeChanged,
40
+ totalChanges,
41
+ identical
42
+ };
43
+ }
44
+ /**
45
+ * Recursively compare two objects
46
+ */
47
+ function compareObjects(from, to, path, added, removed, modified, typeChanged, ignoreFields, depth, maxDepth) {
48
+ if (depth >= maxDepth) {
49
+ // Hit max depth - treat as single value
50
+ if (!deepEqual(from, to)) {
51
+ modified.push({
52
+ path,
53
+ type: 'modified',
54
+ oldValue: from,
55
+ newValue: to
56
+ });
57
+ }
58
+ return;
59
+ }
60
+ // Get all keys from both objects
61
+ const fromKeys = new Set(Object.keys(from || {}));
62
+ const toKeys = new Set(Object.keys(to || {}));
63
+ const allKeys = new Set([...fromKeys, ...toKeys]);
64
+ for (const key of allKeys) {
65
+ const fieldPath = path ? `${path}.${key}` : key;
66
+ // Skip ignored fields
67
+ if (ignoreFields.has(fieldPath) || ignoreFields.has(key)) {
68
+ continue;
69
+ }
70
+ const fromHas = fromKeys.has(key);
71
+ const toHas = toKeys.has(key);
72
+ if (!fromHas && toHas) {
73
+ // Field added
74
+ added.push({
75
+ path: fieldPath,
76
+ type: 'added',
77
+ newValue: to[key]
78
+ });
79
+ }
80
+ else if (fromHas && !toHas) {
81
+ // Field removed
82
+ removed.push({
83
+ path: fieldPath,
84
+ type: 'removed',
85
+ oldValue: from[key]
86
+ });
87
+ }
88
+ else {
89
+ // Field exists in both - check for changes
90
+ const fromValue = from[key];
91
+ const toValue = to[key];
92
+ const fromType = getValueType(fromValue);
93
+ const toType = getValueType(toValue);
94
+ if (fromType !== toType) {
95
+ // Type changed
96
+ typeChanged.push({
97
+ path: fieldPath,
98
+ type: 'type-changed',
99
+ oldValue: fromValue,
100
+ newValue: toValue,
101
+ oldType: fromType,
102
+ newType: toType
103
+ });
104
+ }
105
+ else if (fromType === 'object' && toType === 'object') {
106
+ // Recursively compare nested objects
107
+ compareObjects(fromValue, toValue, fieldPath, added, removed, modified, typeChanged, ignoreFields, depth + 1, maxDepth);
108
+ }
109
+ else if (fromType === 'array' && toType === 'array') {
110
+ // Compare arrays
111
+ if (!arraysEqual(fromValue, toValue)) {
112
+ modified.push({
113
+ path: fieldPath,
114
+ type: 'modified',
115
+ oldValue: fromValue,
116
+ newValue: toValue
117
+ });
118
+ }
119
+ }
120
+ else {
121
+ // Primitive value comparison
122
+ if (!deepEqual(fromValue, toValue)) {
123
+ modified.push({
124
+ path: fieldPath,
125
+ type: 'modified',
126
+ oldValue: fromValue,
127
+ newValue: toValue
128
+ });
129
+ }
130
+ }
131
+ }
132
+ }
133
+ }
134
+ /**
135
+ * Get human-readable type of a value
136
+ */
137
+ function getValueType(value) {
138
+ if (value === null)
139
+ return 'null';
140
+ if (value === undefined)
141
+ return 'undefined';
142
+ if (Array.isArray(value))
143
+ return 'array';
144
+ return typeof value;
145
+ }
146
+ /**
147
+ * Deep equality check
148
+ */
149
+ function deepEqual(a, b) {
150
+ if (a === b)
151
+ return true;
152
+ if (a === null || b === null)
153
+ return false;
154
+ if (a === undefined || b === undefined)
155
+ return false;
156
+ const typeA = getValueType(a);
157
+ const typeB = getValueType(b);
158
+ if (typeA !== typeB)
159
+ return false;
160
+ if (typeA === 'array') {
161
+ return arraysEqual(a, b);
162
+ }
163
+ if (typeA === 'object') {
164
+ return objectsEqual(a, b);
165
+ }
166
+ // Primitive comparison
167
+ return a === b;
168
+ }
169
+ /**
170
+ * Compare arrays for equality
171
+ */
172
+ function arraysEqual(a, b) {
173
+ if (a.length !== b.length)
174
+ return false;
175
+ for (let i = 0; i < a.length; i++) {
176
+ if (!deepEqual(a[i], b[i])) {
177
+ return false;
178
+ }
179
+ }
180
+ return true;
181
+ }
182
+ /**
183
+ * Compare objects for equality
184
+ */
185
+ function objectsEqual(a, b) {
186
+ const keysA = Object.keys(a);
187
+ const keysB = Object.keys(b);
188
+ if (keysA.length !== keysB.length)
189
+ return false;
190
+ for (const key of keysA) {
191
+ if (!keysB.includes(key))
192
+ return false;
193
+ if (!deepEqual(a[key], b[key]))
194
+ return false;
195
+ }
196
+ return true;
197
+ }
198
+ /**
199
+ * Format diff as human-readable string
200
+ *
201
+ * @param diff Diff to format
202
+ * @returns Formatted string
203
+ */
204
+ export function formatDiff(diff) {
205
+ const lines = [];
206
+ lines.push(`Diff: ${diff.entityId} v${diff.fromVersion} → v${diff.toVersion}`);
207
+ lines.push('');
208
+ if (diff.identical) {
209
+ lines.push('No changes');
210
+ return lines.join('\n');
211
+ }
212
+ lines.push(`Total changes: ${diff.totalChanges}`);
213
+ lines.push('');
214
+ if (diff.added.length > 0) {
215
+ lines.push(`Added (${diff.added.length}):`);
216
+ for (const change of diff.added) {
217
+ lines.push(` + ${change.path}: ${formatValue(change.newValue)}`);
218
+ }
219
+ lines.push('');
220
+ }
221
+ if (diff.removed.length > 0) {
222
+ lines.push(`Removed (${diff.removed.length}):`);
223
+ for (const change of diff.removed) {
224
+ lines.push(` - ${change.path}: ${formatValue(change.oldValue)}`);
225
+ }
226
+ lines.push('');
227
+ }
228
+ if (diff.modified.length > 0) {
229
+ lines.push(`Modified (${diff.modified.length}):`);
230
+ for (const change of diff.modified) {
231
+ lines.push(` ~ ${change.path}:`);
232
+ lines.push(` ${formatValue(change.oldValue)}`);
233
+ lines.push(` → ${formatValue(change.newValue)}`);
234
+ }
235
+ lines.push('');
236
+ }
237
+ if (diff.typeChanged.length > 0) {
238
+ lines.push(`Type Changed (${diff.typeChanged.length}):`);
239
+ for (const change of diff.typeChanged) {
240
+ lines.push(` ! ${change.path}: ${change.oldType} → ${change.newType}`);
241
+ lines.push(` ${formatValue(change.oldValue)}`);
242
+ lines.push(` → ${formatValue(change.newValue)}`);
243
+ }
244
+ }
245
+ return lines.join('\n');
246
+ }
247
+ /**
248
+ * Format value for display
249
+ */
250
+ function formatValue(value) {
251
+ if (value === null)
252
+ return 'null';
253
+ if (value === undefined)
254
+ return 'undefined';
255
+ if (typeof value === 'string')
256
+ return `"${value}"`;
257
+ if (typeof value === 'object') {
258
+ try {
259
+ return JSON.stringify(value);
260
+ }
261
+ catch {
262
+ return '[Object]';
263
+ }
264
+ }
265
+ return String(value);
266
+ }
267
+ /**
268
+ * Get summary statistics about a diff
269
+ */
270
+ export function getDiffStats(diff) {
271
+ return {
272
+ changedFields: diff.totalChanges,
273
+ addedFields: diff.added.length,
274
+ removedFields: diff.removed.length,
275
+ modifiedFields: diff.modified.length,
276
+ typeChangedFields: diff.typeChanged.length
277
+ };
278
+ }
279
+ /**
280
+ * Check if diff has any changes
281
+ */
282
+ export function hasChanges(diff) {
283
+ return !diff.identical;
284
+ }
285
+ /**
286
+ * Get all changed field paths
287
+ */
288
+ export function getChangedPaths(diff) {
289
+ const paths = new Set();
290
+ for (const change of diff.added)
291
+ paths.add(change.path);
292
+ for (const change of diff.removed)
293
+ paths.add(change.path);
294
+ for (const change of diff.modified)
295
+ paths.add(change.path);
296
+ for (const change of diff.typeChanged)
297
+ paths.add(change.path);
298
+ return Array.from(paths).sort();
299
+ }
300
+ /**
301
+ * Filter diff to only include specific paths
302
+ */
303
+ export function filterDiff(diff, paths) {
304
+ const pathSet = new Set(paths);
305
+ const filterChanges = (changes) => changes.filter((c) => pathSet.has(c.path) || paths.some((p) => c.path.startsWith(p + '.')));
306
+ const added = filterChanges(diff.added);
307
+ const removed = filterChanges(diff.removed);
308
+ const modified = filterChanges(diff.modified);
309
+ const typeChanged = filterChanges(diff.typeChanged);
310
+ return {
311
+ ...diff,
312
+ added,
313
+ removed,
314
+ modified,
315
+ typeChanged,
316
+ totalChanges: added.length + removed.length + modified.length + typeChanged.length,
317
+ identical: added.length + removed.length + modified.length + typeChanged.length === 0
318
+ };
319
+ }
320
+ //# sourceMappingURL=VersionDiff.js.map
@@ -0,0 +1,126 @@
1
+ /**
2
+ * VersionIndex - Fast Version Lookup Using Existing Index Infrastructure (v5.3.0)
3
+ *
4
+ * Integrates with Brainy's existing index system:
5
+ * - Uses MetadataIndexManager for field indexing
6
+ * - Leverages UnifiedCache for memory management
7
+ * - Uses EntityIdMapper for efficient ID handling
8
+ * - Uses ChunkManager for adaptive chunking
9
+ * - Leverages Roaring Bitmaps for fast set operations
10
+ *
11
+ * Version metadata is stored as regular entities with type='_version'
12
+ * This allows us to use existing index infrastructure without modification!
13
+ *
14
+ * Fields indexed:
15
+ * - versionEntityId: Entity being versioned
16
+ * - versionBranch: Branch version was created on
17
+ * - versionNumber: Version number
18
+ * - versionTag: Optional user tag
19
+ * - versionTimestamp: Creation timestamp
20
+ * - versionCommitHash: Commit hash
21
+ *
22
+ * NO MOCKS - Production implementation
23
+ */
24
+ import type { EntityVersion } from './VersionManager.js';
25
+ import type { VersionQuery } from './VersionManager.js';
26
+ /**
27
+ * VersionIndex - Version lookup and querying using existing indexes
28
+ *
29
+ * Strategy: Store version metadata as special entities with type='_version'
30
+ * This leverages ALL existing index infrastructure automatically!
31
+ */
32
+ export declare class VersionIndex {
33
+ private brain;
34
+ private initialized;
35
+ constructor(brain: any);
36
+ /**
37
+ * Initialize version index
38
+ *
39
+ * No special setup needed - we use existing entity storage and indexes!
40
+ */
41
+ initialize(): Promise<void>;
42
+ /**
43
+ * Add version to index
44
+ *
45
+ * Stores version metadata as a special entity with type='_version'
46
+ * This automatically indexes it using existing MetadataIndexManager!
47
+ *
48
+ * @param version Version metadata
49
+ */
50
+ addVersion(version: EntityVersion): Promise<void>;
51
+ /**
52
+ * Get versions for an entity
53
+ *
54
+ * Uses existing MetadataIndexManager to query efficiently!
55
+ *
56
+ * @param query Version query
57
+ * @returns List of versions (newest first)
58
+ */
59
+ getVersions(query: VersionQuery): Promise<EntityVersion[]>;
60
+ /**
61
+ * Get specific version
62
+ *
63
+ * @param entityId Entity ID
64
+ * @param version Version number
65
+ * @param branch Branch name
66
+ * @returns Version metadata or null
67
+ */
68
+ getVersion(entityId: string, version: number, branch: string): Promise<EntityVersion | null>;
69
+ /**
70
+ * Get version by tag
71
+ *
72
+ * @param entityId Entity ID
73
+ * @param tag Version tag
74
+ * @param branch Branch name
75
+ * @returns Version metadata or null
76
+ */
77
+ getVersionByTag(entityId: string, tag: string, branch: string): Promise<EntityVersion | null>;
78
+ /**
79
+ * Get version count for entity
80
+ *
81
+ * @param entityId Entity ID
82
+ * @param branch Branch name
83
+ * @returns Number of versions
84
+ */
85
+ getVersionCount(entityId: string, branch: string): Promise<number>;
86
+ /**
87
+ * Remove version from index
88
+ *
89
+ * @param entityId Entity ID
90
+ * @param version Version number
91
+ * @param branch Branch name
92
+ */
93
+ removeVersion(entityId: string, version: number, branch: string): Promise<void>;
94
+ /**
95
+ * Convert entity to EntityVersion format
96
+ *
97
+ * @param entity Entity from storage
98
+ * @returns EntityVersion or null if invalid
99
+ */
100
+ private entityToVersion;
101
+ /**
102
+ * Generate unique ID for version entity
103
+ *
104
+ * Format: _version:{entityId}:{version}:{branch}
105
+ *
106
+ * @param entityId Entity ID
107
+ * @param version Version number
108
+ * @param branch Branch name
109
+ * @returns Version entity ID
110
+ */
111
+ private getVersionEntityId;
112
+ /**
113
+ * Get all versioned entities (for cleanup/debugging)
114
+ *
115
+ * @returns List of entity IDs that have versions
116
+ */
117
+ getVersionedEntities(): Promise<string[]>;
118
+ /**
119
+ * Clear all versions for an entity
120
+ *
121
+ * @param entityId Entity ID
122
+ * @param branch Branch name
123
+ * @returns Number of versions deleted
124
+ */
125
+ clearVersions(entityId: string, branch: string): Promise<number>;
126
+ }