@utaba/deep-memory 0.7.0 → 0.8.1

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/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 entityEmbedding = needsReembed ? await this.generateEmbedding(updates.label ?? existing.label, nextSummary) : void 0;
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 texts = entries.map(([, e]) => [e.label, e.summary ?? ""].join(" "));
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) {
953
+ /** Generate an embedding vector from label + summary + embeddable string properties if a provider is available */
954
+ async generateEmbedding(label, summary, properties, entityType) {
685
955
  if (!this.embedding) return void 0;
686
- const text = [label, summary ?? ""].join(" ");
687
- return this.embedding.embed(text);
688
- }
689
- };
690
- function storedToEntity(stored) {
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
- }
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
- return jaro + commonPrefix * Math.min(scalingFactor, 0.25) * (1 - jaro);
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 normalizeTypeName(name) {
768
- return name.toLowerCase().trim().replace(/[-\s.]+/g, "_").replace(/[^a-z0-9_]/g, "");
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 toScreamingSnakeCase(name) {
771
- 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();
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 = [entity.label, entity.summary ?? ""].join(" ");
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.7.0" : "0.1.0";
3492
+ var LIBRARY_VERSION = true ? "0.8.1" : "0.1.0";
3421
3493
  var RepositoryExporter = class {
3422
3494
  storage;
3423
3495
  provenance;
@@ -4001,8 +4073,21 @@ var RepositoryImporter = class {
4001
4073
  relationshipId: rel.id
4002
4074
  });
4003
4075
  } else {
4004
- await this.storage.createRelationship(repositoryId, rel);
4005
- relationshipsImported++;
4076
+ try {
4077
+ await this.storage.createRelationship(repositoryId, rel);
4078
+ relationshipsImported++;
4079
+ } catch (err) {
4080
+ if (err instanceof DuplicateRelationshipError) {
4081
+ relationshipsSkipped++;
4082
+ warnings.push({
4083
+ code: "relationship_skipped",
4084
+ message: `Relationship "${rel.id}" already exists \u2014 skipped`,
4085
+ relationshipId: rel.id
4086
+ });
4087
+ } else {
4088
+ throw err;
4089
+ }
4090
+ }
4006
4091
  }
4007
4092
  }
4008
4093
  }