@utaba/deep-memory 0.6.1 → 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/index.cjs CHANGED
@@ -43,6 +43,8 @@ __export(src_exports, {
43
43
  RelationshipConstraintError: () => RelationshipConstraintError,
44
44
  RelationshipNotFoundError: () => RelationshipNotFoundError,
45
45
  RepositoryNotFoundError: () => RepositoryNotFoundError,
46
+ RepositoryValidator: () => RepositoryValidator,
47
+ SelfReferentialRelationshipError: () => SelfReferentialRelationshipError,
46
48
  TraversalTimeoutError: () => TraversalTimeoutError,
47
49
  TraversalValidationError: () => TraversalValidationError,
48
50
  TraversalVocabularyError: () => TraversalVocabularyError,
@@ -181,6 +183,20 @@ var RelationshipConstraintError = class extends DeepMemoryError {
181
183
  this.targetType = targetType;
182
184
  }
183
185
  };
186
+ var SelfReferentialRelationshipError = class extends DeepMemoryError {
187
+ entityId;
188
+ relationshipType;
189
+ constructor(entityId, relationshipType) {
190
+ super(
191
+ "SELF_REFERENTIAL_RELATIONSHIP",
192
+ `Self-referential relationship not allowed: "${relationshipType}" from entity "${entityId}" to itself`,
193
+ `Relationships must connect two different entities. Check that sourceEntityId and targetEntityId refer to distinct entities.`
194
+ );
195
+ this.name = "SelfReferentialRelationshipError";
196
+ this.entityId = entityId;
197
+ this.relationshipType = relationshipType;
198
+ }
199
+ };
184
200
  var GovernanceDeniedError = class extends DeepMemoryError {
185
201
  governanceMode;
186
202
  constructor(governanceMode, reason) {
@@ -323,6 +339,270 @@ function generateRelationshipId() {
323
339
  return generateId();
324
340
  }
325
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
+
326
606
  // src/entities/EntityManager.ts
327
607
  var EntityManager = class {
328
608
  constructor(repositoryId, vocabularyEngine, provenanceTracker, eventBus, storage, embedding) {
@@ -339,6 +619,10 @@ var EntityManager = class {
339
619
  eventBus;
340
620
  storage;
341
621
  embedding;
622
+ /** Swap the embedding provider, e.g. after a repository re-embed changes the model or dimensionality */
623
+ setEmbeddingProvider(embedding) {
624
+ this.embedding = embedding;
625
+ }
342
626
  /** Create one or more entities with vocabulary validation, ID generation, provenance, and events */
343
627
  async create(inputs) {
344
628
  const results = [];
@@ -368,7 +652,7 @@ var EntityManager = class {
368
652
  }
369
653
  );
370
654
  const provenance = this.provenanceTracker.stampCreate();
371
- const entityEmbedding = await this.generateEmbedding(input.label, input.summary);
655
+ const entityEmbedding = await this.generateEmbedding(input.label, input.summary, input.properties ?? {}, input.entityType);
372
656
  const storedEntity = {
373
657
  id,
374
658
  slug,
@@ -414,14 +698,42 @@ var EntityManager = class {
414
698
  throw new OperationCancelledError("Entity update", hookResult.reason ?? "cancelled by hook");
415
699
  }
416
700
  const provenance = this.provenanceTracker.stampUpdate(existing.provenance);
417
- const mergedProperties = updates.properties ? { ...existing.properties, ...updates.properties } : existing.properties;
418
- const needsReembed = updates.reembed === true || updates.label !== void 0 || updates.summary !== void 0;
419
- const entityEmbedding = needsReembed ? await this.generateEmbedding(
420
- updates.label ?? existing.label,
421
- updates.summary !== void 0 ? updates.summary : existing.summary
422
- ) : void 0;
701
+ let mergedProperties = existing.properties;
702
+ if (updates.properties) {
703
+ const merged = { ...existing.properties };
704
+ for (const [key, value] of Object.entries(updates.properties)) {
705
+ if (value === null) {
706
+ delete merged[key];
707
+ } else {
708
+ merged[key] = value;
709
+ }
710
+ }
711
+ mergedProperties = merged;
712
+ }
713
+ const typeChanged = updates.entityType !== void 0 && updates.entityType !== existing.entityType;
714
+ const labelChanged = updates.label !== void 0 && updates.label !== existing.label;
715
+ let newSlug;
716
+ if (typeChanged || labelChanged) {
717
+ const nextType = updates.entityType ?? existing.entityType;
718
+ const nextLabel = updates.label ?? existing.label;
719
+ newSlug = await generateUniqueSlug(
720
+ nextType,
721
+ nextLabel,
722
+ async (candidateSlug) => {
723
+ if (candidateSlug === existing.slug) return false;
724
+ const other = await this.storage.getEntityBySlug(this.repositoryId, candidateSlug);
725
+ return other !== null;
726
+ }
727
+ );
728
+ }
729
+ const needsReembed = updates.reembed === true || updates.label !== void 0 || updates.summary !== void 0 || updates.properties !== void 0;
730
+ const nextSummary = updates.summary === void 0 ? existing.summary : updates.summary ?? 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;
423
733
  const updated = await this.storage.updateEntity(this.repositoryId, entityId, {
734
+ entityType: typeChanged ? updates.entityType : void 0,
424
735
  label: updates.label,
736
+ slug: newSlug,
425
737
  summary: updates.summary,
426
738
  properties: updates.properties ? mergedProperties : void 0,
427
739
  data: updates.data,
@@ -492,18 +804,33 @@ var EntityManager = class {
492
804
  offset: result.offset
493
805
  };
494
806
  }
495
- /** Delete an entity */
807
+ /** Delete an entity — throws EntityNotFoundError if it does not exist */
496
808
  async delete(entityId) {
497
809
  const existing = await this.storage.getEntity(this.repositoryId, entityId);
498
810
  if (!existing) {
499
811
  throw new EntityNotFoundError(entityId);
500
812
  }
501
- const hookResult = await this.eventBus.emitHook("entity:deleting", { id: entityId });
813
+ const hookResult = await this.eventBus.emitHook("entity:deleting", { ids: [entityId] });
502
814
  if (hookResult.cancelled) {
503
815
  throw new OperationCancelledError("Entity deletion", hookResult.reason ?? "cancelled by hook");
504
816
  }
505
817
  await this.storage.deleteEntity(this.repositoryId, entityId);
506
- await this.eventBus.emit("entity:deleted", { id: entityId });
818
+ await this.eventBus.emit("entity:deleted", { ids: [entityId] });
819
+ }
820
+ /** Delete multiple entities in a single batch operation */
821
+ async deleteMany(ids) {
822
+ const hookResult = await this.eventBus.emitHook("entity:deleting", { ids });
823
+ if (hookResult.cancelled) {
824
+ throw new OperationCancelledError("Entity deletion", hookResult.reason ?? "cancelled by hook");
825
+ }
826
+ const { deleted, notFound } = await this.storage.deleteEntities(this.repositoryId, ids);
827
+ if (deleted.length > 0) {
828
+ await this.eventBus.emit("entity:deleted", { ids: deleted });
829
+ }
830
+ return {
831
+ deleted,
832
+ failed: notFound.map((id) => ({ id, error: `Entity '${id}' not found` }))
833
+ };
507
834
  }
508
835
  /**
509
836
  * Re-embed a specific set of entities using the current EmbeddingProvider.
@@ -516,7 +843,12 @@ var EntityManager = class {
516
843
  const maxRetries = options?.maxRetries ?? 3;
517
844
  const storedMap = await this.storage.getEntities(this.repositoryId, entityIds);
518
845
  const entries = Array.from(storedMap.entries());
519
- 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
+ });
520
852
  let embeddings;
521
853
  let attempt = 0;
522
854
  while (true) {
@@ -618,10 +950,13 @@ var EntityManager = class {
618
950
  dimensions: this.embedding.dimensions()
619
951
  };
620
952
  }
621
- /** Generate an embedding vector from label + summary if a provider is available */
622
- 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) {
623
955
  if (!this.embedding) return void 0;
624
- const text = [label, summary ?? ""].join(" ");
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(" ");
625
960
  return this.embedding.embed(text);
626
961
  }
627
962
  };
@@ -658,57 +993,6 @@ function storedToBrief(stored) {
658
993
  };
659
994
  }
660
995
 
661
- // src/vocabulary/similarity.ts
662
- function jaroSimilarity(a, b) {
663
- if (a === b) return 1;
664
- if (a.length === 0 || b.length === 0) return 0;
665
- const matchWindow = Math.max(Math.floor(Math.max(a.length, b.length) / 2) - 1, 0);
666
- const aMatches = new Array(a.length).fill(false);
667
- const bMatches = new Array(b.length).fill(false);
668
- let matches = 0;
669
- let transpositions = 0;
670
- for (let i = 0; i < a.length; i++) {
671
- const start = Math.max(0, i - matchWindow);
672
- const end = Math.min(i + matchWindow + 1, b.length);
673
- for (let j = start; j < end; j++) {
674
- if (bMatches[j] || a[i] !== b[j]) continue;
675
- aMatches[i] = true;
676
- bMatches[j] = true;
677
- matches++;
678
- break;
679
- }
680
- }
681
- if (matches === 0) return 0;
682
- let k = 0;
683
- for (let i = 0; i < a.length; i++) {
684
- if (!aMatches[i]) continue;
685
- while (!bMatches[k]) k++;
686
- if (a[i] !== b[k]) transpositions++;
687
- k++;
688
- }
689
- return (matches / a.length + matches / b.length + (matches - transpositions / 2) / matches) / 3;
690
- }
691
- function jaroWinklerSimilarity(a, b, scalingFactor = 0.1) {
692
- const jaro = jaroSimilarity(a, b);
693
- if (jaro === 0) return 0;
694
- const prefixLength = Math.min(4, Math.min(a.length, b.length));
695
- let commonPrefix = 0;
696
- for (let i = 0; i < prefixLength; i++) {
697
- if (a[i] === b[i]) {
698
- commonPrefix++;
699
- } else {
700
- break;
701
- }
702
- }
703
- return jaro + commonPrefix * Math.min(scalingFactor, 0.25) * (1 - jaro);
704
- }
705
- function normalizeTypeName(name) {
706
- return name.toLowerCase().trim().replace(/[-\s.]+/g, "_").replace(/[^a-z0-9_]/g, "");
707
- }
708
- function toScreamingSnakeCase(name) {
709
- 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();
710
- }
711
-
712
996
  // src/relationships/RelationshipManager.ts
713
997
  var RelationshipManager = class {
714
998
  constructor(repositoryId, vocabularyEngine, provenanceTracker, eventBus, storage) {
@@ -727,6 +1011,9 @@ var RelationshipManager = class {
727
1011
  async create(inputs) {
728
1012
  const results = [];
729
1013
  for (const input of inputs) {
1014
+ if (input.sourceEntityId === input.targetEntityId) {
1015
+ throw new SelfReferentialRelationshipError(input.sourceEntityId, input.relationshipType);
1016
+ }
730
1017
  const sourceEntity = await this.storage.getEntity(this.repositoryId, input.sourceEntityId);
731
1018
  if (!sourceEntity) {
732
1019
  throw new EntityNotFoundError(input.sourceEntityId);
@@ -777,21 +1064,20 @@ var RelationshipManager = class {
777
1064
  }
778
1065
  return results;
779
1066
  }
780
- /** Remove a relationship by ID */
781
- async remove(relationshipId) {
782
- const existing = await this.storage.getRelationship(this.repositoryId, relationshipId);
783
- if (!existing) {
784
- throw new RelationshipNotFoundError(relationshipId);
785
- }
786
- const hookResult = await this.eventBus.emitHook("relationship:removing", { id: relationshipId });
1067
+ /** Remove one or more relationships in a single batch storage operation */
1068
+ async removeMany(ids) {
1069
+ const hookResult = await this.eventBus.emitHook("relationship:removing", { ids });
787
1070
  if (hookResult.cancelled) {
788
- throw new OperationCancelledError(
789
- "Relationship removal",
790
- hookResult.reason ?? "cancelled by hook"
791
- );
1071
+ throw new OperationCancelledError("Relationship removal", hookResult.reason ?? "cancelled by hook");
1072
+ }
1073
+ const { deleted, notFound } = await this.storage.deleteRelationships(this.repositoryId, ids);
1074
+ if (deleted.length > 0) {
1075
+ await this.eventBus.emit("relationship:removed", { ids: deleted });
792
1076
  }
793
- await this.storage.deleteRelationship(this.repositoryId, relationshipId);
794
- await this.eventBus.emit("relationship:removed", { id: relationshipId });
1077
+ return {
1078
+ removed: deleted,
1079
+ failed: notFound.map((id) => ({ id, error: `Relationship '${id}' not found` }))
1080
+ };
795
1081
  }
796
1082
  /** Get relationships for an entity with filtering */
797
1083
  async getForEntity(entityId, options) {
@@ -1531,6 +1817,7 @@ var SearchOrchestrator = class {
1531
1817
  search;
1532
1818
  embedding;
1533
1819
  eventBus;
1820
+ vocabularyEngine;
1534
1821
  defaultSimilarityThreshold;
1535
1822
  conceptSearchScanLimit;
1536
1823
  constructor(config) {
@@ -1539,9 +1826,14 @@ var SearchOrchestrator = class {
1539
1826
  this.search = config.search;
1540
1827
  this.embedding = config.embedding;
1541
1828
  this.eventBus = config.eventBus;
1829
+ this.vocabularyEngine = config.vocabularyEngine;
1542
1830
  this.defaultSimilarityThreshold = config.defaultSimilarityThreshold ?? 0.5;
1543
1831
  this.conceptSearchScanLimit = config.conceptSearchScanLimit ?? 1e3;
1544
1832
  }
1833
+ /** Swap the embedding provider, e.g. after a repository re-embed changes the model or dimensionality */
1834
+ setEmbeddingProvider(embedding) {
1835
+ this.embedding = embedding;
1836
+ }
1545
1837
  /**
1546
1838
  * Find entities by label, type, and property filters.
1547
1839
  * When a SearchProvider is available, its results are merged with
@@ -1609,7 +1901,7 @@ var SearchOrchestrator = class {
1609
1901
  if (entity.embedding && entity.embedding.length > 0) {
1610
1902
  score = this.cosineSimilarity(queryEmbedding, entity.embedding);
1611
1903
  } else {
1612
- const entityText = [entity.label, entity.summary ?? ""].join(" ");
1904
+ const entityText = await this.buildEmbeddingText(entity.entityType, entity.label, entity.summary, entity.properties);
1613
1905
  const entityEmbedding = await this.embedding.embed(entityText);
1614
1906
  score = this.cosineSimilarity(queryEmbedding, entityEmbedding);
1615
1907
  }
@@ -1668,6 +1960,22 @@ var SearchOrchestrator = class {
1668
1960
  offset
1669
1961
  };
1670
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
+ }
1671
1979
  /** Cosine similarity between two vectors */
1672
1980
  cosineSimilarity(a, b) {
1673
1981
  if (this.embedding?.similarity) {
@@ -1694,6 +2002,177 @@ var SearchOrchestrator = class {
1694
2002
  }
1695
2003
  };
1696
2004
 
2005
+ // src/validation/RepositoryValidator.ts
2006
+ var DEFAULT_TAKE = 200;
2007
+ var RepositoryValidator = class {
2008
+ constructor(repositoryId, storage, vocabularyEngine) {
2009
+ this.repositoryId = repositoryId;
2010
+ this.storage = storage;
2011
+ this.vocabularyEngine = vocabularyEngine;
2012
+ }
2013
+ repositoryId;
2014
+ storage;
2015
+ vocabularyEngine;
2016
+ async validateEntities(options) {
2017
+ const offset = options?.offset ?? 0;
2018
+ const take = options?.take ?? DEFAULT_TAKE;
2019
+ const delayMs = options?.delayBetweenChunksMs ?? 0;
2020
+ const vocabulary = await this.vocabularyEngine.getVocabulary();
2021
+ const issues = [];
2022
+ let scanned = 0;
2023
+ let skipped = 0;
2024
+ let stoppedEarly = false;
2025
+ for await (const chunk of this.storage.exportAll(this.repositoryId)) {
2026
+ if (chunk.type !== "entities") {
2027
+ break;
2028
+ }
2029
+ const data = chunk.data;
2030
+ for (const entity of data) {
2031
+ scanned++;
2032
+ const result = validateEntity(
2033
+ {
2034
+ entityType: entity.entityType,
2035
+ label: entity.label,
2036
+ properties: entity.properties
2037
+ },
2038
+ vocabulary
2039
+ );
2040
+ if (!result.valid) {
2041
+ if (skipped < offset) {
2042
+ skipped++;
2043
+ continue;
2044
+ }
2045
+ issues.push({
2046
+ entityId: entity.id,
2047
+ slug: entity.slug,
2048
+ entityType: entity.entityType,
2049
+ label: entity.label,
2050
+ errors: result.errors
2051
+ });
2052
+ if (issues.length >= take) {
2053
+ stoppedEarly = true;
2054
+ break;
2055
+ }
2056
+ }
2057
+ }
2058
+ if (stoppedEarly) break;
2059
+ if (delayMs > 0 && !chunk.isLast) {
2060
+ await new Promise((resolve) => {
2061
+ setTimeout(resolve, delayMs);
2062
+ });
2063
+ }
2064
+ }
2065
+ return {
2066
+ issues,
2067
+ scanned,
2068
+ nextOffset: offset + issues.length,
2069
+ done: !stoppedEarly
2070
+ };
2071
+ }
2072
+ async validateRelationships(options) {
2073
+ const offset = options?.offset ?? 0;
2074
+ const take = options?.take ?? DEFAULT_TAKE;
2075
+ const delayMs = options?.delayBetweenChunksMs ?? 0;
2076
+ const vocabulary = await this.vocabularyEngine.getVocabulary();
2077
+ const entityMap = /* @__PURE__ */ new Map();
2078
+ const issues = [];
2079
+ let scanned = 0;
2080
+ let skipped = 0;
2081
+ let stoppedEarly = false;
2082
+ for await (const chunk of this.storage.exportAll(this.repositoryId)) {
2083
+ if (chunk.type === "entities") {
2084
+ for (const entity of chunk.data) {
2085
+ entityMap.set(entity.id, {
2086
+ entityType: entity.entityType,
2087
+ label: entity.label,
2088
+ slug: entity.slug
2089
+ });
2090
+ }
2091
+ if (delayMs > 0 && !chunk.isLast) {
2092
+ await new Promise((resolve) => {
2093
+ setTimeout(resolve, delayMs);
2094
+ });
2095
+ }
2096
+ continue;
2097
+ }
2098
+ const data = chunk.data;
2099
+ for (const rel of data) {
2100
+ scanned++;
2101
+ const src = entityMap.get(rel.sourceEntityId);
2102
+ const tgt = entityMap.get(rel.targetEntityId);
2103
+ const errors = [];
2104
+ if (!src) {
2105
+ errors.push({
2106
+ field: "sourceEntityId",
2107
+ message: `Source entity "${rel.sourceEntityId}" does not exist in this repository`
2108
+ });
2109
+ }
2110
+ if (!tgt) {
2111
+ errors.push({
2112
+ field: "targetEntityId",
2113
+ message: `Target entity "${rel.targetEntityId}" does not exist in this repository`
2114
+ });
2115
+ }
2116
+ if (rel.sourceEntityId === rel.targetEntityId) {
2117
+ errors.push({
2118
+ field: "targetEntityId",
2119
+ message: `Self-referential relationship: source and target are the same entity "${rel.sourceEntityId}"`,
2120
+ suggestion: "Remove the relationship, or repoint it to a different entity."
2121
+ });
2122
+ }
2123
+ if (src && tgt) {
2124
+ const result = validateRelationship(
2125
+ {
2126
+ relationshipType: rel.relationshipType,
2127
+ sourceEntityId: rel.sourceEntityId,
2128
+ targetEntityId: rel.targetEntityId,
2129
+ properties: rel.properties
2130
+ },
2131
+ vocabulary,
2132
+ src.entityType,
2133
+ tgt.entityType
2134
+ );
2135
+ errors.push(...result.errors);
2136
+ }
2137
+ if (errors.length > 0) {
2138
+ if (skipped < offset) {
2139
+ skipped++;
2140
+ continue;
2141
+ }
2142
+ issues.push({
2143
+ relationshipId: rel.id,
2144
+ relationshipType: rel.relationshipType,
2145
+ sourceEntityId: rel.sourceEntityId,
2146
+ targetEntityId: rel.targetEntityId,
2147
+ sourceLabel: src?.label,
2148
+ targetLabel: tgt?.label,
2149
+ sourceEntityType: src?.entityType,
2150
+ targetEntityType: tgt?.entityType,
2151
+ errors
2152
+ });
2153
+ if (issues.length >= take) {
2154
+ stoppedEarly = true;
2155
+ break;
2156
+ }
2157
+ }
2158
+ }
2159
+ if (stoppedEarly) break;
2160
+ if (delayMs > 0 && !chunk.isLast) {
2161
+ await new Promise((resolve) => {
2162
+ setTimeout(resolve, delayMs);
2163
+ });
2164
+ }
2165
+ }
2166
+ return {
2167
+ issues,
2168
+ scanned,
2169
+ nextOffset: offset + issues.length,
2170
+ done: !stoppedEarly,
2171
+ entitiesInMap: entityMap.size
2172
+ };
2173
+ }
2174
+ };
2175
+
1697
2176
  // src/core/MemoryRepository.ts
1698
2177
  var MemoryRepository = class {
1699
2178
  repositoryId;
@@ -1706,6 +2185,8 @@ var MemoryRepository = class {
1706
2185
  relationshipManager;
1707
2186
  graphTraversal;
1708
2187
  searchOrchestrator;
2188
+ embeddingFactory;
2189
+ embedding;
1709
2190
  constructor(config) {
1710
2191
  this.repositoryId = config.repositoryId;
1711
2192
  this.storage = config.storage;
@@ -1713,6 +2194,8 @@ var MemoryRepository = class {
1713
2194
  this.vocabularyEngine = config.vocabularyEngine;
1714
2195
  this.provenanceTracker = config.provenanceTracker;
1715
2196
  this.eventBus = config.eventBus;
2197
+ this.embedding = config.embedding;
2198
+ this.embeddingFactory = config.embeddingFactory;
1716
2199
  this.entityManager = new EntityManager(
1717
2200
  config.repositoryId,
1718
2201
  config.vocabularyEngine,
@@ -1740,6 +2223,7 @@ var MemoryRepository = class {
1740
2223
  search: config.search,
1741
2224
  embedding: config.embedding,
1742
2225
  eventBus: config.eventBus,
2226
+ vocabularyEngine: config.vocabularyEngine,
1743
2227
  defaultSimilarityThreshold: config.vocabularyEngine.getGovernanceConfig().defaultSimilarityThreshold
1744
2228
  });
1745
2229
  }
@@ -1798,11 +2282,14 @@ var MemoryRepository = class {
1798
2282
  async findEntities(query) {
1799
2283
  return this.searchOrchestrator.findEntities(query);
1800
2284
  }
1801
- async deleteEntity(entityId) {
1802
- await this.entityManager.delete(entityId);
1803
- if (this.search) {
1804
- await this.search.removeEntity(this.repositoryId, entityId);
2285
+ async deleteEntities(ids) {
2286
+ const result = await this.entityManager.deleteMany(ids);
2287
+ if (this.search && result.deleted.length > 0) {
2288
+ for (const id of result.deleted) {
2289
+ await this.search.removeEntity(this.repositoryId, id);
2290
+ }
1805
2291
  }
2292
+ return result;
1806
2293
  }
1807
2294
  // ─── Re-embedding ──────────────────────────────────────────────────
1808
2295
  /** Re-embed specific entities using the current EmbeddingProvider */
@@ -1810,11 +2297,45 @@ var MemoryRepository = class {
1810
2297
  return this.entityManager.reembedEntities(entityIds);
1811
2298
  }
1812
2299
  /**
1813
- * Re-embed all entities in the repository using the current EmbeddingProvider.
2300
+ * Re-embed all entities in the repository. If `model` or `dimensions` is provided,
2301
+ * the repository's embedding configuration is updated to the new values before
2302
+ * re-embedding, so all subsequent entity writes use the new model/dimensionality.
2303
+ *
1814
2304
  * Processes in batches with optional rate limiting. Emits reembed:* events for progress tracking.
1815
2305
  * Updates repository metadata with the new model ID and dimensions on completion.
1816
2306
  */
1817
2307
  async reembedAll(options) {
2308
+ if (options?.model !== void 0 || options?.dimensions !== void 0) {
2309
+ if (!this.embeddingFactory) {
2310
+ throw new Error(
2311
+ "Cannot change embedding model or dimensions: DeepMemory was constructed without an embeddingFactory. Provide embeddingFactory in DeepMemoryConfig to enable per-repository embedding reconfiguration."
2312
+ );
2313
+ }
2314
+ const currentModel = this.embedding?.modelId();
2315
+ let currentDimensions;
2316
+ try {
2317
+ currentDimensions = this.embedding?.dimensions();
2318
+ } catch {
2319
+ currentDimensions = void 0;
2320
+ }
2321
+ const nextModel = options.model ?? currentModel;
2322
+ const nextDimensions = options.dimensions ?? currentDimensions;
2323
+ if (!nextModel || nextDimensions === void 0) {
2324
+ throw new Error(
2325
+ `Cannot rebuild embedding provider: model and dimensions must both be known. Got model="${nextModel ?? "undefined"}", dimensions=${nextDimensions ?? "undefined"}.`
2326
+ );
2327
+ }
2328
+ const newProvider = this.embeddingFactory({ model: nextModel, dimensions: nextDimensions });
2329
+ this.embedding = newProvider;
2330
+ this.entityManager.setEmbeddingProvider(newProvider);
2331
+ this.searchOrchestrator.setEmbeddingProvider(newProvider);
2332
+ await this.storage.updateRepository(this.repositoryId, {
2333
+ metadata: {
2334
+ embeddingModelId: nextModel,
2335
+ embeddingDimensions: nextDimensions
2336
+ }
2337
+ });
2338
+ }
1818
2339
  const stats = await this.getStats();
1819
2340
  await this.eventBus.emit("reembed:started", {
1820
2341
  repositoryId: this.repositoryId,
@@ -1860,8 +2381,8 @@ var MemoryRepository = class {
1860
2381
  async createRelationships(inputs) {
1861
2382
  return this.relationshipManager.create(inputs);
1862
2383
  }
1863
- async removeRelationship(relationshipId) {
1864
- return this.relationshipManager.remove(relationshipId);
2384
+ async removeRelationships(ids) {
2385
+ return this.relationshipManager.removeMany(ids);
1865
2386
  }
1866
2387
  async getRelationships(entityId, options) {
1867
2388
  return this.relationshipManager.getForEntity(entityId, options);
@@ -2047,6 +2568,34 @@ var MemoryRepository = class {
2047
2568
  async getStats() {
2048
2569
  return this.storage.getRepositoryStats(this.repositoryId);
2049
2570
  }
2571
+ // ─── Validation ────────────────────────────────────────────────────
2572
+ /**
2573
+ * Audit a window of entities against the current vocabulary. Returns a single
2574
+ * page; callers loop until `done` is true. Paging is over scanned entities,
2575
+ * not issues. Does not mutate anything.
2576
+ */
2577
+ async validateEntities(options) {
2578
+ const validator = new RepositoryValidator(
2579
+ this.repositoryId,
2580
+ this.storage,
2581
+ this.vocabularyEngine
2582
+ );
2583
+ return validator.validateEntities(options);
2584
+ }
2585
+ /**
2586
+ * Audit a window of relationships against the current vocabulary. Returns a
2587
+ * single page; callers loop until `done` is true. The full entity set is
2588
+ * loaded once per call to resolve orphan and type-mismatch checks, then
2589
+ * offset/take is applied to the relationship stream. Does not mutate anything.
2590
+ */
2591
+ async validateRelationships(options) {
2592
+ const validator = new RepositoryValidator(
2593
+ this.repositoryId,
2594
+ this.storage,
2595
+ this.vocabularyEngine
2596
+ );
2597
+ return validator.validateRelationships(options);
2598
+ }
2050
2599
  // ─── Bulk Operations ──────────────────────────────────────────────
2051
2600
  /** Delete all entities and relationships in this repository, preserving the repository and vocabulary */
2052
2601
  async deleteAllContents() {
@@ -2620,195 +3169,6 @@ function generateChangeId() {
2620
3169
  return `change_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
2621
3170
  }
2622
3171
 
2623
- // src/vocabulary/VocabularyValidator.ts
2624
- function ok() {
2625
- return { valid: true, errors: [] };
2626
- }
2627
- function fail(...errors) {
2628
- return { valid: false, errors };
2629
- }
2630
- function merge(...results) {
2631
- const errors = results.flatMap((r) => r.errors);
2632
- return { valid: errors.length === 0, errors };
2633
- }
2634
- function findClosestType(typeName, types) {
2635
- if (types.length === 0) return void 0;
2636
- const lower = typeName.toLowerCase();
2637
- const match = types.find(
2638
- (t) => t.type.toLowerCase().includes(lower) || lower.includes(t.type.toLowerCase())
2639
- );
2640
- return match?.type;
2641
- }
2642
- function validatePropertyValue(name, value, schema) {
2643
- if (value === void 0 || value === null) {
2644
- if (schema.required) {
2645
- return fail({
2646
- field: `properties.${name}`,
2647
- message: `Required property "${name}" is missing`
2648
- });
2649
- }
2650
- return ok();
2651
- }
2652
- switch (schema.type) {
2653
- case "string":
2654
- if (typeof value !== "string") {
2655
- return fail({
2656
- field: `properties.${name}`,
2657
- message: `Property "${name}" must be a string, got ${typeof value}`
2658
- });
2659
- }
2660
- break;
2661
- case "number":
2662
- if (typeof value !== "number" || Number.isNaN(value)) {
2663
- return fail({
2664
- field: `properties.${name}`,
2665
- message: `Property "${name}" must be a number, got ${typeof value}`
2666
- });
2667
- }
2668
- break;
2669
- case "boolean":
2670
- if (typeof value !== "boolean") {
2671
- return fail({
2672
- field: `properties.${name}`,
2673
- message: `Property "${name}" must be a boolean, got ${typeof value}`
2674
- });
2675
- }
2676
- break;
2677
- case "date":
2678
- if (typeof value !== "string" || Number.isNaN(Date.parse(value))) {
2679
- return fail({
2680
- field: `properties.${name}`,
2681
- message: `Property "${name}" must be a valid ISO date string`
2682
- });
2683
- }
2684
- break;
2685
- case "enum":
2686
- if (typeof value !== "string") {
2687
- return fail({
2688
- field: `properties.${name}`,
2689
- message: `Property "${name}" must be a string (enum value), got ${typeof value}`
2690
- });
2691
- }
2692
- if (schema.enumValues && !schema.enumValues.includes(value)) {
2693
- return fail({
2694
- field: `properties.${name}`,
2695
- message: `Property "${name}" must be one of: ${schema.enumValues.join(", ")}`,
2696
- suggestion: `Valid values: ${schema.enumValues.join(", ")}`
2697
- });
2698
- }
2699
- break;
2700
- }
2701
- return ok();
2702
- }
2703
- function validateProperties(properties, schemas, context) {
2704
- const results = [];
2705
- const providedProperties = properties ?? {};
2706
- for (const schema of schemas) {
2707
- const value = providedProperties[schema.name];
2708
- results.push(validatePropertyValue(schema.name, value, schema));
2709
- }
2710
- const knownNames = new Set(schemas.map((s) => s.name));
2711
- for (const key of Object.keys(providedProperties)) {
2712
- if (!knownNames.has(key)) {
2713
- results.push(
2714
- fail({
2715
- field: `properties.${key}`,
2716
- message: `Unknown property "${key}" on ${context}`,
2717
- suggestion: `Known properties: ${schemas.map((s) => s.name).join(", ") || "none"}`
2718
- })
2719
- );
2720
- }
2721
- }
2722
- return merge(...results);
2723
- }
2724
- function validateEntity(input, vocabulary) {
2725
- const entityTypeDef = vocabulary.entityTypes.find((et) => et.type === input.entityType);
2726
- if (!entityTypeDef) {
2727
- const closest = findClosestType(input.entityType, vocabulary.entityTypes);
2728
- return fail({
2729
- field: "entityType",
2730
- message: `Entity type "${input.entityType}" does not exist in the vocabulary`,
2731
- suggestion: closest ? `Did you mean "${closest}"?` : `Available types: ${vocabulary.entityTypes.map((et) => et.type).join(", ") || "none"}`
2732
- });
2733
- }
2734
- if (!input.label || input.label.trim().length === 0) {
2735
- return fail({
2736
- field: "label",
2737
- message: "Entity label is required and cannot be empty"
2738
- });
2739
- }
2740
- return validateProperties(input.properties, entityTypeDef.properties, input.entityType);
2741
- }
2742
- function validateEntityUpdate(input, entityTypeDef) {
2743
- if (input.label !== void 0 && input.label.trim().length === 0) {
2744
- return fail({
2745
- field: "label",
2746
- message: "Entity label cannot be empty"
2747
- });
2748
- }
2749
- if (input.properties) {
2750
- const results = [];
2751
- for (const [key, value] of Object.entries(input.properties)) {
2752
- const schema = entityTypeDef.properties.find((p) => p.name === key);
2753
- if (!schema) {
2754
- results.push(
2755
- fail({
2756
- field: `properties.${key}`,
2757
- message: `Unknown property "${key}" on ${entityTypeDef.type}`,
2758
- suggestion: `Known properties: ${entityTypeDef.properties.map((p) => p.name).join(", ") || "none"}`
2759
- })
2760
- );
2761
- } else {
2762
- results.push(validatePropertyValue(key, value, schema));
2763
- }
2764
- }
2765
- return merge(...results);
2766
- }
2767
- return ok();
2768
- }
2769
- function validateRelationship(input, vocabulary, sourceEntityType, targetEntityType) {
2770
- const normalizedType = toScreamingSnakeCase(input.relationshipType);
2771
- const relTypeDef = vocabulary.relationshipTypes.find(
2772
- (rt) => rt.type === normalizedType
2773
- );
2774
- if (!relTypeDef) {
2775
- const closest = findClosestType(input.relationshipType, vocabulary.relationshipTypes);
2776
- return fail({
2777
- field: "relationshipType",
2778
- message: `Relationship type "${input.relationshipType}" does not exist in the vocabulary`,
2779
- suggestion: closest ? `Did you mean "${closest}"?` : `Available types: ${vocabulary.relationshipTypes.map((rt) => rt.type).join(", ") || "none"}`
2780
- });
2781
- }
2782
- const results = [];
2783
- if (relTypeDef.allowedSourceTypes.length > 0 && !relTypeDef.allowedSourceTypes.includes(sourceEntityType)) {
2784
- results.push(
2785
- fail({
2786
- field: "sourceEntityId",
2787
- message: `Entity type "${sourceEntityType}" is not allowed as source for relationship type "${input.relationshipType}"`,
2788
- suggestion: `Allowed source types: ${relTypeDef.allowedSourceTypes.join(", ")}`
2789
- })
2790
- );
2791
- }
2792
- if (relTypeDef.allowedTargetTypes.length > 0 && !relTypeDef.allowedTargetTypes.includes(targetEntityType)) {
2793
- results.push(
2794
- fail({
2795
- field: "targetEntityId",
2796
- message: `Entity type "${targetEntityType}" is not allowed as target for relationship type "${input.relationshipType}"`,
2797
- suggestion: `Allowed target types: ${relTypeDef.allowedTargetTypes.join(", ")}`
2798
- })
2799
- );
2800
- }
2801
- if (relTypeDef.properties) {
2802
- results.push(
2803
- validateProperties(input.properties, relTypeDef.properties, input.relationshipType)
2804
- );
2805
- }
2806
- return merge(...results);
2807
- }
2808
- function getEntityTypeDef(entityType, vocabulary) {
2809
- return vocabulary.entityTypes.find((et) => et.type === entityType) ?? null;
2810
- }
2811
-
2812
3172
  // src/core/VocabularyEngine.ts
2813
3173
  var VocabularyEngine = class {
2814
3174
  repositoryId;
@@ -2870,7 +3230,7 @@ var VocabularyEngine = class {
2870
3230
  ]
2871
3231
  };
2872
3232
  }
2873
- return validateEntityUpdate(input, typeDef);
3233
+ return validateEntityUpdate(input, typeDef, vocabulary);
2874
3234
  }
2875
3235
  /** Validate a relationship creation input against the vocabulary */
2876
3236
  async validateRelationship(input, sourceEntityType, targetEntityType) {
@@ -2907,6 +3267,10 @@ var VocabularyEngine = class {
2907
3267
  }
2908
3268
  }
2909
3269
  }
3270
+ const propertySchemaErrors = this.validateProposalPropertySchemas(proposal);
3271
+ if (propertySchemaErrors) {
3272
+ return propertySchemaErrors;
3273
+ }
2910
3274
  const { result, updatedVocabulary } = processProposal(
2911
3275
  vocabulary,
2912
3276
  proposal,
@@ -2944,6 +3308,37 @@ var VocabularyEngine = class {
2944
3308
  }
2945
3309
  return { deletedEntities: 0, deletedRelationships: 0 };
2946
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
+ }
2947
3342
  getExistingTypesForProposal(proposal, vocabulary) {
2948
3343
  if (proposal.proposalType === "entity_type") {
2949
3344
  return vocabulary.entityTypes.map((et) => ({
@@ -3094,7 +3489,7 @@ var EventBus = class {
3094
3489
  };
3095
3490
 
3096
3491
  // src/portability/RepositoryExporter.ts
3097
- var LIBRARY_VERSION = true ? "0.6.1" : "0.1.0";
3492
+ var LIBRARY_VERSION = true ? "0.8.0" : "0.1.0";
3098
3493
  var RepositoryExporter = class {
3099
3494
  storage;
3100
3495
  provenance;
@@ -3744,7 +4139,9 @@ function isValidUuid(value) {
3744
4139
  var DeepMemory = class {
3745
4140
  storage;
3746
4141
  search;
3747
- embedding;
4142
+ embeddingFactory;
4143
+ defaultEmbeddingModel;
4144
+ defaultEmbeddingDimensions;
3748
4145
  /** Reserved for future distributed locking support */
3749
4146
  lock;
3750
4147
  graphTraversalProvider;
@@ -3754,12 +4151,22 @@ var DeepMemory = class {
3754
4151
  constructor(config) {
3755
4152
  this.storage = config.storage;
3756
4153
  this.search = config.search;
3757
- this.embedding = config.embedding;
4154
+ this.embeddingFactory = config.embeddingFactory;
4155
+ this.defaultEmbeddingModel = config.defaultEmbeddingModel;
4156
+ this.defaultEmbeddingDimensions = config.defaultEmbeddingDimensions;
3758
4157
  this.lock = config.lock;
3759
4158
  this.graphTraversalProvider = config.graphTraversal;
3760
4159
  this.provenance = new ProvenanceTracker(config.provenance);
3761
4160
  this.globalEventBus = new EventBus(config.provenance);
3762
4161
  }
4162
+ /**
4163
+ * Build an embedding provider for a repository given its stored model + dimensions.
4164
+ * Returns undefined when no factory is configured or when model/dimensions are missing.
4165
+ */
4166
+ buildEmbeddingForRepository(model, dimensions) {
4167
+ if (!this.embeddingFactory || !model || dimensions === void 0) return void 0;
4168
+ return this.embeddingFactory({ model, dimensions });
4169
+ }
3763
4170
  /** Initialize the storage provider and optional providers (call once before use) */
3764
4171
  async ensureInitialized() {
3765
4172
  if (!this.initialized) {
@@ -3811,16 +4218,11 @@ var DeepMemory = class {
3811
4218
  const now = (/* @__PURE__ */ new Date()).toISOString();
3812
4219
  const governanceConfig = config.governance ?? { mode: "open" };
3813
4220
  const metadata = { ...config.metadata };
3814
- if (this.embedding) {
3815
- if (!metadata.embeddingModelId) {
3816
- metadata.embeddingModelId = this.embedding.modelId();
3817
- }
3818
- if (metadata.embeddingDimensions === void 0) {
3819
- try {
3820
- metadata.embeddingDimensions = this.embedding.dimensions();
3821
- } catch {
3822
- }
3823
- }
4221
+ if (!metadata.embeddingModelId && this.defaultEmbeddingModel) {
4222
+ metadata.embeddingModelId = this.defaultEmbeddingModel;
4223
+ }
4224
+ if (metadata.embeddingDimensions === void 0 && this.defaultEmbeddingDimensions !== void 0) {
4225
+ metadata.embeddingDimensions = this.defaultEmbeddingDimensions;
3824
4226
  }
3825
4227
  await this.storage.createRepository({
3826
4228
  repositoryId,
@@ -3836,18 +4238,23 @@ var DeepMemory = class {
3836
4238
  });
3837
4239
  const vocabulary = config.vocabulary ? buildVocabulary(config.vocabulary, context.actorId) : createEmptyVocabulary(context.actorId);
3838
4240
  await this.storage.saveVocabulary(repositoryId, vocabulary);
4241
+ const embedding = this.buildEmbeddingForRepository(
4242
+ metadata.embeddingModelId,
4243
+ metadata.embeddingDimensions
4244
+ );
3839
4245
  const eventBus = new EventBus(context, repositoryId);
3840
4246
  const vocabularyEngine = new VocabularyEngine({
3841
4247
  repositoryId,
3842
4248
  storageProvider: this.storage,
3843
4249
  governanceConfig,
3844
- embeddingProvider: this.embedding
4250
+ embeddingProvider: embedding
3845
4251
  });
3846
4252
  const repo = new MemoryRepository({
3847
4253
  repositoryId,
3848
4254
  storage: this.storage,
3849
4255
  search: this.search,
3850
- embedding: this.embedding,
4256
+ embedding,
4257
+ embeddingFactory: this.embeddingFactory,
3851
4258
  graphTraversal: this.graphTraversalProvider,
3852
4259
  vocabularyEngine,
3853
4260
  provenanceTracker: this.provenance,
@@ -3869,17 +4276,22 @@ var DeepMemory = class {
3869
4276
  }
3870
4277
  const context = this.provenance.getContext();
3871
4278
  const eventBus = new EventBus(context, repositoryId);
4279
+ const embedding = this.buildEmbeddingForRepository(
4280
+ storedRepo.metadata?.embeddingModelId ?? this.defaultEmbeddingModel,
4281
+ storedRepo.metadata?.embeddingDimensions ?? this.defaultEmbeddingDimensions
4282
+ );
3872
4283
  const vocabularyEngine = new VocabularyEngine({
3873
4284
  repositoryId,
3874
4285
  storageProvider: this.storage,
3875
4286
  governanceConfig: storedRepo.governanceConfig,
3876
- embeddingProvider: this.embedding
4287
+ embeddingProvider: embedding
3877
4288
  });
3878
4289
  const repo = new MemoryRepository({
3879
4290
  repositoryId,
3880
4291
  storage: this.storage,
3881
4292
  search: this.search,
3882
- embedding: this.embedding,
4293
+ embedding,
4294
+ embeddingFactory: this.embeddingFactory,
3883
4295
  graphTraversal: this.graphTraversalProvider,
3884
4296
  vocabularyEngine,
3885
4297
  provenanceTracker: this.provenance,
@@ -4601,11 +5013,12 @@ var InMemoryStorageProvider = class {
4601
5013
  }
4602
5014
  const updated = {
4603
5015
  ...existing,
5016
+ entityType: updates.entityType ?? existing.entityType,
4604
5017
  label: updates.label ?? existing.label,
4605
- summary: updates.summary !== void 0 ? updates.summary : existing.summary,
5018
+ summary: updates.summary === void 0 ? existing.summary : updates.summary ?? void 0,
4606
5019
  properties: updates.properties ?? existing.properties,
4607
- data: updates.data !== void 0 ? updates.data : existing.data,
4608
- dataFormat: updates.dataFormat !== void 0 ? updates.dataFormat : existing.dataFormat,
5020
+ data: updates.data === void 0 ? existing.data : updates.data ?? void 0,
5021
+ dataFormat: updates.dataFormat === void 0 ? existing.dataFormat : updates.dataFormat ?? void 0,
4609
5022
  provenance: updates.provenance,
4610
5023
  embedding: updates.embedding ?? existing.embedding
4611
5024
  };
@@ -4732,6 +5145,29 @@ var InMemoryStorageProvider = class {
4732
5145
  offset
4733
5146
  };
4734
5147
  }
5148
+ async deleteEntities(repositoryId, ids) {
5149
+ const store = this.getStore(repositoryId);
5150
+ const deleted = [];
5151
+ const notFound = [];
5152
+ const deletedSet = /* @__PURE__ */ new Set();
5153
+ for (const id of ids) {
5154
+ const existing = store.entities.get(id);
5155
+ if (!existing) {
5156
+ notFound.push(id);
5157
+ continue;
5158
+ }
5159
+ store.slugIndex.delete(existing.slug);
5160
+ store.entities.delete(id);
5161
+ deleted.push(id);
5162
+ deletedSet.add(id);
5163
+ }
5164
+ for (const [relId, rel] of store.relationships) {
5165
+ if (deletedSet.has(rel.sourceEntityId) || deletedSet.has(rel.targetEntityId)) {
5166
+ store.relationships.delete(relId);
5167
+ }
5168
+ }
5169
+ return { deleted, notFound };
5170
+ }
4735
5171
  async deleteRelationship(repositoryId, relationshipId) {
4736
5172
  const store = this.getStore(repositoryId);
4737
5173
  if (!store.relationships.has(relationshipId)) {
@@ -4739,6 +5175,20 @@ var InMemoryStorageProvider = class {
4739
5175
  }
4740
5176
  store.relationships.delete(relationshipId);
4741
5177
  }
5178
+ async deleteRelationships(repositoryId, ids) {
5179
+ const store = this.getStore(repositoryId);
5180
+ const deleted = [];
5181
+ const notFound = [];
5182
+ for (const id of ids) {
5183
+ if (store.relationships.has(id)) {
5184
+ store.relationships.delete(id);
5185
+ deleted.push(id);
5186
+ } else {
5187
+ notFound.push(id);
5188
+ }
5189
+ }
5190
+ return { deleted, notFound };
5191
+ }
4742
5192
  async deleteRelationshipsByType(repositoryId, relationshipType) {
4743
5193
  const store = this.getStore(repositoryId);
4744
5194
  let deletedRelationships = 0;
@@ -5150,6 +5600,8 @@ var NoOpEmbeddingProvider = class {
5150
5600
  RelationshipConstraintError,
5151
5601
  RelationshipNotFoundError,
5152
5602
  RepositoryNotFoundError,
5603
+ RepositoryValidator,
5604
+ SelfReferentialRelationshipError,
5153
5605
  TraversalTimeoutError,
5154
5606
  TraversalValidationError,
5155
5607
  TraversalVocabularyError,