@ruvector/edge-net 0.5.0 → 0.5.3
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/README.md +281 -10
- package/core-invariants.js +942 -0
- package/models/adapter-hub.js +1008 -0
- package/models/adapter-security.js +792 -0
- package/models/benchmark.js +688 -0
- package/models/distribution.js +791 -0
- package/models/index.js +109 -0
- package/models/integrity.js +753 -0
- package/models/loader.js +725 -0
- package/models/microlora.js +1298 -0
- package/models/model-loader.js +922 -0
- package/models/model-optimizer.js +1245 -0
- package/models/model-registry.js +696 -0
- package/models/model-utils.js +548 -0
- package/models/models-cli.js +914 -0
- package/models/registry.json +214 -0
- package/models/training-utils.js +1418 -0
- package/models/wasm-core.js +1025 -0
- package/network-genesis.js +2847 -0
- package/onnx-worker.js +462 -8
- package/package.json +33 -3
- package/plugins/SECURITY-AUDIT.md +654 -0
- package/plugins/cli.js +43 -3
- package/plugins/implementations/e2e-encryption.js +57 -12
- package/plugins/plugin-loader.js +610 -21
- package/tests/model-optimizer.test.js +644 -0
- package/tests/network-genesis.test.js +562 -0
- package/tests/plugin-benchmark.js +1239 -0
- package/tests/plugin-system-test.js +163 -0
- package/tests/wasm-core.test.js +368 -0
|
@@ -0,0 +1,696 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @ruvector/edge-net Model Registry
|
|
3
|
+
*
|
|
4
|
+
* Manages model metadata, versions, dependencies, and discovery
|
|
5
|
+
* for the distributed model distribution infrastructure.
|
|
6
|
+
*
|
|
7
|
+
* @module @ruvector/edge-net/models/model-registry
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { EventEmitter } from 'events';
|
|
11
|
+
import { createHash, randomBytes } from 'crypto';
|
|
12
|
+
import { promises as fs } from 'fs';
|
|
13
|
+
import path from 'path';
|
|
14
|
+
|
|
15
|
+
// ============================================
|
|
16
|
+
// SEMVER UTILITIES
|
|
17
|
+
// ============================================
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Parse a semver version string
|
|
21
|
+
* @param {string} version - Version string (e.g., "1.2.3", "1.0.0-beta.1")
|
|
22
|
+
* @returns {object} Parsed version object
|
|
23
|
+
*/
|
|
24
|
+
export function parseSemver(version) {
|
|
25
|
+
const match = String(version).match(
|
|
26
|
+
/^(\d+)\.(\d+)\.(\d+)(?:-([a-zA-Z0-9.-]+))?(?:\+([a-zA-Z0-9.-]+))?$/
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
if (!match) {
|
|
30
|
+
throw new Error(`Invalid semver: ${version}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
major: parseInt(match[1], 10),
|
|
35
|
+
minor: parseInt(match[2], 10),
|
|
36
|
+
patch: parseInt(match[3], 10),
|
|
37
|
+
prerelease: match[4] || null,
|
|
38
|
+
build: match[5] || null,
|
|
39
|
+
raw: version,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Compare two semver versions
|
|
45
|
+
* @param {string} a - First version
|
|
46
|
+
* @param {string} b - Second version
|
|
47
|
+
* @returns {number} -1 if a < b, 0 if equal, 1 if a > b
|
|
48
|
+
*/
|
|
49
|
+
export function compareSemver(a, b) {
|
|
50
|
+
const va = parseSemver(a);
|
|
51
|
+
const vb = parseSemver(b);
|
|
52
|
+
|
|
53
|
+
if (va.major !== vb.major) return va.major - vb.major;
|
|
54
|
+
if (va.minor !== vb.minor) return va.minor - vb.minor;
|
|
55
|
+
if (va.patch !== vb.patch) return va.patch - vb.patch;
|
|
56
|
+
|
|
57
|
+
// Prerelease versions have lower precedence
|
|
58
|
+
if (va.prerelease && !vb.prerelease) return -1;
|
|
59
|
+
if (!va.prerelease && vb.prerelease) return 1;
|
|
60
|
+
if (va.prerelease && vb.prerelease) {
|
|
61
|
+
return va.prerelease.localeCompare(vb.prerelease);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Check if version satisfies a version range
|
|
69
|
+
* Supports: "1.0.0", "^1.0.0", "~1.0.0", ">=1.0.0", "1.x", "*"
|
|
70
|
+
* @param {string} version - Version to check
|
|
71
|
+
* @param {string} range - Version range
|
|
72
|
+
* @returns {boolean}
|
|
73
|
+
*/
|
|
74
|
+
export function satisfiesSemver(version, range) {
|
|
75
|
+
const v = parseSemver(version);
|
|
76
|
+
|
|
77
|
+
// Exact match
|
|
78
|
+
if (range === version) return true;
|
|
79
|
+
|
|
80
|
+
// Wildcard
|
|
81
|
+
if (range === '*' || range === 'latest') return true;
|
|
82
|
+
|
|
83
|
+
// X-range: 1.x, 1.2.x
|
|
84
|
+
const xMatch = range.match(/^(\d+)(?:\.(\d+))?\.x$/);
|
|
85
|
+
if (xMatch) {
|
|
86
|
+
const major = parseInt(xMatch[1], 10);
|
|
87
|
+
const minor = xMatch[2] ? parseInt(xMatch[2], 10) : null;
|
|
88
|
+
if (v.major !== major) return false;
|
|
89
|
+
if (minor !== null && v.minor !== minor) return false;
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Caret range: ^1.0.0 (compatible with)
|
|
94
|
+
if (range.startsWith('^')) {
|
|
95
|
+
const r = parseSemver(range.slice(1));
|
|
96
|
+
if (v.major !== r.major) return false;
|
|
97
|
+
if (v.major === 0) {
|
|
98
|
+
if (v.minor !== r.minor) return false;
|
|
99
|
+
return v.patch >= r.patch;
|
|
100
|
+
}
|
|
101
|
+
return compareSemver(version, range.slice(1)) >= 0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Tilde range: ~1.0.0 (approximately equivalent)
|
|
105
|
+
if (range.startsWith('~')) {
|
|
106
|
+
const r = parseSemver(range.slice(1));
|
|
107
|
+
if (v.major !== r.major) return false;
|
|
108
|
+
if (v.minor !== r.minor) return false;
|
|
109
|
+
return v.patch >= r.patch;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Comparison ranges: >=1.0.0, >1.0.0, <=1.0.0, <1.0.0
|
|
113
|
+
if (range.startsWith('>=')) {
|
|
114
|
+
return compareSemver(version, range.slice(2)) >= 0;
|
|
115
|
+
}
|
|
116
|
+
if (range.startsWith('>')) {
|
|
117
|
+
return compareSemver(version, range.slice(1)) > 0;
|
|
118
|
+
}
|
|
119
|
+
if (range.startsWith('<=')) {
|
|
120
|
+
return compareSemver(version, range.slice(2)) <= 0;
|
|
121
|
+
}
|
|
122
|
+
if (range.startsWith('<')) {
|
|
123
|
+
return compareSemver(version, range.slice(1)) < 0;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Fallback to exact match
|
|
127
|
+
return compareSemver(version, range) === 0;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Get the latest version from a list
|
|
132
|
+
* @param {string[]} versions - List of version strings
|
|
133
|
+
* @returns {string} Latest version
|
|
134
|
+
*/
|
|
135
|
+
export function getLatestVersion(versions) {
|
|
136
|
+
if (!versions || versions.length === 0) return null;
|
|
137
|
+
return versions.sort((a, b) => compareSemver(b, a))[0];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ============================================
|
|
141
|
+
// MODEL METADATA
|
|
142
|
+
// ============================================
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Model metadata structure
|
|
146
|
+
* @typedef {object} ModelMetadata
|
|
147
|
+
* @property {string} name - Model identifier (e.g., "phi-1.5-int4")
|
|
148
|
+
* @property {string} version - Semantic version
|
|
149
|
+
* @property {number} size - Model size in bytes
|
|
150
|
+
* @property {string} hash - SHA256 hash for integrity
|
|
151
|
+
* @property {string} format - Model format (onnx, safetensors, gguf)
|
|
152
|
+
* @property {string[]} capabilities - Model capabilities
|
|
153
|
+
* @property {object} sources - Download sources (gcs, ipfs, cdn)
|
|
154
|
+
* @property {object} dependencies - Base models and adapters
|
|
155
|
+
* @property {object} quantization - Quantization details
|
|
156
|
+
* @property {object} metadata - Additional metadata
|
|
157
|
+
*/
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Create a model metadata object
|
|
161
|
+
* @param {object} options - Model options
|
|
162
|
+
* @returns {ModelMetadata}
|
|
163
|
+
*/
|
|
164
|
+
export function createModelMetadata(options) {
|
|
165
|
+
const {
|
|
166
|
+
name,
|
|
167
|
+
version = '1.0.0',
|
|
168
|
+
size = 0,
|
|
169
|
+
hash = '',
|
|
170
|
+
format = 'onnx',
|
|
171
|
+
capabilities = [],
|
|
172
|
+
sources = {},
|
|
173
|
+
dependencies = {},
|
|
174
|
+
quantization = null,
|
|
175
|
+
metadata = {},
|
|
176
|
+
} = options;
|
|
177
|
+
|
|
178
|
+
if (!name) {
|
|
179
|
+
throw new Error('Model name is required');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Validate version
|
|
183
|
+
parseSemver(version);
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
name,
|
|
187
|
+
version,
|
|
188
|
+
size,
|
|
189
|
+
hash,
|
|
190
|
+
format,
|
|
191
|
+
capabilities: Array.isArray(capabilities) ? capabilities : [capabilities],
|
|
192
|
+
sources: {
|
|
193
|
+
gcs: sources.gcs || null,
|
|
194
|
+
ipfs: sources.ipfs || null,
|
|
195
|
+
cdn: sources.cdn || null,
|
|
196
|
+
...sources,
|
|
197
|
+
},
|
|
198
|
+
dependencies: {
|
|
199
|
+
base: dependencies.base || null,
|
|
200
|
+
adapters: dependencies.adapters || [],
|
|
201
|
+
...dependencies,
|
|
202
|
+
},
|
|
203
|
+
quantization: quantization ? {
|
|
204
|
+
type: quantization.type || 'int4',
|
|
205
|
+
bits: quantization.bits || 4,
|
|
206
|
+
blockSize: quantization.blockSize || 32,
|
|
207
|
+
symmetric: quantization.symmetric ?? true,
|
|
208
|
+
} : null,
|
|
209
|
+
metadata: {
|
|
210
|
+
createdAt: metadata.createdAt || new Date().toISOString(),
|
|
211
|
+
updatedAt: metadata.updatedAt || new Date().toISOString(),
|
|
212
|
+
author: metadata.author || 'RuVector',
|
|
213
|
+
license: metadata.license || 'Apache-2.0',
|
|
214
|
+
description: metadata.description || '',
|
|
215
|
+
tags: metadata.tags || [],
|
|
216
|
+
...metadata,
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ============================================
|
|
222
|
+
// MODEL REGISTRY
|
|
223
|
+
// ============================================
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* ModelRegistry - Manages model metadata, versions, and dependencies
|
|
227
|
+
*/
|
|
228
|
+
export class ModelRegistry extends EventEmitter {
|
|
229
|
+
/**
|
|
230
|
+
* Create a new ModelRegistry
|
|
231
|
+
* @param {object} options - Registry options
|
|
232
|
+
*/
|
|
233
|
+
constructor(options = {}) {
|
|
234
|
+
super();
|
|
235
|
+
|
|
236
|
+
this.id = `registry-${randomBytes(6).toString('hex')}`;
|
|
237
|
+
this.registryPath = options.registryPath || null;
|
|
238
|
+
|
|
239
|
+
// Model storage: { modelName: { version: ModelMetadata } }
|
|
240
|
+
this.models = new Map();
|
|
241
|
+
|
|
242
|
+
// Dependency graph
|
|
243
|
+
this.dependencies = new Map();
|
|
244
|
+
|
|
245
|
+
// Search index
|
|
246
|
+
this.searchIndex = {
|
|
247
|
+
byCapability: new Map(),
|
|
248
|
+
byFormat: new Map(),
|
|
249
|
+
byTag: new Map(),
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
// Stats
|
|
253
|
+
this.stats = {
|
|
254
|
+
totalModels: 0,
|
|
255
|
+
totalVersions: 0,
|
|
256
|
+
totalSize: 0,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Register a new model or version
|
|
262
|
+
* @param {object} modelData - Model metadata
|
|
263
|
+
* @returns {ModelMetadata}
|
|
264
|
+
*/
|
|
265
|
+
register(modelData) {
|
|
266
|
+
const metadata = createModelMetadata(modelData);
|
|
267
|
+
const { name, version } = metadata;
|
|
268
|
+
|
|
269
|
+
// Get or create model entry
|
|
270
|
+
if (!this.models.has(name)) {
|
|
271
|
+
this.models.set(name, new Map());
|
|
272
|
+
this.stats.totalModels++;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const versions = this.models.get(name);
|
|
276
|
+
|
|
277
|
+
// Check if version exists
|
|
278
|
+
if (versions.has(version)) {
|
|
279
|
+
this.emit('warning', {
|
|
280
|
+
type: 'version_exists',
|
|
281
|
+
model: name,
|
|
282
|
+
version,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Store metadata
|
|
287
|
+
versions.set(version, metadata);
|
|
288
|
+
this.stats.totalVersions++;
|
|
289
|
+
this.stats.totalSize += metadata.size;
|
|
290
|
+
|
|
291
|
+
// Update search index
|
|
292
|
+
this._indexModel(metadata);
|
|
293
|
+
|
|
294
|
+
// Update dependency graph
|
|
295
|
+
this._updateDependencies(metadata);
|
|
296
|
+
|
|
297
|
+
this.emit('registered', { name, version, metadata });
|
|
298
|
+
|
|
299
|
+
return metadata;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Get model metadata
|
|
304
|
+
* @param {string} name - Model name
|
|
305
|
+
* @param {string} version - Version (default: latest)
|
|
306
|
+
* @returns {ModelMetadata|null}
|
|
307
|
+
*/
|
|
308
|
+
get(name, version = 'latest') {
|
|
309
|
+
const versions = this.models.get(name);
|
|
310
|
+
if (!versions) return null;
|
|
311
|
+
|
|
312
|
+
if (version === 'latest') {
|
|
313
|
+
const latest = getLatestVersion([...versions.keys()]);
|
|
314
|
+
return latest ? versions.get(latest) : null;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Check for exact match first
|
|
318
|
+
if (versions.has(version)) {
|
|
319
|
+
return versions.get(version);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Try to find matching version in range
|
|
323
|
+
for (const [v, metadata] of versions) {
|
|
324
|
+
if (satisfiesSemver(v, version)) {
|
|
325
|
+
return metadata;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* List all versions of a model
|
|
334
|
+
* @param {string} name - Model name
|
|
335
|
+
* @returns {string[]}
|
|
336
|
+
*/
|
|
337
|
+
listVersions(name) {
|
|
338
|
+
const versions = this.models.get(name);
|
|
339
|
+
if (!versions) return [];
|
|
340
|
+
|
|
341
|
+
return [...versions.keys()].sort((a, b) => compareSemver(b, a));
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* List all registered models
|
|
346
|
+
* @returns {string[]}
|
|
347
|
+
*/
|
|
348
|
+
listModels() {
|
|
349
|
+
return [...this.models.keys()];
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Search for models
|
|
354
|
+
* @param {object} criteria - Search criteria
|
|
355
|
+
* @returns {ModelMetadata[]}
|
|
356
|
+
*/
|
|
357
|
+
search(criteria = {}) {
|
|
358
|
+
const {
|
|
359
|
+
name = null,
|
|
360
|
+
capability = null,
|
|
361
|
+
format = null,
|
|
362
|
+
tag = null,
|
|
363
|
+
minVersion = null,
|
|
364
|
+
maxVersion = null,
|
|
365
|
+
maxSize = null,
|
|
366
|
+
query = null,
|
|
367
|
+
} = criteria;
|
|
368
|
+
|
|
369
|
+
let results = [];
|
|
370
|
+
|
|
371
|
+
// Start with all models or filtered by name
|
|
372
|
+
if (name) {
|
|
373
|
+
const versions = this.models.get(name);
|
|
374
|
+
if (versions) {
|
|
375
|
+
results = [...versions.values()];
|
|
376
|
+
}
|
|
377
|
+
} else {
|
|
378
|
+
// Collect all model versions
|
|
379
|
+
for (const versions of this.models.values()) {
|
|
380
|
+
results.push(...versions.values());
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Filter by capability
|
|
385
|
+
if (capability) {
|
|
386
|
+
results = results.filter(m =>
|
|
387
|
+
m.capabilities.includes(capability)
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Filter by format
|
|
392
|
+
if (format) {
|
|
393
|
+
results = results.filter(m => m.format === format);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Filter by tag
|
|
397
|
+
if (tag) {
|
|
398
|
+
results = results.filter(m =>
|
|
399
|
+
m.metadata.tags && m.metadata.tags.includes(tag)
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Filter by version range
|
|
404
|
+
if (minVersion) {
|
|
405
|
+
results = results.filter(m =>
|
|
406
|
+
compareSemver(m.version, minVersion) >= 0
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (maxVersion) {
|
|
411
|
+
results = results.filter(m =>
|
|
412
|
+
compareSemver(m.version, maxVersion) <= 0
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Filter by size
|
|
417
|
+
if (maxSize) {
|
|
418
|
+
results = results.filter(m => m.size <= maxSize);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Text search
|
|
422
|
+
if (query) {
|
|
423
|
+
const q = query.toLowerCase();
|
|
424
|
+
results = results.filter(m =>
|
|
425
|
+
m.name.toLowerCase().includes(q) ||
|
|
426
|
+
m.metadata.description?.toLowerCase().includes(q) ||
|
|
427
|
+
m.metadata.tags?.some(t => t.toLowerCase().includes(q))
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return results;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Get models by capability
|
|
436
|
+
* @param {string} capability - Capability to search for
|
|
437
|
+
* @returns {ModelMetadata[]}
|
|
438
|
+
*/
|
|
439
|
+
getByCapability(capability) {
|
|
440
|
+
const models = this.searchIndex.byCapability.get(capability);
|
|
441
|
+
if (!models) return [];
|
|
442
|
+
|
|
443
|
+
return models.map(key => {
|
|
444
|
+
const [name, version] = key.split('@');
|
|
445
|
+
return this.get(name, version);
|
|
446
|
+
}).filter(Boolean);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Get all dependencies for a model
|
|
451
|
+
* @param {string} name - Model name
|
|
452
|
+
* @param {string} version - Version
|
|
453
|
+
* @param {boolean} recursive - Include transitive dependencies
|
|
454
|
+
* @returns {ModelMetadata[]}
|
|
455
|
+
*/
|
|
456
|
+
getDependencies(name, version = 'latest', recursive = true) {
|
|
457
|
+
const model = this.get(name, version);
|
|
458
|
+
if (!model) return [];
|
|
459
|
+
|
|
460
|
+
const deps = [];
|
|
461
|
+
const visited = new Set();
|
|
462
|
+
const queue = [model];
|
|
463
|
+
|
|
464
|
+
while (queue.length > 0) {
|
|
465
|
+
const current = queue.shift();
|
|
466
|
+
const key = `${current.name}@${current.version}`;
|
|
467
|
+
|
|
468
|
+
if (visited.has(key)) continue;
|
|
469
|
+
visited.add(key);
|
|
470
|
+
|
|
471
|
+
// Add base model
|
|
472
|
+
if (current.dependencies.base) {
|
|
473
|
+
const [baseName, baseVersion] = current.dependencies.base.split('@');
|
|
474
|
+
const baseDep = this.get(baseName, baseVersion || 'latest');
|
|
475
|
+
if (baseDep) {
|
|
476
|
+
deps.push(baseDep);
|
|
477
|
+
if (recursive) queue.push(baseDep);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Add adapters
|
|
482
|
+
if (current.dependencies.adapters) {
|
|
483
|
+
for (const adapter of current.dependencies.adapters) {
|
|
484
|
+
const [adapterName, adapterVersion] = adapter.split('@');
|
|
485
|
+
const adapterDep = this.get(adapterName, adapterVersion || 'latest');
|
|
486
|
+
if (adapterDep) {
|
|
487
|
+
deps.push(adapterDep);
|
|
488
|
+
if (recursive) queue.push(adapterDep);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return deps;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Get dependents (models that depend on this one)
|
|
499
|
+
* @param {string} name - Model name
|
|
500
|
+
* @param {string} version - Version
|
|
501
|
+
* @returns {ModelMetadata[]}
|
|
502
|
+
*/
|
|
503
|
+
getDependents(name, version = 'latest') {
|
|
504
|
+
const key = version === 'latest'
|
|
505
|
+
? name
|
|
506
|
+
: `${name}@${version}`;
|
|
507
|
+
|
|
508
|
+
const dependents = [];
|
|
509
|
+
|
|
510
|
+
for (const [depKey, dependencies] of this.dependencies) {
|
|
511
|
+
if (dependencies.includes(key) || dependencies.includes(name)) {
|
|
512
|
+
const [modelName, modelVersion] = depKey.split('@');
|
|
513
|
+
const model = this.get(modelName, modelVersion);
|
|
514
|
+
if (model) dependents.push(model);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
return dependents;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Compute hash for a model file
|
|
523
|
+
* @param {Buffer|Uint8Array} data - Model data
|
|
524
|
+
* @returns {string} SHA256 hash
|
|
525
|
+
*/
|
|
526
|
+
static computeHash(data) {
|
|
527
|
+
return `sha256:${createHash('sha256').update(data).digest('hex')}`;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Verify model integrity
|
|
532
|
+
* @param {string} name - Model name
|
|
533
|
+
* @param {string} version - Version
|
|
534
|
+
* @param {Buffer|Uint8Array} data - Model data
|
|
535
|
+
* @returns {boolean}
|
|
536
|
+
*/
|
|
537
|
+
verify(name, version, data) {
|
|
538
|
+
const model = this.get(name, version);
|
|
539
|
+
if (!model) return false;
|
|
540
|
+
|
|
541
|
+
const computedHash = ModelRegistry.computeHash(data);
|
|
542
|
+
return model.hash === computedHash;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Update search index for a model
|
|
547
|
+
* @private
|
|
548
|
+
*/
|
|
549
|
+
_indexModel(metadata) {
|
|
550
|
+
const key = `${metadata.name}@${metadata.version}`;
|
|
551
|
+
|
|
552
|
+
// Index by capability
|
|
553
|
+
for (const cap of metadata.capabilities) {
|
|
554
|
+
if (!this.searchIndex.byCapability.has(cap)) {
|
|
555
|
+
this.searchIndex.byCapability.set(cap, []);
|
|
556
|
+
}
|
|
557
|
+
this.searchIndex.byCapability.get(cap).push(key);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Index by format
|
|
561
|
+
if (!this.searchIndex.byFormat.has(metadata.format)) {
|
|
562
|
+
this.searchIndex.byFormat.set(metadata.format, []);
|
|
563
|
+
}
|
|
564
|
+
this.searchIndex.byFormat.get(metadata.format).push(key);
|
|
565
|
+
|
|
566
|
+
// Index by tags
|
|
567
|
+
if (metadata.metadata.tags) {
|
|
568
|
+
for (const tag of metadata.metadata.tags) {
|
|
569
|
+
if (!this.searchIndex.byTag.has(tag)) {
|
|
570
|
+
this.searchIndex.byTag.set(tag, []);
|
|
571
|
+
}
|
|
572
|
+
this.searchIndex.byTag.get(tag).push(key);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Update dependency graph
|
|
579
|
+
* @private
|
|
580
|
+
*/
|
|
581
|
+
_updateDependencies(metadata) {
|
|
582
|
+
const key = `${metadata.name}@${metadata.version}`;
|
|
583
|
+
const deps = [];
|
|
584
|
+
|
|
585
|
+
if (metadata.dependencies.base) {
|
|
586
|
+
deps.push(metadata.dependencies.base);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
if (metadata.dependencies.adapters) {
|
|
590
|
+
deps.push(...metadata.dependencies.adapters);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
if (deps.length > 0) {
|
|
594
|
+
this.dependencies.set(key, deps);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Export registry to JSON
|
|
600
|
+
* @returns {object}
|
|
601
|
+
*/
|
|
602
|
+
export() {
|
|
603
|
+
const models = {};
|
|
604
|
+
|
|
605
|
+
for (const [name, versions] of this.models) {
|
|
606
|
+
models[name] = {};
|
|
607
|
+
for (const [version, metadata] of versions) {
|
|
608
|
+
models[name][version] = metadata;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
return {
|
|
613
|
+
version: '1.0.0',
|
|
614
|
+
generatedAt: new Date().toISOString(),
|
|
615
|
+
stats: this.stats,
|
|
616
|
+
models,
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Import registry from JSON
|
|
622
|
+
* @param {object} data - Registry data
|
|
623
|
+
*/
|
|
624
|
+
import(data) {
|
|
625
|
+
if (!data.models) return;
|
|
626
|
+
|
|
627
|
+
for (const [name, versions] of Object.entries(data.models)) {
|
|
628
|
+
for (const [version, metadata] of Object.entries(versions)) {
|
|
629
|
+
this.register({
|
|
630
|
+
...metadata,
|
|
631
|
+
name,
|
|
632
|
+
version,
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Save registry to file
|
|
640
|
+
* @param {string} filePath - File path
|
|
641
|
+
*/
|
|
642
|
+
async save(filePath = null) {
|
|
643
|
+
const targetPath = filePath || this.registryPath;
|
|
644
|
+
if (!targetPath) {
|
|
645
|
+
throw new Error('No registry path specified');
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const data = JSON.stringify(this.export(), null, 2);
|
|
649
|
+
await fs.writeFile(targetPath, data, 'utf-8');
|
|
650
|
+
|
|
651
|
+
this.emit('saved', { path: targetPath });
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Load registry from file
|
|
656
|
+
* @param {string} filePath - File path
|
|
657
|
+
*/
|
|
658
|
+
async load(filePath = null) {
|
|
659
|
+
const targetPath = filePath || this.registryPath;
|
|
660
|
+
if (!targetPath) {
|
|
661
|
+
throw new Error('No registry path specified');
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
try {
|
|
665
|
+
const data = await fs.readFile(targetPath, 'utf-8');
|
|
666
|
+
this.import(JSON.parse(data));
|
|
667
|
+
this.emit('loaded', { path: targetPath });
|
|
668
|
+
} catch (error) {
|
|
669
|
+
if (error.code === 'ENOENT') {
|
|
670
|
+
this.emit('warning', { message: 'Registry file not found, starting fresh' });
|
|
671
|
+
} else {
|
|
672
|
+
throw error;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* Get registry statistics
|
|
679
|
+
* @returns {object}
|
|
680
|
+
*/
|
|
681
|
+
getStats() {
|
|
682
|
+
return {
|
|
683
|
+
...this.stats,
|
|
684
|
+
capabilities: this.searchIndex.byCapability.size,
|
|
685
|
+
formats: this.searchIndex.byFormat.size,
|
|
686
|
+
tags: this.searchIndex.byTag.size,
|
|
687
|
+
dependencyEdges: this.dependencies.size,
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// ============================================
|
|
693
|
+
// DEFAULT EXPORT
|
|
694
|
+
// ============================================
|
|
695
|
+
|
|
696
|
+
export default ModelRegistry;
|