@utaba/deep-memory 0.7.0 → 0.8.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/{StorageProvider-CrIcN06Q.d.ts → StorageProvider-CRenWERy.d.ts} +1 -1
- package/dist/{StorageProvider-CX4mKNph.d.cts → StorageProvider-DmKddrEg.d.cts} +1 -1
- package/dist/index.cjs +368 -296
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -5
- package/dist/index.d.ts +7 -5
- package/dist/index.js +368 -296
- package/dist/index.js.map +1 -1
- package/dist/{portability-fa5sVgsP.d.cts → portability-dhIuWJbJ.d.cts} +3 -1
- package/dist/{portability-fa5sVgsP.d.ts → portability-dhIuWJbJ.d.ts} +3 -1
- package/dist/providers/index.d.cts +3 -3
- package/dist/providers/index.d.ts +3 -3
- package/dist/testing/conformance.d.cts +2 -2
- package/dist/testing/conformance.d.ts +2 -2
- package/dist/{traversal-CXqN780S.d.cts → traversal-Bmv4we_g.d.cts} +1 -1
- package/dist/{traversal-DpSdLzxn.d.ts → traversal-D8SGKq0z.d.ts} +1 -1
- package/dist/types/index.d.cts +3 -3
- package/dist/types/index.d.ts +3 -3
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -281,6 +281,270 @@ function generateRelationshipId() {
|
|
|
281
281
|
return generateId();
|
|
282
282
|
}
|
|
283
283
|
|
|
284
|
+
// src/vocabulary/similarity.ts
|
|
285
|
+
function jaroSimilarity(a, b) {
|
|
286
|
+
if (a === b) return 1;
|
|
287
|
+
if (a.length === 0 || b.length === 0) return 0;
|
|
288
|
+
const matchWindow = Math.max(Math.floor(Math.max(a.length, b.length) / 2) - 1, 0);
|
|
289
|
+
const aMatches = new Array(a.length).fill(false);
|
|
290
|
+
const bMatches = new Array(b.length).fill(false);
|
|
291
|
+
let matches = 0;
|
|
292
|
+
let transpositions = 0;
|
|
293
|
+
for (let i = 0; i < a.length; i++) {
|
|
294
|
+
const start = Math.max(0, i - matchWindow);
|
|
295
|
+
const end = Math.min(i + matchWindow + 1, b.length);
|
|
296
|
+
for (let j = start; j < end; j++) {
|
|
297
|
+
if (bMatches[j] || a[i] !== b[j]) continue;
|
|
298
|
+
aMatches[i] = true;
|
|
299
|
+
bMatches[j] = true;
|
|
300
|
+
matches++;
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (matches === 0) return 0;
|
|
305
|
+
let k = 0;
|
|
306
|
+
for (let i = 0; i < a.length; i++) {
|
|
307
|
+
if (!aMatches[i]) continue;
|
|
308
|
+
while (!bMatches[k]) k++;
|
|
309
|
+
if (a[i] !== b[k]) transpositions++;
|
|
310
|
+
k++;
|
|
311
|
+
}
|
|
312
|
+
return (matches / a.length + matches / b.length + (matches - transpositions / 2) / matches) / 3;
|
|
313
|
+
}
|
|
314
|
+
function jaroWinklerSimilarity(a, b, scalingFactor = 0.1) {
|
|
315
|
+
const jaro = jaroSimilarity(a, b);
|
|
316
|
+
if (jaro === 0) return 0;
|
|
317
|
+
const prefixLength = Math.min(4, Math.min(a.length, b.length));
|
|
318
|
+
let commonPrefix = 0;
|
|
319
|
+
for (let i = 0; i < prefixLength; i++) {
|
|
320
|
+
if (a[i] === b[i]) {
|
|
321
|
+
commonPrefix++;
|
|
322
|
+
} else {
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return jaro + commonPrefix * Math.min(scalingFactor, 0.25) * (1 - jaro);
|
|
327
|
+
}
|
|
328
|
+
function normalizeTypeName(name) {
|
|
329
|
+
return name.toLowerCase().trim().replace(/[-\s.]+/g, "_").replace(/[^a-z0-9_]/g, "");
|
|
330
|
+
}
|
|
331
|
+
function toScreamingSnakeCase(name) {
|
|
332
|
+
return name.trim().replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2").replace(/[-\s.]+/g, "_").replace(/[^A-Za-z0-9_]/g, "").replace(/_+/g, "_").replace(/^_+|_+$/g, "").toUpperCase();
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// src/vocabulary/VocabularyValidator.ts
|
|
336
|
+
function ok() {
|
|
337
|
+
return { valid: true, errors: [] };
|
|
338
|
+
}
|
|
339
|
+
function fail(...errors) {
|
|
340
|
+
return { valid: false, errors };
|
|
341
|
+
}
|
|
342
|
+
function merge(...results) {
|
|
343
|
+
const errors = results.flatMap((r) => r.errors);
|
|
344
|
+
return { valid: errors.length === 0, errors };
|
|
345
|
+
}
|
|
346
|
+
function findClosestType(typeName, types) {
|
|
347
|
+
if (types.length === 0) return void 0;
|
|
348
|
+
const lower = typeName.toLowerCase();
|
|
349
|
+
const match = types.find(
|
|
350
|
+
(t) => t.type.toLowerCase().includes(lower) || lower.includes(t.type.toLowerCase())
|
|
351
|
+
);
|
|
352
|
+
return match?.type;
|
|
353
|
+
}
|
|
354
|
+
function validatePropertySchema(schema) {
|
|
355
|
+
if (schema.embeddable === true && schema.type !== "string") {
|
|
356
|
+
return fail({
|
|
357
|
+
field: `properties.${schema.name}`,
|
|
358
|
+
message: `Property "${schema.name}" cannot be embeddable \u2014 only string properties support embedding (type is "${schema.type}")`
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
return ok();
|
|
362
|
+
}
|
|
363
|
+
function validatePropertyValue(name, value, schema) {
|
|
364
|
+
if (value === void 0 || value === null) {
|
|
365
|
+
if (schema.required) {
|
|
366
|
+
return fail({
|
|
367
|
+
field: `properties.${name}`,
|
|
368
|
+
message: `Required property "${name}" is missing`
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
return ok();
|
|
372
|
+
}
|
|
373
|
+
switch (schema.type) {
|
|
374
|
+
case "string":
|
|
375
|
+
if (typeof value !== "string") {
|
|
376
|
+
return fail({
|
|
377
|
+
field: `properties.${name}`,
|
|
378
|
+
message: `Property "${name}" must be a string, got ${typeof value}`
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
break;
|
|
382
|
+
case "number":
|
|
383
|
+
if (typeof value !== "number" || Number.isNaN(value)) {
|
|
384
|
+
return fail({
|
|
385
|
+
field: `properties.${name}`,
|
|
386
|
+
message: `Property "${name}" must be a number, got ${typeof value}`
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
break;
|
|
390
|
+
case "boolean":
|
|
391
|
+
if (typeof value !== "boolean") {
|
|
392
|
+
return fail({
|
|
393
|
+
field: `properties.${name}`,
|
|
394
|
+
message: `Property "${name}" must be a boolean, got ${typeof value}`
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
break;
|
|
398
|
+
case "date":
|
|
399
|
+
if (typeof value !== "string" || Number.isNaN(Date.parse(value))) {
|
|
400
|
+
return fail({
|
|
401
|
+
field: `properties.${name}`,
|
|
402
|
+
message: `Property "${name}" must be a valid ISO date string`
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
break;
|
|
406
|
+
case "enum":
|
|
407
|
+
if (typeof value !== "string") {
|
|
408
|
+
return fail({
|
|
409
|
+
field: `properties.${name}`,
|
|
410
|
+
message: `Property "${name}" must be a string (enum value), got ${typeof value}`
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
if (schema.enumValues && !schema.enumValues.includes(value)) {
|
|
414
|
+
return fail({
|
|
415
|
+
field: `properties.${name}`,
|
|
416
|
+
message: `Property "${name}" must be one of: ${schema.enumValues.join(", ")}`,
|
|
417
|
+
suggestion: `Valid values: ${schema.enumValues.join(", ")}`
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
return ok();
|
|
423
|
+
}
|
|
424
|
+
function validateProperties(properties, schemas) {
|
|
425
|
+
const results = [];
|
|
426
|
+
const providedProperties = properties ?? {};
|
|
427
|
+
for (const schema of schemas) {
|
|
428
|
+
const value = providedProperties[schema.name];
|
|
429
|
+
results.push(validatePropertyValue(schema.name, value, schema));
|
|
430
|
+
}
|
|
431
|
+
if (schemas.length > 0) {
|
|
432
|
+
const knownNames = new Set(schemas.map((s) => s.name));
|
|
433
|
+
for (const key of Object.keys(providedProperties)) {
|
|
434
|
+
if (!knownNames.has(key)) {
|
|
435
|
+
results.push(fail({
|
|
436
|
+
field: `properties.${key}`,
|
|
437
|
+
message: `Unknown property "${key}"`,
|
|
438
|
+
suggestion: `Known properties: ${schemas.map((s) => s.name).join(", ")}`
|
|
439
|
+
}));
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
return merge(...results);
|
|
444
|
+
}
|
|
445
|
+
function validateEntity(input, vocabulary) {
|
|
446
|
+
const entityTypeDef = vocabulary.entityTypes.find((et) => et.type === input.entityType);
|
|
447
|
+
if (!entityTypeDef) {
|
|
448
|
+
const closest = findClosestType(input.entityType, vocabulary.entityTypes);
|
|
449
|
+
return fail({
|
|
450
|
+
field: "entityType",
|
|
451
|
+
message: `Entity type "${input.entityType}" does not exist in the vocabulary`,
|
|
452
|
+
suggestion: closest ? `Did you mean "${closest}"?` : `Available types: ${vocabulary.entityTypes.map((et) => et.type).join(", ") || "none"}`
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
if (!input.label || input.label.trim().length === 0) {
|
|
456
|
+
return fail({
|
|
457
|
+
field: "label",
|
|
458
|
+
message: "Entity label is required and cannot be empty"
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
return validateProperties(input.properties, entityTypeDef.properties);
|
|
462
|
+
}
|
|
463
|
+
function validateEntityUpdate(input, currentEntityTypeDef, vocabulary) {
|
|
464
|
+
if (input.label !== void 0 && input.label.trim().length === 0) {
|
|
465
|
+
return fail({
|
|
466
|
+
field: "label",
|
|
467
|
+
message: "Entity label cannot be empty"
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
let targetTypeDef = currentEntityTypeDef;
|
|
471
|
+
if (input.entityType !== void 0 && input.entityType !== currentEntityTypeDef.type) {
|
|
472
|
+
const newTypeDef = vocabulary.entityTypes.find((et) => et.type === input.entityType);
|
|
473
|
+
if (!newTypeDef) {
|
|
474
|
+
const closest = findClosestType(input.entityType, vocabulary.entityTypes);
|
|
475
|
+
return fail({
|
|
476
|
+
field: "entityType",
|
|
477
|
+
message: `Entity type "${input.entityType}" does not exist in the vocabulary`,
|
|
478
|
+
suggestion: closest ? `Did you mean "${closest}"?` : `Available types: ${vocabulary.entityTypes.map((et) => et.type).join(", ") || "none"}`
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
targetTypeDef = newTypeDef;
|
|
482
|
+
}
|
|
483
|
+
if (input.properties) {
|
|
484
|
+
const results = [];
|
|
485
|
+
const hasDefinedProps = targetTypeDef.properties.length > 0;
|
|
486
|
+
for (const [key, value] of Object.entries(input.properties)) {
|
|
487
|
+
if (value === null) continue;
|
|
488
|
+
const schema = targetTypeDef.properties.find((p) => p.name === key);
|
|
489
|
+
if (!schema) {
|
|
490
|
+
if (hasDefinedProps) {
|
|
491
|
+
results.push(fail({
|
|
492
|
+
field: `properties.${key}`,
|
|
493
|
+
message: `Unknown property "${key}"`,
|
|
494
|
+
suggestion: `Known properties: ${targetTypeDef.properties.map((p) => p.name).join(", ")}`
|
|
495
|
+
}));
|
|
496
|
+
}
|
|
497
|
+
} else {
|
|
498
|
+
results.push(validatePropertyValue(key, value, schema));
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
return merge(...results);
|
|
502
|
+
}
|
|
503
|
+
return ok();
|
|
504
|
+
}
|
|
505
|
+
function validateRelationship(input, vocabulary, sourceEntityType, targetEntityType) {
|
|
506
|
+
const normalizedType = toScreamingSnakeCase(input.relationshipType);
|
|
507
|
+
const relTypeDef = vocabulary.relationshipTypes.find(
|
|
508
|
+
(rt) => rt.type === normalizedType
|
|
509
|
+
);
|
|
510
|
+
if (!relTypeDef) {
|
|
511
|
+
const closest = findClosestType(input.relationshipType, vocabulary.relationshipTypes);
|
|
512
|
+
return fail({
|
|
513
|
+
field: "relationshipType",
|
|
514
|
+
message: `Relationship type "${input.relationshipType}" does not exist in the vocabulary`,
|
|
515
|
+
suggestion: closest ? `Did you mean "${closest}"?` : `Available types: ${vocabulary.relationshipTypes.map((rt) => rt.type).join(", ") || "none"}`
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
const results = [];
|
|
519
|
+
if (relTypeDef.allowedSourceTypes.length > 0 && !relTypeDef.allowedSourceTypes.includes(sourceEntityType)) {
|
|
520
|
+
results.push(
|
|
521
|
+
fail({
|
|
522
|
+
field: "sourceEntityId",
|
|
523
|
+
message: `Entity type "${sourceEntityType}" is not allowed as source for relationship type "${input.relationshipType}"`,
|
|
524
|
+
suggestion: `Allowed source types: ${relTypeDef.allowedSourceTypes.join(", ")}`
|
|
525
|
+
})
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
if (relTypeDef.allowedTargetTypes.length > 0 && !relTypeDef.allowedTargetTypes.includes(targetEntityType)) {
|
|
529
|
+
results.push(
|
|
530
|
+
fail({
|
|
531
|
+
field: "targetEntityId",
|
|
532
|
+
message: `Entity type "${targetEntityType}" is not allowed as target for relationship type "${input.relationshipType}"`,
|
|
533
|
+
suggestion: `Allowed target types: ${relTypeDef.allowedTargetTypes.join(", ")}`
|
|
534
|
+
})
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
if (relTypeDef.properties) {
|
|
538
|
+
results.push(
|
|
539
|
+
validateProperties(input.properties, relTypeDef.properties)
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
return merge(...results);
|
|
543
|
+
}
|
|
544
|
+
function getEntityTypeDef(entityType, vocabulary) {
|
|
545
|
+
return vocabulary.entityTypes.find((et) => et.type === entityType) ?? null;
|
|
546
|
+
}
|
|
547
|
+
|
|
284
548
|
// src/entities/EntityManager.ts
|
|
285
549
|
var EntityManager = class {
|
|
286
550
|
constructor(repositoryId, vocabularyEngine, provenanceTracker, eventBus, storage, embedding) {
|
|
@@ -330,7 +594,7 @@ var EntityManager = class {
|
|
|
330
594
|
}
|
|
331
595
|
);
|
|
332
596
|
const provenance = this.provenanceTracker.stampCreate();
|
|
333
|
-
const entityEmbedding = await this.generateEmbedding(input.label, input.summary);
|
|
597
|
+
const entityEmbedding = await this.generateEmbedding(input.label, input.summary, input.properties ?? {}, input.entityType);
|
|
334
598
|
const storedEntity = {
|
|
335
599
|
id,
|
|
336
600
|
slug,
|
|
@@ -404,9 +668,10 @@ var EntityManager = class {
|
|
|
404
668
|
}
|
|
405
669
|
);
|
|
406
670
|
}
|
|
407
|
-
const needsReembed = updates.reembed === true || updates.label !== void 0 || updates.summary !== void 0;
|
|
671
|
+
const needsReembed = updates.reembed === true || updates.label !== void 0 || updates.summary !== void 0 || updates.properties !== void 0;
|
|
408
672
|
const nextSummary = updates.summary === void 0 ? existing.summary : updates.summary ?? void 0;
|
|
409
|
-
const
|
|
673
|
+
const nextEntityType = updates.entityType ?? existing.entityType;
|
|
674
|
+
const entityEmbedding = needsReembed ? await this.generateEmbedding(updates.label ?? existing.label, nextSummary, mergedProperties, nextEntityType) : void 0;
|
|
410
675
|
const updated = await this.storage.updateEntity(this.repositoryId, entityId, {
|
|
411
676
|
entityType: typeChanged ? updates.entityType : void 0,
|
|
412
677
|
label: updates.label,
|
|
@@ -520,7 +785,12 @@ var EntityManager = class {
|
|
|
520
785
|
const maxRetries = options?.maxRetries ?? 3;
|
|
521
786
|
const storedMap = await this.storage.getEntities(this.repositoryId, entityIds);
|
|
522
787
|
const entries = Array.from(storedMap.entries());
|
|
523
|
-
const
|
|
788
|
+
const vocab = await this.vocabularyEngine.getVocabulary();
|
|
789
|
+
const texts = entries.map(([, e]) => {
|
|
790
|
+
const typeDef = getEntityTypeDef(e.entityType, vocab);
|
|
791
|
+
const embeddableValues = typeDef ? typeDef.properties.filter((p) => p.embeddable === true && typeof e.properties[p.name] === "string").map((p) => e.properties[p.name]) : [];
|
|
792
|
+
return [e.label, e.summary ?? "", ...embeddableValues].filter(Boolean).join(" ");
|
|
793
|
+
});
|
|
524
794
|
let embeddings;
|
|
525
795
|
let attempt = 0;
|
|
526
796
|
while (true) {
|
|
@@ -622,95 +892,47 @@ var EntityManager = class {
|
|
|
622
892
|
dimensions: this.embedding.dimensions()
|
|
623
893
|
};
|
|
624
894
|
}
|
|
625
|
-
/** Generate an embedding vector from label + summary if a provider is available */
|
|
626
|
-
async generateEmbedding(label, summary) {
|
|
627
|
-
if (!this.embedding) return void 0;
|
|
628
|
-
const
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
return {
|
|
634
|
-
id: stored.id,
|
|
635
|
-
slug: stored.slug,
|
|
636
|
-
entityType: stored.entityType,
|
|
637
|
-
label: stored.label,
|
|
638
|
-
summary: stored.summary,
|
|
639
|
-
properties: stored.properties,
|
|
640
|
-
data: stored.data,
|
|
641
|
-
dataFormat: stored.dataFormat,
|
|
642
|
-
provenance: stored.provenance
|
|
643
|
-
};
|
|
644
|
-
}
|
|
645
|
-
function storedToSummary(stored) {
|
|
646
|
-
return {
|
|
647
|
-
id: stored.id,
|
|
648
|
-
slug: stored.slug,
|
|
649
|
-
entityType: stored.entityType,
|
|
650
|
-
label: stored.label,
|
|
651
|
-
summary: stored.summary,
|
|
652
|
-
properties: stored.properties
|
|
653
|
-
};
|
|
654
|
-
}
|
|
655
|
-
function storedToBrief(stored) {
|
|
656
|
-
return {
|
|
657
|
-
id: stored.id,
|
|
658
|
-
slug: stored.slug,
|
|
659
|
-
entityType: stored.entityType,
|
|
660
|
-
label: stored.label,
|
|
661
|
-
summary: stored.summary
|
|
662
|
-
};
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
// src/vocabulary/similarity.ts
|
|
666
|
-
function jaroSimilarity(a, b) {
|
|
667
|
-
if (a === b) return 1;
|
|
668
|
-
if (a.length === 0 || b.length === 0) return 0;
|
|
669
|
-
const matchWindow = Math.max(Math.floor(Math.max(a.length, b.length) / 2) - 1, 0);
|
|
670
|
-
const aMatches = new Array(a.length).fill(false);
|
|
671
|
-
const bMatches = new Array(b.length).fill(false);
|
|
672
|
-
let matches = 0;
|
|
673
|
-
let transpositions = 0;
|
|
674
|
-
for (let i = 0; i < a.length; i++) {
|
|
675
|
-
const start = Math.max(0, i - matchWindow);
|
|
676
|
-
const end = Math.min(i + matchWindow + 1, b.length);
|
|
677
|
-
for (let j = start; j < end; j++) {
|
|
678
|
-
if (bMatches[j] || a[i] !== b[j]) continue;
|
|
679
|
-
aMatches[i] = true;
|
|
680
|
-
bMatches[j] = true;
|
|
681
|
-
matches++;
|
|
682
|
-
break;
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
if (matches === 0) return 0;
|
|
686
|
-
let k = 0;
|
|
687
|
-
for (let i = 0; i < a.length; i++) {
|
|
688
|
-
if (!aMatches[i]) continue;
|
|
689
|
-
while (!bMatches[k]) k++;
|
|
690
|
-
if (a[i] !== b[k]) transpositions++;
|
|
691
|
-
k++;
|
|
692
|
-
}
|
|
693
|
-
return (matches / a.length + matches / b.length + (matches - transpositions / 2) / matches) / 3;
|
|
694
|
-
}
|
|
695
|
-
function jaroWinklerSimilarity(a, b, scalingFactor = 0.1) {
|
|
696
|
-
const jaro = jaroSimilarity(a, b);
|
|
697
|
-
if (jaro === 0) return 0;
|
|
698
|
-
const prefixLength = Math.min(4, Math.min(a.length, b.length));
|
|
699
|
-
let commonPrefix = 0;
|
|
700
|
-
for (let i = 0; i < prefixLength; i++) {
|
|
701
|
-
if (a[i] === b[i]) {
|
|
702
|
-
commonPrefix++;
|
|
703
|
-
} else {
|
|
704
|
-
break;
|
|
705
|
-
}
|
|
895
|
+
/** Generate an embedding vector from label + summary + embeddable string properties if a provider is available */
|
|
896
|
+
async generateEmbedding(label, summary, properties, entityType) {
|
|
897
|
+
if (!this.embedding) return void 0;
|
|
898
|
+
const vocab = await this.vocabularyEngine.getVocabulary();
|
|
899
|
+
const typeDef = getEntityTypeDef(entityType, vocab);
|
|
900
|
+
const embeddableValues = typeDef ? typeDef.properties.filter((p) => p.embeddable === true && typeof properties[p.name] === "string").map((p) => properties[p.name]) : [];
|
|
901
|
+
const text = [label, summary ?? "", ...embeddableValues].filter(Boolean).join(" ");
|
|
902
|
+
return this.embedding.embed(text);
|
|
706
903
|
}
|
|
707
|
-
|
|
904
|
+
};
|
|
905
|
+
function storedToEntity(stored) {
|
|
906
|
+
return {
|
|
907
|
+
id: stored.id,
|
|
908
|
+
slug: stored.slug,
|
|
909
|
+
entityType: stored.entityType,
|
|
910
|
+
label: stored.label,
|
|
911
|
+
summary: stored.summary,
|
|
912
|
+
properties: stored.properties,
|
|
913
|
+
data: stored.data,
|
|
914
|
+
dataFormat: stored.dataFormat,
|
|
915
|
+
provenance: stored.provenance
|
|
916
|
+
};
|
|
708
917
|
}
|
|
709
|
-
function
|
|
710
|
-
return
|
|
918
|
+
function storedToSummary(stored) {
|
|
919
|
+
return {
|
|
920
|
+
id: stored.id,
|
|
921
|
+
slug: stored.slug,
|
|
922
|
+
entityType: stored.entityType,
|
|
923
|
+
label: stored.label,
|
|
924
|
+
summary: stored.summary,
|
|
925
|
+
properties: stored.properties
|
|
926
|
+
};
|
|
711
927
|
}
|
|
712
|
-
function
|
|
713
|
-
return
|
|
928
|
+
function storedToBrief(stored) {
|
|
929
|
+
return {
|
|
930
|
+
id: stored.id,
|
|
931
|
+
slug: stored.slug,
|
|
932
|
+
entityType: stored.entityType,
|
|
933
|
+
label: stored.label,
|
|
934
|
+
summary: stored.summary
|
|
935
|
+
};
|
|
714
936
|
}
|
|
715
937
|
|
|
716
938
|
// src/relationships/RelationshipManager.ts
|
|
@@ -1537,6 +1759,7 @@ var SearchOrchestrator = class {
|
|
|
1537
1759
|
search;
|
|
1538
1760
|
embedding;
|
|
1539
1761
|
eventBus;
|
|
1762
|
+
vocabularyEngine;
|
|
1540
1763
|
defaultSimilarityThreshold;
|
|
1541
1764
|
conceptSearchScanLimit;
|
|
1542
1765
|
constructor(config) {
|
|
@@ -1545,6 +1768,7 @@ var SearchOrchestrator = class {
|
|
|
1545
1768
|
this.search = config.search;
|
|
1546
1769
|
this.embedding = config.embedding;
|
|
1547
1770
|
this.eventBus = config.eventBus;
|
|
1771
|
+
this.vocabularyEngine = config.vocabularyEngine;
|
|
1548
1772
|
this.defaultSimilarityThreshold = config.defaultSimilarityThreshold ?? 0.5;
|
|
1549
1773
|
this.conceptSearchScanLimit = config.conceptSearchScanLimit ?? 1e3;
|
|
1550
1774
|
}
|
|
@@ -1619,7 +1843,7 @@ var SearchOrchestrator = class {
|
|
|
1619
1843
|
if (entity.embedding && entity.embedding.length > 0) {
|
|
1620
1844
|
score = this.cosineSimilarity(queryEmbedding, entity.embedding);
|
|
1621
1845
|
} else {
|
|
1622
|
-
const entityText =
|
|
1846
|
+
const entityText = await this.buildEmbeddingText(entity.entityType, entity.label, entity.summary, entity.properties);
|
|
1623
1847
|
const entityEmbedding = await this.embedding.embed(entityText);
|
|
1624
1848
|
score = this.cosineSimilarity(queryEmbedding, entityEmbedding);
|
|
1625
1849
|
}
|
|
@@ -1678,6 +1902,22 @@ var SearchOrchestrator = class {
|
|
|
1678
1902
|
offset
|
|
1679
1903
|
};
|
|
1680
1904
|
}
|
|
1905
|
+
/** Build the embedding text for an entity — label + summary + embeddable string properties */
|
|
1906
|
+
async buildEmbeddingText(entityType, label, summary, properties) {
|
|
1907
|
+
const embeddableValues = [];
|
|
1908
|
+
if (this.vocabularyEngine) {
|
|
1909
|
+
const vocab = await this.vocabularyEngine.getVocabulary();
|
|
1910
|
+
const typeDef = getEntityTypeDef(entityType, vocab);
|
|
1911
|
+
if (typeDef) {
|
|
1912
|
+
for (const p of typeDef.properties) {
|
|
1913
|
+
if (p.embeddable === true && typeof properties[p.name] === "string") {
|
|
1914
|
+
embeddableValues.push(properties[p.name]);
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
return [label, summary ?? "", ...embeddableValues].filter(Boolean).join(" ");
|
|
1920
|
+
}
|
|
1681
1921
|
/** Cosine similarity between two vectors */
|
|
1682
1922
|
cosineSimilarity(a, b) {
|
|
1683
1923
|
if (this.embedding?.similarity) {
|
|
@@ -1704,210 +1944,6 @@ var SearchOrchestrator = class {
|
|
|
1704
1944
|
}
|
|
1705
1945
|
};
|
|
1706
1946
|
|
|
1707
|
-
// src/vocabulary/VocabularyValidator.ts
|
|
1708
|
-
function ok() {
|
|
1709
|
-
return { valid: true, errors: [] };
|
|
1710
|
-
}
|
|
1711
|
-
function fail(...errors) {
|
|
1712
|
-
return { valid: false, errors };
|
|
1713
|
-
}
|
|
1714
|
-
function merge(...results) {
|
|
1715
|
-
const errors = results.flatMap((r) => r.errors);
|
|
1716
|
-
return { valid: errors.length === 0, errors };
|
|
1717
|
-
}
|
|
1718
|
-
function findClosestType(typeName, types) {
|
|
1719
|
-
if (types.length === 0) return void 0;
|
|
1720
|
-
const lower = typeName.toLowerCase();
|
|
1721
|
-
const match = types.find(
|
|
1722
|
-
(t) => t.type.toLowerCase().includes(lower) || lower.includes(t.type.toLowerCase())
|
|
1723
|
-
);
|
|
1724
|
-
return match?.type;
|
|
1725
|
-
}
|
|
1726
|
-
function validatePropertyValue(name, value, schema) {
|
|
1727
|
-
if (value === void 0 || value === null) {
|
|
1728
|
-
if (schema.required) {
|
|
1729
|
-
return fail({
|
|
1730
|
-
field: `properties.${name}`,
|
|
1731
|
-
message: `Required property "${name}" is missing`
|
|
1732
|
-
});
|
|
1733
|
-
}
|
|
1734
|
-
return ok();
|
|
1735
|
-
}
|
|
1736
|
-
switch (schema.type) {
|
|
1737
|
-
case "string":
|
|
1738
|
-
if (typeof value !== "string") {
|
|
1739
|
-
return fail({
|
|
1740
|
-
field: `properties.${name}`,
|
|
1741
|
-
message: `Property "${name}" must be a string, got ${typeof value}`
|
|
1742
|
-
});
|
|
1743
|
-
}
|
|
1744
|
-
break;
|
|
1745
|
-
case "number":
|
|
1746
|
-
if (typeof value !== "number" || Number.isNaN(value)) {
|
|
1747
|
-
return fail({
|
|
1748
|
-
field: `properties.${name}`,
|
|
1749
|
-
message: `Property "${name}" must be a number, got ${typeof value}`
|
|
1750
|
-
});
|
|
1751
|
-
}
|
|
1752
|
-
break;
|
|
1753
|
-
case "boolean":
|
|
1754
|
-
if (typeof value !== "boolean") {
|
|
1755
|
-
return fail({
|
|
1756
|
-
field: `properties.${name}`,
|
|
1757
|
-
message: `Property "${name}" must be a boolean, got ${typeof value}`
|
|
1758
|
-
});
|
|
1759
|
-
}
|
|
1760
|
-
break;
|
|
1761
|
-
case "date":
|
|
1762
|
-
if (typeof value !== "string" || Number.isNaN(Date.parse(value))) {
|
|
1763
|
-
return fail({
|
|
1764
|
-
field: `properties.${name}`,
|
|
1765
|
-
message: `Property "${name}" must be a valid ISO date string`
|
|
1766
|
-
});
|
|
1767
|
-
}
|
|
1768
|
-
break;
|
|
1769
|
-
case "enum":
|
|
1770
|
-
if (typeof value !== "string") {
|
|
1771
|
-
return fail({
|
|
1772
|
-
field: `properties.${name}`,
|
|
1773
|
-
message: `Property "${name}" must be a string (enum value), got ${typeof value}`
|
|
1774
|
-
});
|
|
1775
|
-
}
|
|
1776
|
-
if (schema.enumValues && !schema.enumValues.includes(value)) {
|
|
1777
|
-
return fail({
|
|
1778
|
-
field: `properties.${name}`,
|
|
1779
|
-
message: `Property "${name}" must be one of: ${schema.enumValues.join(", ")}`,
|
|
1780
|
-
suggestion: `Valid values: ${schema.enumValues.join(", ")}`
|
|
1781
|
-
});
|
|
1782
|
-
}
|
|
1783
|
-
break;
|
|
1784
|
-
}
|
|
1785
|
-
return ok();
|
|
1786
|
-
}
|
|
1787
|
-
function validateProperties(properties, schemas) {
|
|
1788
|
-
const results = [];
|
|
1789
|
-
const providedProperties = properties ?? {};
|
|
1790
|
-
for (const schema of schemas) {
|
|
1791
|
-
const value = providedProperties[schema.name];
|
|
1792
|
-
results.push(validatePropertyValue(schema.name, value, schema));
|
|
1793
|
-
}
|
|
1794
|
-
if (schemas.length > 0) {
|
|
1795
|
-
const knownNames = new Set(schemas.map((s) => s.name));
|
|
1796
|
-
for (const key of Object.keys(providedProperties)) {
|
|
1797
|
-
if (!knownNames.has(key)) {
|
|
1798
|
-
results.push(fail({
|
|
1799
|
-
field: `properties.${key}`,
|
|
1800
|
-
message: `Unknown property "${key}"`,
|
|
1801
|
-
suggestion: `Known properties: ${schemas.map((s) => s.name).join(", ")}`
|
|
1802
|
-
}));
|
|
1803
|
-
}
|
|
1804
|
-
}
|
|
1805
|
-
}
|
|
1806
|
-
return merge(...results);
|
|
1807
|
-
}
|
|
1808
|
-
function validateEntity(input, vocabulary) {
|
|
1809
|
-
const entityTypeDef = vocabulary.entityTypes.find((et) => et.type === input.entityType);
|
|
1810
|
-
if (!entityTypeDef) {
|
|
1811
|
-
const closest = findClosestType(input.entityType, vocabulary.entityTypes);
|
|
1812
|
-
return fail({
|
|
1813
|
-
field: "entityType",
|
|
1814
|
-
message: `Entity type "${input.entityType}" does not exist in the vocabulary`,
|
|
1815
|
-
suggestion: closest ? `Did you mean "${closest}"?` : `Available types: ${vocabulary.entityTypes.map((et) => et.type).join(", ") || "none"}`
|
|
1816
|
-
});
|
|
1817
|
-
}
|
|
1818
|
-
if (!input.label || input.label.trim().length === 0) {
|
|
1819
|
-
return fail({
|
|
1820
|
-
field: "label",
|
|
1821
|
-
message: "Entity label is required and cannot be empty"
|
|
1822
|
-
});
|
|
1823
|
-
}
|
|
1824
|
-
return validateProperties(input.properties, entityTypeDef.properties);
|
|
1825
|
-
}
|
|
1826
|
-
function validateEntityUpdate(input, currentEntityTypeDef, vocabulary) {
|
|
1827
|
-
if (input.label !== void 0 && input.label.trim().length === 0) {
|
|
1828
|
-
return fail({
|
|
1829
|
-
field: "label",
|
|
1830
|
-
message: "Entity label cannot be empty"
|
|
1831
|
-
});
|
|
1832
|
-
}
|
|
1833
|
-
let targetTypeDef = currentEntityTypeDef;
|
|
1834
|
-
if (input.entityType !== void 0 && input.entityType !== currentEntityTypeDef.type) {
|
|
1835
|
-
const newTypeDef = vocabulary.entityTypes.find((et) => et.type === input.entityType);
|
|
1836
|
-
if (!newTypeDef) {
|
|
1837
|
-
const closest = findClosestType(input.entityType, vocabulary.entityTypes);
|
|
1838
|
-
return fail({
|
|
1839
|
-
field: "entityType",
|
|
1840
|
-
message: `Entity type "${input.entityType}" does not exist in the vocabulary`,
|
|
1841
|
-
suggestion: closest ? `Did you mean "${closest}"?` : `Available types: ${vocabulary.entityTypes.map((et) => et.type).join(", ") || "none"}`
|
|
1842
|
-
});
|
|
1843
|
-
}
|
|
1844
|
-
targetTypeDef = newTypeDef;
|
|
1845
|
-
}
|
|
1846
|
-
if (input.properties) {
|
|
1847
|
-
const results = [];
|
|
1848
|
-
const hasDefinedProps = targetTypeDef.properties.length > 0;
|
|
1849
|
-
for (const [key, value] of Object.entries(input.properties)) {
|
|
1850
|
-
if (value === null) continue;
|
|
1851
|
-
const schema = targetTypeDef.properties.find((p) => p.name === key);
|
|
1852
|
-
if (!schema) {
|
|
1853
|
-
if (hasDefinedProps) {
|
|
1854
|
-
results.push(fail({
|
|
1855
|
-
field: `properties.${key}`,
|
|
1856
|
-
message: `Unknown property "${key}"`,
|
|
1857
|
-
suggestion: `Known properties: ${targetTypeDef.properties.map((p) => p.name).join(", ")}`
|
|
1858
|
-
}));
|
|
1859
|
-
}
|
|
1860
|
-
} else {
|
|
1861
|
-
results.push(validatePropertyValue(key, value, schema));
|
|
1862
|
-
}
|
|
1863
|
-
}
|
|
1864
|
-
return merge(...results);
|
|
1865
|
-
}
|
|
1866
|
-
return ok();
|
|
1867
|
-
}
|
|
1868
|
-
function validateRelationship(input, vocabulary, sourceEntityType, targetEntityType) {
|
|
1869
|
-
const normalizedType = toScreamingSnakeCase(input.relationshipType);
|
|
1870
|
-
const relTypeDef = vocabulary.relationshipTypes.find(
|
|
1871
|
-
(rt) => rt.type === normalizedType
|
|
1872
|
-
);
|
|
1873
|
-
if (!relTypeDef) {
|
|
1874
|
-
const closest = findClosestType(input.relationshipType, vocabulary.relationshipTypes);
|
|
1875
|
-
return fail({
|
|
1876
|
-
field: "relationshipType",
|
|
1877
|
-
message: `Relationship type "${input.relationshipType}" does not exist in the vocabulary`,
|
|
1878
|
-
suggestion: closest ? `Did you mean "${closest}"?` : `Available types: ${vocabulary.relationshipTypes.map((rt) => rt.type).join(", ") || "none"}`
|
|
1879
|
-
});
|
|
1880
|
-
}
|
|
1881
|
-
const results = [];
|
|
1882
|
-
if (relTypeDef.allowedSourceTypes.length > 0 && !relTypeDef.allowedSourceTypes.includes(sourceEntityType)) {
|
|
1883
|
-
results.push(
|
|
1884
|
-
fail({
|
|
1885
|
-
field: "sourceEntityId",
|
|
1886
|
-
message: `Entity type "${sourceEntityType}" is not allowed as source for relationship type "${input.relationshipType}"`,
|
|
1887
|
-
suggestion: `Allowed source types: ${relTypeDef.allowedSourceTypes.join(", ")}`
|
|
1888
|
-
})
|
|
1889
|
-
);
|
|
1890
|
-
}
|
|
1891
|
-
if (relTypeDef.allowedTargetTypes.length > 0 && !relTypeDef.allowedTargetTypes.includes(targetEntityType)) {
|
|
1892
|
-
results.push(
|
|
1893
|
-
fail({
|
|
1894
|
-
field: "targetEntityId",
|
|
1895
|
-
message: `Entity type "${targetEntityType}" is not allowed as target for relationship type "${input.relationshipType}"`,
|
|
1896
|
-
suggestion: `Allowed target types: ${relTypeDef.allowedTargetTypes.join(", ")}`
|
|
1897
|
-
})
|
|
1898
|
-
);
|
|
1899
|
-
}
|
|
1900
|
-
if (relTypeDef.properties) {
|
|
1901
|
-
results.push(
|
|
1902
|
-
validateProperties(input.properties, relTypeDef.properties)
|
|
1903
|
-
);
|
|
1904
|
-
}
|
|
1905
|
-
return merge(...results);
|
|
1906
|
-
}
|
|
1907
|
-
function getEntityTypeDef(entityType, vocabulary) {
|
|
1908
|
-
return vocabulary.entityTypes.find((et) => et.type === entityType) ?? null;
|
|
1909
|
-
}
|
|
1910
|
-
|
|
1911
1947
|
// src/validation/RepositoryValidator.ts
|
|
1912
1948
|
var DEFAULT_TAKE = 200;
|
|
1913
1949
|
var RepositoryValidator = class {
|
|
@@ -2129,6 +2165,7 @@ var MemoryRepository = class {
|
|
|
2129
2165
|
search: config.search,
|
|
2130
2166
|
embedding: config.embedding,
|
|
2131
2167
|
eventBus: config.eventBus,
|
|
2168
|
+
vocabularyEngine: config.vocabularyEngine,
|
|
2132
2169
|
defaultSimilarityThreshold: config.vocabularyEngine.getGovernanceConfig().defaultSimilarityThreshold
|
|
2133
2170
|
});
|
|
2134
2171
|
}
|
|
@@ -3172,6 +3209,10 @@ var VocabularyEngine = class {
|
|
|
3172
3209
|
}
|
|
3173
3210
|
}
|
|
3174
3211
|
}
|
|
3212
|
+
const propertySchemaErrors = this.validateProposalPropertySchemas(proposal);
|
|
3213
|
+
if (propertySchemaErrors) {
|
|
3214
|
+
return propertySchemaErrors;
|
|
3215
|
+
}
|
|
3175
3216
|
const { result, updatedVocabulary } = processProposal(
|
|
3176
3217
|
vocabulary,
|
|
3177
3218
|
proposal,
|
|
@@ -3209,6 +3250,37 @@ var VocabularyEngine = class {
|
|
|
3209
3250
|
}
|
|
3210
3251
|
return { deletedEntities: 0, deletedRelationships: 0 };
|
|
3211
3252
|
}
|
|
3253
|
+
/** Validate all property schemas in a proposal — returns a rejected result on the first invalid schema, or undefined if all pass */
|
|
3254
|
+
validateProposalPropertySchemas(proposal) {
|
|
3255
|
+
const schemas = [];
|
|
3256
|
+
let typeName = "";
|
|
3257
|
+
if (proposal.proposalType === "entity_type" && proposal.entityType) {
|
|
3258
|
+
schemas.push(...proposal.entityType.properties ?? []);
|
|
3259
|
+
typeName = proposal.entityType.type;
|
|
3260
|
+
} else if (proposal.proposalType === "relationship_type" && proposal.relationshipType) {
|
|
3261
|
+
schemas.push(...proposal.relationshipType.properties ?? []);
|
|
3262
|
+
typeName = proposal.relationshipType.type;
|
|
3263
|
+
} else if (proposal.proposalType === "edit_entity_type" && proposal.editEntityType) {
|
|
3264
|
+
schemas.push(...proposal.editEntityType.addProperties ?? []);
|
|
3265
|
+
schemas.push(...proposal.editEntityType.updateProperties ?? []);
|
|
3266
|
+
typeName = proposal.editEntityType.type;
|
|
3267
|
+
} else if (proposal.proposalType === "edit_relationship_type" && proposal.editRelationshipType) {
|
|
3268
|
+
schemas.push(...proposal.editRelationshipType.addProperties ?? []);
|
|
3269
|
+
schemas.push(...proposal.editRelationshipType.updateProperties ?? []);
|
|
3270
|
+
typeName = proposal.editRelationshipType.type;
|
|
3271
|
+
}
|
|
3272
|
+
for (const schema of schemas) {
|
|
3273
|
+
const result = validatePropertySchema(schema);
|
|
3274
|
+
if (!result.valid) {
|
|
3275
|
+
return {
|
|
3276
|
+
status: "rejected",
|
|
3277
|
+
type: typeName,
|
|
3278
|
+
reason: result.errors.map((e) => e.message).join("; ")
|
|
3279
|
+
};
|
|
3280
|
+
}
|
|
3281
|
+
}
|
|
3282
|
+
return void 0;
|
|
3283
|
+
}
|
|
3212
3284
|
getExistingTypesForProposal(proposal, vocabulary) {
|
|
3213
3285
|
if (proposal.proposalType === "entity_type") {
|
|
3214
3286
|
return vocabulary.entityTypes.map((et) => ({
|
|
@@ -3359,7 +3431,7 @@ var EventBus = class {
|
|
|
3359
3431
|
};
|
|
3360
3432
|
|
|
3361
3433
|
// src/portability/RepositoryExporter.ts
|
|
3362
|
-
var LIBRARY_VERSION = true ? "0.
|
|
3434
|
+
var LIBRARY_VERSION = true ? "0.8.0" : "0.1.0";
|
|
3363
3435
|
var RepositoryExporter = class {
|
|
3364
3436
|
storage;
|
|
3365
3437
|
provenance;
|