@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.cjs
CHANGED
|
@@ -339,6 +339,270 @@ function generateRelationshipId() {
|
|
|
339
339
|
return generateId();
|
|
340
340
|
}
|
|
341
341
|
|
|
342
|
+
// src/vocabulary/similarity.ts
|
|
343
|
+
function jaroSimilarity(a, b) {
|
|
344
|
+
if (a === b) return 1;
|
|
345
|
+
if (a.length === 0 || b.length === 0) return 0;
|
|
346
|
+
const matchWindow = Math.max(Math.floor(Math.max(a.length, b.length) / 2) - 1, 0);
|
|
347
|
+
const aMatches = new Array(a.length).fill(false);
|
|
348
|
+
const bMatches = new Array(b.length).fill(false);
|
|
349
|
+
let matches = 0;
|
|
350
|
+
let transpositions = 0;
|
|
351
|
+
for (let i = 0; i < a.length; i++) {
|
|
352
|
+
const start = Math.max(0, i - matchWindow);
|
|
353
|
+
const end = Math.min(i + matchWindow + 1, b.length);
|
|
354
|
+
for (let j = start; j < end; j++) {
|
|
355
|
+
if (bMatches[j] || a[i] !== b[j]) continue;
|
|
356
|
+
aMatches[i] = true;
|
|
357
|
+
bMatches[j] = true;
|
|
358
|
+
matches++;
|
|
359
|
+
break;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
if (matches === 0) return 0;
|
|
363
|
+
let k = 0;
|
|
364
|
+
for (let i = 0; i < a.length; i++) {
|
|
365
|
+
if (!aMatches[i]) continue;
|
|
366
|
+
while (!bMatches[k]) k++;
|
|
367
|
+
if (a[i] !== b[k]) transpositions++;
|
|
368
|
+
k++;
|
|
369
|
+
}
|
|
370
|
+
return (matches / a.length + matches / b.length + (matches - transpositions / 2) / matches) / 3;
|
|
371
|
+
}
|
|
372
|
+
function jaroWinklerSimilarity(a, b, scalingFactor = 0.1) {
|
|
373
|
+
const jaro = jaroSimilarity(a, b);
|
|
374
|
+
if (jaro === 0) return 0;
|
|
375
|
+
const prefixLength = Math.min(4, Math.min(a.length, b.length));
|
|
376
|
+
let commonPrefix = 0;
|
|
377
|
+
for (let i = 0; i < prefixLength; i++) {
|
|
378
|
+
if (a[i] === b[i]) {
|
|
379
|
+
commonPrefix++;
|
|
380
|
+
} else {
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return jaro + commonPrefix * Math.min(scalingFactor, 0.25) * (1 - jaro);
|
|
385
|
+
}
|
|
386
|
+
function normalizeTypeName(name) {
|
|
387
|
+
return name.toLowerCase().trim().replace(/[-\s.]+/g, "_").replace(/[^a-z0-9_]/g, "");
|
|
388
|
+
}
|
|
389
|
+
function toScreamingSnakeCase(name) {
|
|
390
|
+
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();
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// src/vocabulary/VocabularyValidator.ts
|
|
394
|
+
function ok() {
|
|
395
|
+
return { valid: true, errors: [] };
|
|
396
|
+
}
|
|
397
|
+
function fail(...errors) {
|
|
398
|
+
return { valid: false, errors };
|
|
399
|
+
}
|
|
400
|
+
function merge(...results) {
|
|
401
|
+
const errors = results.flatMap((r) => r.errors);
|
|
402
|
+
return { valid: errors.length === 0, errors };
|
|
403
|
+
}
|
|
404
|
+
function findClosestType(typeName, types) {
|
|
405
|
+
if (types.length === 0) return void 0;
|
|
406
|
+
const lower = typeName.toLowerCase();
|
|
407
|
+
const match = types.find(
|
|
408
|
+
(t) => t.type.toLowerCase().includes(lower) || lower.includes(t.type.toLowerCase())
|
|
409
|
+
);
|
|
410
|
+
return match?.type;
|
|
411
|
+
}
|
|
412
|
+
function validatePropertySchema(schema) {
|
|
413
|
+
if (schema.embeddable === true && schema.type !== "string") {
|
|
414
|
+
return fail({
|
|
415
|
+
field: `properties.${schema.name}`,
|
|
416
|
+
message: `Property "${schema.name}" cannot be embeddable \u2014 only string properties support embedding (type is "${schema.type}")`
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
return ok();
|
|
420
|
+
}
|
|
421
|
+
function validatePropertyValue(name, value, schema) {
|
|
422
|
+
if (value === void 0 || value === null) {
|
|
423
|
+
if (schema.required) {
|
|
424
|
+
return fail({
|
|
425
|
+
field: `properties.${name}`,
|
|
426
|
+
message: `Required property "${name}" is missing`
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
return ok();
|
|
430
|
+
}
|
|
431
|
+
switch (schema.type) {
|
|
432
|
+
case "string":
|
|
433
|
+
if (typeof value !== "string") {
|
|
434
|
+
return fail({
|
|
435
|
+
field: `properties.${name}`,
|
|
436
|
+
message: `Property "${name}" must be a string, got ${typeof value}`
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
break;
|
|
440
|
+
case "number":
|
|
441
|
+
if (typeof value !== "number" || Number.isNaN(value)) {
|
|
442
|
+
return fail({
|
|
443
|
+
field: `properties.${name}`,
|
|
444
|
+
message: `Property "${name}" must be a number, got ${typeof value}`
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
break;
|
|
448
|
+
case "boolean":
|
|
449
|
+
if (typeof value !== "boolean") {
|
|
450
|
+
return fail({
|
|
451
|
+
field: `properties.${name}`,
|
|
452
|
+
message: `Property "${name}" must be a boolean, got ${typeof value}`
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
break;
|
|
456
|
+
case "date":
|
|
457
|
+
if (typeof value !== "string" || Number.isNaN(Date.parse(value))) {
|
|
458
|
+
return fail({
|
|
459
|
+
field: `properties.${name}`,
|
|
460
|
+
message: `Property "${name}" must be a valid ISO date string`
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
break;
|
|
464
|
+
case "enum":
|
|
465
|
+
if (typeof value !== "string") {
|
|
466
|
+
return fail({
|
|
467
|
+
field: `properties.${name}`,
|
|
468
|
+
message: `Property "${name}" must be a string (enum value), got ${typeof value}`
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
if (schema.enumValues && !schema.enumValues.includes(value)) {
|
|
472
|
+
return fail({
|
|
473
|
+
field: `properties.${name}`,
|
|
474
|
+
message: `Property "${name}" must be one of: ${schema.enumValues.join(", ")}`,
|
|
475
|
+
suggestion: `Valid values: ${schema.enumValues.join(", ")}`
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
break;
|
|
479
|
+
}
|
|
480
|
+
return ok();
|
|
481
|
+
}
|
|
482
|
+
function validateProperties(properties, schemas) {
|
|
483
|
+
const results = [];
|
|
484
|
+
const providedProperties = properties ?? {};
|
|
485
|
+
for (const schema of schemas) {
|
|
486
|
+
const value = providedProperties[schema.name];
|
|
487
|
+
results.push(validatePropertyValue(schema.name, value, schema));
|
|
488
|
+
}
|
|
489
|
+
if (schemas.length > 0) {
|
|
490
|
+
const knownNames = new Set(schemas.map((s) => s.name));
|
|
491
|
+
for (const key of Object.keys(providedProperties)) {
|
|
492
|
+
if (!knownNames.has(key)) {
|
|
493
|
+
results.push(fail({
|
|
494
|
+
field: `properties.${key}`,
|
|
495
|
+
message: `Unknown property "${key}"`,
|
|
496
|
+
suggestion: `Known properties: ${schemas.map((s) => s.name).join(", ")}`
|
|
497
|
+
}));
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
return merge(...results);
|
|
502
|
+
}
|
|
503
|
+
function validateEntity(input, vocabulary) {
|
|
504
|
+
const entityTypeDef = vocabulary.entityTypes.find((et) => et.type === input.entityType);
|
|
505
|
+
if (!entityTypeDef) {
|
|
506
|
+
const closest = findClosestType(input.entityType, vocabulary.entityTypes);
|
|
507
|
+
return fail({
|
|
508
|
+
field: "entityType",
|
|
509
|
+
message: `Entity type "${input.entityType}" does not exist in the vocabulary`,
|
|
510
|
+
suggestion: closest ? `Did you mean "${closest}"?` : `Available types: ${vocabulary.entityTypes.map((et) => et.type).join(", ") || "none"}`
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
if (!input.label || input.label.trim().length === 0) {
|
|
514
|
+
return fail({
|
|
515
|
+
field: "label",
|
|
516
|
+
message: "Entity label is required and cannot be empty"
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
return validateProperties(input.properties, entityTypeDef.properties);
|
|
520
|
+
}
|
|
521
|
+
function validateEntityUpdate(input, currentEntityTypeDef, vocabulary) {
|
|
522
|
+
if (input.label !== void 0 && input.label.trim().length === 0) {
|
|
523
|
+
return fail({
|
|
524
|
+
field: "label",
|
|
525
|
+
message: "Entity label cannot be empty"
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
let targetTypeDef = currentEntityTypeDef;
|
|
529
|
+
if (input.entityType !== void 0 && input.entityType !== currentEntityTypeDef.type) {
|
|
530
|
+
const newTypeDef = vocabulary.entityTypes.find((et) => et.type === input.entityType);
|
|
531
|
+
if (!newTypeDef) {
|
|
532
|
+
const closest = findClosestType(input.entityType, vocabulary.entityTypes);
|
|
533
|
+
return fail({
|
|
534
|
+
field: "entityType",
|
|
535
|
+
message: `Entity type "${input.entityType}" does not exist in the vocabulary`,
|
|
536
|
+
suggestion: closest ? `Did you mean "${closest}"?` : `Available types: ${vocabulary.entityTypes.map((et) => et.type).join(", ") || "none"}`
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
targetTypeDef = newTypeDef;
|
|
540
|
+
}
|
|
541
|
+
if (input.properties) {
|
|
542
|
+
const results = [];
|
|
543
|
+
const hasDefinedProps = targetTypeDef.properties.length > 0;
|
|
544
|
+
for (const [key, value] of Object.entries(input.properties)) {
|
|
545
|
+
if (value === null) continue;
|
|
546
|
+
const schema = targetTypeDef.properties.find((p) => p.name === key);
|
|
547
|
+
if (!schema) {
|
|
548
|
+
if (hasDefinedProps) {
|
|
549
|
+
results.push(fail({
|
|
550
|
+
field: `properties.${key}`,
|
|
551
|
+
message: `Unknown property "${key}"`,
|
|
552
|
+
suggestion: `Known properties: ${targetTypeDef.properties.map((p) => p.name).join(", ")}`
|
|
553
|
+
}));
|
|
554
|
+
}
|
|
555
|
+
} else {
|
|
556
|
+
results.push(validatePropertyValue(key, value, schema));
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
return merge(...results);
|
|
560
|
+
}
|
|
561
|
+
return ok();
|
|
562
|
+
}
|
|
563
|
+
function validateRelationship(input, vocabulary, sourceEntityType, targetEntityType) {
|
|
564
|
+
const normalizedType = toScreamingSnakeCase(input.relationshipType);
|
|
565
|
+
const relTypeDef = vocabulary.relationshipTypes.find(
|
|
566
|
+
(rt) => rt.type === normalizedType
|
|
567
|
+
);
|
|
568
|
+
if (!relTypeDef) {
|
|
569
|
+
const closest = findClosestType(input.relationshipType, vocabulary.relationshipTypes);
|
|
570
|
+
return fail({
|
|
571
|
+
field: "relationshipType",
|
|
572
|
+
message: `Relationship type "${input.relationshipType}" does not exist in the vocabulary`,
|
|
573
|
+
suggestion: closest ? `Did you mean "${closest}"?` : `Available types: ${vocabulary.relationshipTypes.map((rt) => rt.type).join(", ") || "none"}`
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
const results = [];
|
|
577
|
+
if (relTypeDef.allowedSourceTypes.length > 0 && !relTypeDef.allowedSourceTypes.includes(sourceEntityType)) {
|
|
578
|
+
results.push(
|
|
579
|
+
fail({
|
|
580
|
+
field: "sourceEntityId",
|
|
581
|
+
message: `Entity type "${sourceEntityType}" is not allowed as source for relationship type "${input.relationshipType}"`,
|
|
582
|
+
suggestion: `Allowed source types: ${relTypeDef.allowedSourceTypes.join(", ")}`
|
|
583
|
+
})
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
if (relTypeDef.allowedTargetTypes.length > 0 && !relTypeDef.allowedTargetTypes.includes(targetEntityType)) {
|
|
587
|
+
results.push(
|
|
588
|
+
fail({
|
|
589
|
+
field: "targetEntityId",
|
|
590
|
+
message: `Entity type "${targetEntityType}" is not allowed as target for relationship type "${input.relationshipType}"`,
|
|
591
|
+
suggestion: `Allowed target types: ${relTypeDef.allowedTargetTypes.join(", ")}`
|
|
592
|
+
})
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
if (relTypeDef.properties) {
|
|
596
|
+
results.push(
|
|
597
|
+
validateProperties(input.properties, relTypeDef.properties)
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
return merge(...results);
|
|
601
|
+
}
|
|
602
|
+
function getEntityTypeDef(entityType, vocabulary) {
|
|
603
|
+
return vocabulary.entityTypes.find((et) => et.type === entityType) ?? null;
|
|
604
|
+
}
|
|
605
|
+
|
|
342
606
|
// src/entities/EntityManager.ts
|
|
343
607
|
var EntityManager = class {
|
|
344
608
|
constructor(repositoryId, vocabularyEngine, provenanceTracker, eventBus, storage, embedding) {
|
|
@@ -388,7 +652,7 @@ var EntityManager = class {
|
|
|
388
652
|
}
|
|
389
653
|
);
|
|
390
654
|
const provenance = this.provenanceTracker.stampCreate();
|
|
391
|
-
const entityEmbedding = await this.generateEmbedding(input.label, input.summary);
|
|
655
|
+
const entityEmbedding = await this.generateEmbedding(input.label, input.summary, input.properties ?? {}, input.entityType);
|
|
392
656
|
const storedEntity = {
|
|
393
657
|
id,
|
|
394
658
|
slug,
|
|
@@ -462,9 +726,10 @@ var EntityManager = class {
|
|
|
462
726
|
}
|
|
463
727
|
);
|
|
464
728
|
}
|
|
465
|
-
const needsReembed = updates.reembed === true || updates.label !== void 0 || updates.summary !== void 0;
|
|
729
|
+
const needsReembed = updates.reembed === true || updates.label !== void 0 || updates.summary !== void 0 || updates.properties !== void 0;
|
|
466
730
|
const nextSummary = updates.summary === void 0 ? existing.summary : updates.summary ?? void 0;
|
|
467
|
-
const
|
|
731
|
+
const nextEntityType = updates.entityType ?? existing.entityType;
|
|
732
|
+
const entityEmbedding = needsReembed ? await this.generateEmbedding(updates.label ?? existing.label, nextSummary, mergedProperties, nextEntityType) : void 0;
|
|
468
733
|
const updated = await this.storage.updateEntity(this.repositoryId, entityId, {
|
|
469
734
|
entityType: typeChanged ? updates.entityType : void 0,
|
|
470
735
|
label: updates.label,
|
|
@@ -578,7 +843,12 @@ var EntityManager = class {
|
|
|
578
843
|
const maxRetries = options?.maxRetries ?? 3;
|
|
579
844
|
const storedMap = await this.storage.getEntities(this.repositoryId, entityIds);
|
|
580
845
|
const entries = Array.from(storedMap.entries());
|
|
581
|
-
const
|
|
846
|
+
const vocab = await this.vocabularyEngine.getVocabulary();
|
|
847
|
+
const texts = entries.map(([, e]) => {
|
|
848
|
+
const typeDef = getEntityTypeDef(e.entityType, vocab);
|
|
849
|
+
const embeddableValues = typeDef ? typeDef.properties.filter((p) => p.embeddable === true && typeof e.properties[p.name] === "string").map((p) => e.properties[p.name]) : [];
|
|
850
|
+
return [e.label, e.summary ?? "", ...embeddableValues].filter(Boolean).join(" ");
|
|
851
|
+
});
|
|
582
852
|
let embeddings;
|
|
583
853
|
let attempt = 0;
|
|
584
854
|
while (true) {
|
|
@@ -680,95 +950,47 @@ var EntityManager = class {
|
|
|
680
950
|
dimensions: this.embedding.dimensions()
|
|
681
951
|
};
|
|
682
952
|
}
|
|
683
|
-
/** Generate an embedding vector from label + summary if a provider is available */
|
|
684
|
-
async generateEmbedding(label, summary) {
|
|
685
|
-
if (!this.embedding) return void 0;
|
|
686
|
-
const
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
return {
|
|
692
|
-
id: stored.id,
|
|
693
|
-
slug: stored.slug,
|
|
694
|
-
entityType: stored.entityType,
|
|
695
|
-
label: stored.label,
|
|
696
|
-
summary: stored.summary,
|
|
697
|
-
properties: stored.properties,
|
|
698
|
-
data: stored.data,
|
|
699
|
-
dataFormat: stored.dataFormat,
|
|
700
|
-
provenance: stored.provenance
|
|
701
|
-
};
|
|
702
|
-
}
|
|
703
|
-
function storedToSummary(stored) {
|
|
704
|
-
return {
|
|
705
|
-
id: stored.id,
|
|
706
|
-
slug: stored.slug,
|
|
707
|
-
entityType: stored.entityType,
|
|
708
|
-
label: stored.label,
|
|
709
|
-
summary: stored.summary,
|
|
710
|
-
properties: stored.properties
|
|
711
|
-
};
|
|
712
|
-
}
|
|
713
|
-
function storedToBrief(stored) {
|
|
714
|
-
return {
|
|
715
|
-
id: stored.id,
|
|
716
|
-
slug: stored.slug,
|
|
717
|
-
entityType: stored.entityType,
|
|
718
|
-
label: stored.label,
|
|
719
|
-
summary: stored.summary
|
|
720
|
-
};
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
// src/vocabulary/similarity.ts
|
|
724
|
-
function jaroSimilarity(a, b) {
|
|
725
|
-
if (a === b) return 1;
|
|
726
|
-
if (a.length === 0 || b.length === 0) return 0;
|
|
727
|
-
const matchWindow = Math.max(Math.floor(Math.max(a.length, b.length) / 2) - 1, 0);
|
|
728
|
-
const aMatches = new Array(a.length).fill(false);
|
|
729
|
-
const bMatches = new Array(b.length).fill(false);
|
|
730
|
-
let matches = 0;
|
|
731
|
-
let transpositions = 0;
|
|
732
|
-
for (let i = 0; i < a.length; i++) {
|
|
733
|
-
const start = Math.max(0, i - matchWindow);
|
|
734
|
-
const end = Math.min(i + matchWindow + 1, b.length);
|
|
735
|
-
for (let j = start; j < end; j++) {
|
|
736
|
-
if (bMatches[j] || a[i] !== b[j]) continue;
|
|
737
|
-
aMatches[i] = true;
|
|
738
|
-
bMatches[j] = true;
|
|
739
|
-
matches++;
|
|
740
|
-
break;
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
if (matches === 0) return 0;
|
|
744
|
-
let k = 0;
|
|
745
|
-
for (let i = 0; i < a.length; i++) {
|
|
746
|
-
if (!aMatches[i]) continue;
|
|
747
|
-
while (!bMatches[k]) k++;
|
|
748
|
-
if (a[i] !== b[k]) transpositions++;
|
|
749
|
-
k++;
|
|
750
|
-
}
|
|
751
|
-
return (matches / a.length + matches / b.length + (matches - transpositions / 2) / matches) / 3;
|
|
752
|
-
}
|
|
753
|
-
function jaroWinklerSimilarity(a, b, scalingFactor = 0.1) {
|
|
754
|
-
const jaro = jaroSimilarity(a, b);
|
|
755
|
-
if (jaro === 0) return 0;
|
|
756
|
-
const prefixLength = Math.min(4, Math.min(a.length, b.length));
|
|
757
|
-
let commonPrefix = 0;
|
|
758
|
-
for (let i = 0; i < prefixLength; i++) {
|
|
759
|
-
if (a[i] === b[i]) {
|
|
760
|
-
commonPrefix++;
|
|
761
|
-
} else {
|
|
762
|
-
break;
|
|
763
|
-
}
|
|
953
|
+
/** Generate an embedding vector from label + summary + embeddable string properties if a provider is available */
|
|
954
|
+
async generateEmbedding(label, summary, properties, entityType) {
|
|
955
|
+
if (!this.embedding) return void 0;
|
|
956
|
+
const vocab = await this.vocabularyEngine.getVocabulary();
|
|
957
|
+
const typeDef = getEntityTypeDef(entityType, vocab);
|
|
958
|
+
const embeddableValues = typeDef ? typeDef.properties.filter((p) => p.embeddable === true && typeof properties[p.name] === "string").map((p) => properties[p.name]) : [];
|
|
959
|
+
const text = [label, summary ?? "", ...embeddableValues].filter(Boolean).join(" ");
|
|
960
|
+
return this.embedding.embed(text);
|
|
764
961
|
}
|
|
765
|
-
|
|
962
|
+
};
|
|
963
|
+
function storedToEntity(stored) {
|
|
964
|
+
return {
|
|
965
|
+
id: stored.id,
|
|
966
|
+
slug: stored.slug,
|
|
967
|
+
entityType: stored.entityType,
|
|
968
|
+
label: stored.label,
|
|
969
|
+
summary: stored.summary,
|
|
970
|
+
properties: stored.properties,
|
|
971
|
+
data: stored.data,
|
|
972
|
+
dataFormat: stored.dataFormat,
|
|
973
|
+
provenance: stored.provenance
|
|
974
|
+
};
|
|
766
975
|
}
|
|
767
|
-
function
|
|
768
|
-
return
|
|
976
|
+
function storedToSummary(stored) {
|
|
977
|
+
return {
|
|
978
|
+
id: stored.id,
|
|
979
|
+
slug: stored.slug,
|
|
980
|
+
entityType: stored.entityType,
|
|
981
|
+
label: stored.label,
|
|
982
|
+
summary: stored.summary,
|
|
983
|
+
properties: stored.properties
|
|
984
|
+
};
|
|
769
985
|
}
|
|
770
|
-
function
|
|
771
|
-
return
|
|
986
|
+
function storedToBrief(stored) {
|
|
987
|
+
return {
|
|
988
|
+
id: stored.id,
|
|
989
|
+
slug: stored.slug,
|
|
990
|
+
entityType: stored.entityType,
|
|
991
|
+
label: stored.label,
|
|
992
|
+
summary: stored.summary
|
|
993
|
+
};
|
|
772
994
|
}
|
|
773
995
|
|
|
774
996
|
// src/relationships/RelationshipManager.ts
|
|
@@ -1595,6 +1817,7 @@ var SearchOrchestrator = class {
|
|
|
1595
1817
|
search;
|
|
1596
1818
|
embedding;
|
|
1597
1819
|
eventBus;
|
|
1820
|
+
vocabularyEngine;
|
|
1598
1821
|
defaultSimilarityThreshold;
|
|
1599
1822
|
conceptSearchScanLimit;
|
|
1600
1823
|
constructor(config) {
|
|
@@ -1603,6 +1826,7 @@ var SearchOrchestrator = class {
|
|
|
1603
1826
|
this.search = config.search;
|
|
1604
1827
|
this.embedding = config.embedding;
|
|
1605
1828
|
this.eventBus = config.eventBus;
|
|
1829
|
+
this.vocabularyEngine = config.vocabularyEngine;
|
|
1606
1830
|
this.defaultSimilarityThreshold = config.defaultSimilarityThreshold ?? 0.5;
|
|
1607
1831
|
this.conceptSearchScanLimit = config.conceptSearchScanLimit ?? 1e3;
|
|
1608
1832
|
}
|
|
@@ -1677,7 +1901,7 @@ var SearchOrchestrator = class {
|
|
|
1677
1901
|
if (entity.embedding && entity.embedding.length > 0) {
|
|
1678
1902
|
score = this.cosineSimilarity(queryEmbedding, entity.embedding);
|
|
1679
1903
|
} else {
|
|
1680
|
-
const entityText =
|
|
1904
|
+
const entityText = await this.buildEmbeddingText(entity.entityType, entity.label, entity.summary, entity.properties);
|
|
1681
1905
|
const entityEmbedding = await this.embedding.embed(entityText);
|
|
1682
1906
|
score = this.cosineSimilarity(queryEmbedding, entityEmbedding);
|
|
1683
1907
|
}
|
|
@@ -1736,6 +1960,22 @@ var SearchOrchestrator = class {
|
|
|
1736
1960
|
offset
|
|
1737
1961
|
};
|
|
1738
1962
|
}
|
|
1963
|
+
/** Build the embedding text for an entity — label + summary + embeddable string properties */
|
|
1964
|
+
async buildEmbeddingText(entityType, label, summary, properties) {
|
|
1965
|
+
const embeddableValues = [];
|
|
1966
|
+
if (this.vocabularyEngine) {
|
|
1967
|
+
const vocab = await this.vocabularyEngine.getVocabulary();
|
|
1968
|
+
const typeDef = getEntityTypeDef(entityType, vocab);
|
|
1969
|
+
if (typeDef) {
|
|
1970
|
+
for (const p of typeDef.properties) {
|
|
1971
|
+
if (p.embeddable === true && typeof properties[p.name] === "string") {
|
|
1972
|
+
embeddableValues.push(properties[p.name]);
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
return [label, summary ?? "", ...embeddableValues].filter(Boolean).join(" ");
|
|
1978
|
+
}
|
|
1739
1979
|
/** Cosine similarity between two vectors */
|
|
1740
1980
|
cosineSimilarity(a, b) {
|
|
1741
1981
|
if (this.embedding?.similarity) {
|
|
@@ -1762,210 +2002,6 @@ var SearchOrchestrator = class {
|
|
|
1762
2002
|
}
|
|
1763
2003
|
};
|
|
1764
2004
|
|
|
1765
|
-
// src/vocabulary/VocabularyValidator.ts
|
|
1766
|
-
function ok() {
|
|
1767
|
-
return { valid: true, errors: [] };
|
|
1768
|
-
}
|
|
1769
|
-
function fail(...errors) {
|
|
1770
|
-
return { valid: false, errors };
|
|
1771
|
-
}
|
|
1772
|
-
function merge(...results) {
|
|
1773
|
-
const errors = results.flatMap((r) => r.errors);
|
|
1774
|
-
return { valid: errors.length === 0, errors };
|
|
1775
|
-
}
|
|
1776
|
-
function findClosestType(typeName, types) {
|
|
1777
|
-
if (types.length === 0) return void 0;
|
|
1778
|
-
const lower = typeName.toLowerCase();
|
|
1779
|
-
const match = types.find(
|
|
1780
|
-
(t) => t.type.toLowerCase().includes(lower) || lower.includes(t.type.toLowerCase())
|
|
1781
|
-
);
|
|
1782
|
-
return match?.type;
|
|
1783
|
-
}
|
|
1784
|
-
function validatePropertyValue(name, value, schema) {
|
|
1785
|
-
if (value === void 0 || value === null) {
|
|
1786
|
-
if (schema.required) {
|
|
1787
|
-
return fail({
|
|
1788
|
-
field: `properties.${name}`,
|
|
1789
|
-
message: `Required property "${name}" is missing`
|
|
1790
|
-
});
|
|
1791
|
-
}
|
|
1792
|
-
return ok();
|
|
1793
|
-
}
|
|
1794
|
-
switch (schema.type) {
|
|
1795
|
-
case "string":
|
|
1796
|
-
if (typeof value !== "string") {
|
|
1797
|
-
return fail({
|
|
1798
|
-
field: `properties.${name}`,
|
|
1799
|
-
message: `Property "${name}" must be a string, got ${typeof value}`
|
|
1800
|
-
});
|
|
1801
|
-
}
|
|
1802
|
-
break;
|
|
1803
|
-
case "number":
|
|
1804
|
-
if (typeof value !== "number" || Number.isNaN(value)) {
|
|
1805
|
-
return fail({
|
|
1806
|
-
field: `properties.${name}`,
|
|
1807
|
-
message: `Property "${name}" must be a number, got ${typeof value}`
|
|
1808
|
-
});
|
|
1809
|
-
}
|
|
1810
|
-
break;
|
|
1811
|
-
case "boolean":
|
|
1812
|
-
if (typeof value !== "boolean") {
|
|
1813
|
-
return fail({
|
|
1814
|
-
field: `properties.${name}`,
|
|
1815
|
-
message: `Property "${name}" must be a boolean, got ${typeof value}`
|
|
1816
|
-
});
|
|
1817
|
-
}
|
|
1818
|
-
break;
|
|
1819
|
-
case "date":
|
|
1820
|
-
if (typeof value !== "string" || Number.isNaN(Date.parse(value))) {
|
|
1821
|
-
return fail({
|
|
1822
|
-
field: `properties.${name}`,
|
|
1823
|
-
message: `Property "${name}" must be a valid ISO date string`
|
|
1824
|
-
});
|
|
1825
|
-
}
|
|
1826
|
-
break;
|
|
1827
|
-
case "enum":
|
|
1828
|
-
if (typeof value !== "string") {
|
|
1829
|
-
return fail({
|
|
1830
|
-
field: `properties.${name}`,
|
|
1831
|
-
message: `Property "${name}" must be a string (enum value), got ${typeof value}`
|
|
1832
|
-
});
|
|
1833
|
-
}
|
|
1834
|
-
if (schema.enumValues && !schema.enumValues.includes(value)) {
|
|
1835
|
-
return fail({
|
|
1836
|
-
field: `properties.${name}`,
|
|
1837
|
-
message: `Property "${name}" must be one of: ${schema.enumValues.join(", ")}`,
|
|
1838
|
-
suggestion: `Valid values: ${schema.enumValues.join(", ")}`
|
|
1839
|
-
});
|
|
1840
|
-
}
|
|
1841
|
-
break;
|
|
1842
|
-
}
|
|
1843
|
-
return ok();
|
|
1844
|
-
}
|
|
1845
|
-
function validateProperties(properties, schemas) {
|
|
1846
|
-
const results = [];
|
|
1847
|
-
const providedProperties = properties ?? {};
|
|
1848
|
-
for (const schema of schemas) {
|
|
1849
|
-
const value = providedProperties[schema.name];
|
|
1850
|
-
results.push(validatePropertyValue(schema.name, value, schema));
|
|
1851
|
-
}
|
|
1852
|
-
if (schemas.length > 0) {
|
|
1853
|
-
const knownNames = new Set(schemas.map((s) => s.name));
|
|
1854
|
-
for (const key of Object.keys(providedProperties)) {
|
|
1855
|
-
if (!knownNames.has(key)) {
|
|
1856
|
-
results.push(fail({
|
|
1857
|
-
field: `properties.${key}`,
|
|
1858
|
-
message: `Unknown property "${key}"`,
|
|
1859
|
-
suggestion: `Known properties: ${schemas.map((s) => s.name).join(", ")}`
|
|
1860
|
-
}));
|
|
1861
|
-
}
|
|
1862
|
-
}
|
|
1863
|
-
}
|
|
1864
|
-
return merge(...results);
|
|
1865
|
-
}
|
|
1866
|
-
function validateEntity(input, vocabulary) {
|
|
1867
|
-
const entityTypeDef = vocabulary.entityTypes.find((et) => et.type === input.entityType);
|
|
1868
|
-
if (!entityTypeDef) {
|
|
1869
|
-
const closest = findClosestType(input.entityType, vocabulary.entityTypes);
|
|
1870
|
-
return fail({
|
|
1871
|
-
field: "entityType",
|
|
1872
|
-
message: `Entity type "${input.entityType}" does not exist in the vocabulary`,
|
|
1873
|
-
suggestion: closest ? `Did you mean "${closest}"?` : `Available types: ${vocabulary.entityTypes.map((et) => et.type).join(", ") || "none"}`
|
|
1874
|
-
});
|
|
1875
|
-
}
|
|
1876
|
-
if (!input.label || input.label.trim().length === 0) {
|
|
1877
|
-
return fail({
|
|
1878
|
-
field: "label",
|
|
1879
|
-
message: "Entity label is required and cannot be empty"
|
|
1880
|
-
});
|
|
1881
|
-
}
|
|
1882
|
-
return validateProperties(input.properties, entityTypeDef.properties);
|
|
1883
|
-
}
|
|
1884
|
-
function validateEntityUpdate(input, currentEntityTypeDef, vocabulary) {
|
|
1885
|
-
if (input.label !== void 0 && input.label.trim().length === 0) {
|
|
1886
|
-
return fail({
|
|
1887
|
-
field: "label",
|
|
1888
|
-
message: "Entity label cannot be empty"
|
|
1889
|
-
});
|
|
1890
|
-
}
|
|
1891
|
-
let targetTypeDef = currentEntityTypeDef;
|
|
1892
|
-
if (input.entityType !== void 0 && input.entityType !== currentEntityTypeDef.type) {
|
|
1893
|
-
const newTypeDef = vocabulary.entityTypes.find((et) => et.type === input.entityType);
|
|
1894
|
-
if (!newTypeDef) {
|
|
1895
|
-
const closest = findClosestType(input.entityType, vocabulary.entityTypes);
|
|
1896
|
-
return fail({
|
|
1897
|
-
field: "entityType",
|
|
1898
|
-
message: `Entity type "${input.entityType}" does not exist in the vocabulary`,
|
|
1899
|
-
suggestion: closest ? `Did you mean "${closest}"?` : `Available types: ${vocabulary.entityTypes.map((et) => et.type).join(", ") || "none"}`
|
|
1900
|
-
});
|
|
1901
|
-
}
|
|
1902
|
-
targetTypeDef = newTypeDef;
|
|
1903
|
-
}
|
|
1904
|
-
if (input.properties) {
|
|
1905
|
-
const results = [];
|
|
1906
|
-
const hasDefinedProps = targetTypeDef.properties.length > 0;
|
|
1907
|
-
for (const [key, value] of Object.entries(input.properties)) {
|
|
1908
|
-
if (value === null) continue;
|
|
1909
|
-
const schema = targetTypeDef.properties.find((p) => p.name === key);
|
|
1910
|
-
if (!schema) {
|
|
1911
|
-
if (hasDefinedProps) {
|
|
1912
|
-
results.push(fail({
|
|
1913
|
-
field: `properties.${key}`,
|
|
1914
|
-
message: `Unknown property "${key}"`,
|
|
1915
|
-
suggestion: `Known properties: ${targetTypeDef.properties.map((p) => p.name).join(", ")}`
|
|
1916
|
-
}));
|
|
1917
|
-
}
|
|
1918
|
-
} else {
|
|
1919
|
-
results.push(validatePropertyValue(key, value, schema));
|
|
1920
|
-
}
|
|
1921
|
-
}
|
|
1922
|
-
return merge(...results);
|
|
1923
|
-
}
|
|
1924
|
-
return ok();
|
|
1925
|
-
}
|
|
1926
|
-
function validateRelationship(input, vocabulary, sourceEntityType, targetEntityType) {
|
|
1927
|
-
const normalizedType = toScreamingSnakeCase(input.relationshipType);
|
|
1928
|
-
const relTypeDef = vocabulary.relationshipTypes.find(
|
|
1929
|
-
(rt) => rt.type === normalizedType
|
|
1930
|
-
);
|
|
1931
|
-
if (!relTypeDef) {
|
|
1932
|
-
const closest = findClosestType(input.relationshipType, vocabulary.relationshipTypes);
|
|
1933
|
-
return fail({
|
|
1934
|
-
field: "relationshipType",
|
|
1935
|
-
message: `Relationship type "${input.relationshipType}" does not exist in the vocabulary`,
|
|
1936
|
-
suggestion: closest ? `Did you mean "${closest}"?` : `Available types: ${vocabulary.relationshipTypes.map((rt) => rt.type).join(", ") || "none"}`
|
|
1937
|
-
});
|
|
1938
|
-
}
|
|
1939
|
-
const results = [];
|
|
1940
|
-
if (relTypeDef.allowedSourceTypes.length > 0 && !relTypeDef.allowedSourceTypes.includes(sourceEntityType)) {
|
|
1941
|
-
results.push(
|
|
1942
|
-
fail({
|
|
1943
|
-
field: "sourceEntityId",
|
|
1944
|
-
message: `Entity type "${sourceEntityType}" is not allowed as source for relationship type "${input.relationshipType}"`,
|
|
1945
|
-
suggestion: `Allowed source types: ${relTypeDef.allowedSourceTypes.join(", ")}`
|
|
1946
|
-
})
|
|
1947
|
-
);
|
|
1948
|
-
}
|
|
1949
|
-
if (relTypeDef.allowedTargetTypes.length > 0 && !relTypeDef.allowedTargetTypes.includes(targetEntityType)) {
|
|
1950
|
-
results.push(
|
|
1951
|
-
fail({
|
|
1952
|
-
field: "targetEntityId",
|
|
1953
|
-
message: `Entity type "${targetEntityType}" is not allowed as target for relationship type "${input.relationshipType}"`,
|
|
1954
|
-
suggestion: `Allowed target types: ${relTypeDef.allowedTargetTypes.join(", ")}`
|
|
1955
|
-
})
|
|
1956
|
-
);
|
|
1957
|
-
}
|
|
1958
|
-
if (relTypeDef.properties) {
|
|
1959
|
-
results.push(
|
|
1960
|
-
validateProperties(input.properties, relTypeDef.properties)
|
|
1961
|
-
);
|
|
1962
|
-
}
|
|
1963
|
-
return merge(...results);
|
|
1964
|
-
}
|
|
1965
|
-
function getEntityTypeDef(entityType, vocabulary) {
|
|
1966
|
-
return vocabulary.entityTypes.find((et) => et.type === entityType) ?? null;
|
|
1967
|
-
}
|
|
1968
|
-
|
|
1969
2005
|
// src/validation/RepositoryValidator.ts
|
|
1970
2006
|
var DEFAULT_TAKE = 200;
|
|
1971
2007
|
var RepositoryValidator = class {
|
|
@@ -2187,6 +2223,7 @@ var MemoryRepository = class {
|
|
|
2187
2223
|
search: config.search,
|
|
2188
2224
|
embedding: config.embedding,
|
|
2189
2225
|
eventBus: config.eventBus,
|
|
2226
|
+
vocabularyEngine: config.vocabularyEngine,
|
|
2190
2227
|
defaultSimilarityThreshold: config.vocabularyEngine.getGovernanceConfig().defaultSimilarityThreshold
|
|
2191
2228
|
});
|
|
2192
2229
|
}
|
|
@@ -3230,6 +3267,10 @@ var VocabularyEngine = class {
|
|
|
3230
3267
|
}
|
|
3231
3268
|
}
|
|
3232
3269
|
}
|
|
3270
|
+
const propertySchemaErrors = this.validateProposalPropertySchemas(proposal);
|
|
3271
|
+
if (propertySchemaErrors) {
|
|
3272
|
+
return propertySchemaErrors;
|
|
3273
|
+
}
|
|
3233
3274
|
const { result, updatedVocabulary } = processProposal(
|
|
3234
3275
|
vocabulary,
|
|
3235
3276
|
proposal,
|
|
@@ -3267,6 +3308,37 @@ var VocabularyEngine = class {
|
|
|
3267
3308
|
}
|
|
3268
3309
|
return { deletedEntities: 0, deletedRelationships: 0 };
|
|
3269
3310
|
}
|
|
3311
|
+
/** Validate all property schemas in a proposal — returns a rejected result on the first invalid schema, or undefined if all pass */
|
|
3312
|
+
validateProposalPropertySchemas(proposal) {
|
|
3313
|
+
const schemas = [];
|
|
3314
|
+
let typeName = "";
|
|
3315
|
+
if (proposal.proposalType === "entity_type" && proposal.entityType) {
|
|
3316
|
+
schemas.push(...proposal.entityType.properties ?? []);
|
|
3317
|
+
typeName = proposal.entityType.type;
|
|
3318
|
+
} else if (proposal.proposalType === "relationship_type" && proposal.relationshipType) {
|
|
3319
|
+
schemas.push(...proposal.relationshipType.properties ?? []);
|
|
3320
|
+
typeName = proposal.relationshipType.type;
|
|
3321
|
+
} else if (proposal.proposalType === "edit_entity_type" && proposal.editEntityType) {
|
|
3322
|
+
schemas.push(...proposal.editEntityType.addProperties ?? []);
|
|
3323
|
+
schemas.push(...proposal.editEntityType.updateProperties ?? []);
|
|
3324
|
+
typeName = proposal.editEntityType.type;
|
|
3325
|
+
} else if (proposal.proposalType === "edit_relationship_type" && proposal.editRelationshipType) {
|
|
3326
|
+
schemas.push(...proposal.editRelationshipType.addProperties ?? []);
|
|
3327
|
+
schemas.push(...proposal.editRelationshipType.updateProperties ?? []);
|
|
3328
|
+
typeName = proposal.editRelationshipType.type;
|
|
3329
|
+
}
|
|
3330
|
+
for (const schema of schemas) {
|
|
3331
|
+
const result = validatePropertySchema(schema);
|
|
3332
|
+
if (!result.valid) {
|
|
3333
|
+
return {
|
|
3334
|
+
status: "rejected",
|
|
3335
|
+
type: typeName,
|
|
3336
|
+
reason: result.errors.map((e) => e.message).join("; ")
|
|
3337
|
+
};
|
|
3338
|
+
}
|
|
3339
|
+
}
|
|
3340
|
+
return void 0;
|
|
3341
|
+
}
|
|
3270
3342
|
getExistingTypesForProposal(proposal, vocabulary) {
|
|
3271
3343
|
if (proposal.proposalType === "entity_type") {
|
|
3272
3344
|
return vocabulary.entityTypes.map((et) => ({
|
|
@@ -3417,7 +3489,7 @@ var EventBus = class {
|
|
|
3417
3489
|
};
|
|
3418
3490
|
|
|
3419
3491
|
// src/portability/RepositoryExporter.ts
|
|
3420
|
-
var LIBRARY_VERSION = true ? "0.
|
|
3492
|
+
var LIBRARY_VERSION = true ? "0.8.0" : "0.1.0";
|
|
3421
3493
|
var RepositoryExporter = class {
|
|
3422
3494
|
storage;
|
|
3423
3495
|
provenance;
|