@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.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 entityEmbedding = needsReembed ? await this.generateEmbedding(updates.label ?? existing.label, nextSummary) : void 0;
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 texts = entries.map(([, e]) => [e.label, e.summary ?? ""].join(" "));
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) {
895
+ /** Generate an embedding vector from label + summary + embeddable string properties if a provider is available */
896
+ async generateEmbedding(label, summary, properties, entityType) {
627
897
  if (!this.embedding) return void 0;
628
- const text = [label, summary ?? ""].join(" ");
629
- return this.embedding.embed(text);
630
- }
631
- };
632
- function storedToEntity(stored) {
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
- }
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
- return jaro + commonPrefix * Math.min(scalingFactor, 0.25) * (1 - jaro);
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 normalizeTypeName(name) {
710
- return name.toLowerCase().trim().replace(/[-\s.]+/g, "_").replace(/[^a-z0-9_]/g, "");
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 toScreamingSnakeCase(name) {
713
- 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();
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 = [entity.label, entity.summary ?? ""].join(" ");
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.7.0" : "0.1.0";
3434
+ var LIBRARY_VERSION = true ? "0.8.1" : "0.1.0";
3363
3435
  var RepositoryExporter = class {
3364
3436
  storage;
3365
3437
  provenance;
@@ -3943,8 +4015,21 @@ var RepositoryImporter = class {
3943
4015
  relationshipId: rel.id
3944
4016
  });
3945
4017
  } else {
3946
- await this.storage.createRelationship(repositoryId, rel);
3947
- relationshipsImported++;
4018
+ try {
4019
+ await this.storage.createRelationship(repositoryId, rel);
4020
+ relationshipsImported++;
4021
+ } catch (err) {
4022
+ if (err instanceof DuplicateRelationshipError) {
4023
+ relationshipsSkipped++;
4024
+ warnings.push({
4025
+ code: "relationship_skipped",
4026
+ message: `Relationship "${rel.id}" already exists \u2014 skipped`,
4027
+ relationshipId: rel.id
4028
+ });
4029
+ } else {
4030
+ throw err;
4031
+ }
4032
+ }
3948
4033
  }
3949
4034
  }
3950
4035
  }