@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.js CHANGED
@@ -125,6 +125,20 @@ var RelationshipConstraintError = class extends DeepMemoryError {
125
125
  this.targetType = targetType;
126
126
  }
127
127
  };
128
+ var SelfReferentialRelationshipError = class extends DeepMemoryError {
129
+ entityId;
130
+ relationshipType;
131
+ constructor(entityId, relationshipType) {
132
+ super(
133
+ "SELF_REFERENTIAL_RELATIONSHIP",
134
+ `Self-referential relationship not allowed: "${relationshipType}" from entity "${entityId}" to itself`,
135
+ `Relationships must connect two different entities. Check that sourceEntityId and targetEntityId refer to distinct entities.`
136
+ );
137
+ this.name = "SelfReferentialRelationshipError";
138
+ this.entityId = entityId;
139
+ this.relationshipType = relationshipType;
140
+ }
141
+ };
128
142
  var GovernanceDeniedError = class extends DeepMemoryError {
129
143
  governanceMode;
130
144
  constructor(governanceMode, reason) {
@@ -267,6 +281,270 @@ function generateRelationshipId() {
267
281
  return generateId();
268
282
  }
269
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
+
270
548
  // src/entities/EntityManager.ts
271
549
  var EntityManager = class {
272
550
  constructor(repositoryId, vocabularyEngine, provenanceTracker, eventBus, storage, embedding) {
@@ -283,6 +561,10 @@ var EntityManager = class {
283
561
  eventBus;
284
562
  storage;
285
563
  embedding;
564
+ /** Swap the embedding provider, e.g. after a repository re-embed changes the model or dimensionality */
565
+ setEmbeddingProvider(embedding) {
566
+ this.embedding = embedding;
567
+ }
286
568
  /** Create one or more entities with vocabulary validation, ID generation, provenance, and events */
287
569
  async create(inputs) {
288
570
  const results = [];
@@ -312,7 +594,7 @@ var EntityManager = class {
312
594
  }
313
595
  );
314
596
  const provenance = this.provenanceTracker.stampCreate();
315
- const entityEmbedding = await this.generateEmbedding(input.label, input.summary);
597
+ const entityEmbedding = await this.generateEmbedding(input.label, input.summary, input.properties ?? {}, input.entityType);
316
598
  const storedEntity = {
317
599
  id,
318
600
  slug,
@@ -358,14 +640,42 @@ var EntityManager = class {
358
640
  throw new OperationCancelledError("Entity update", hookResult.reason ?? "cancelled by hook");
359
641
  }
360
642
  const provenance = this.provenanceTracker.stampUpdate(existing.provenance);
361
- const mergedProperties = updates.properties ? { ...existing.properties, ...updates.properties } : existing.properties;
362
- const needsReembed = updates.reembed === true || updates.label !== void 0 || updates.summary !== void 0;
363
- const entityEmbedding = needsReembed ? await this.generateEmbedding(
364
- updates.label ?? existing.label,
365
- updates.summary !== void 0 ? updates.summary : existing.summary
366
- ) : void 0;
643
+ let mergedProperties = existing.properties;
644
+ if (updates.properties) {
645
+ const merged = { ...existing.properties };
646
+ for (const [key, value] of Object.entries(updates.properties)) {
647
+ if (value === null) {
648
+ delete merged[key];
649
+ } else {
650
+ merged[key] = value;
651
+ }
652
+ }
653
+ mergedProperties = merged;
654
+ }
655
+ const typeChanged = updates.entityType !== void 0 && updates.entityType !== existing.entityType;
656
+ const labelChanged = updates.label !== void 0 && updates.label !== existing.label;
657
+ let newSlug;
658
+ if (typeChanged || labelChanged) {
659
+ const nextType = updates.entityType ?? existing.entityType;
660
+ const nextLabel = updates.label ?? existing.label;
661
+ newSlug = await generateUniqueSlug(
662
+ nextType,
663
+ nextLabel,
664
+ async (candidateSlug) => {
665
+ if (candidateSlug === existing.slug) return false;
666
+ const other = await this.storage.getEntityBySlug(this.repositoryId, candidateSlug);
667
+ return other !== null;
668
+ }
669
+ );
670
+ }
671
+ const needsReembed = updates.reembed === true || updates.label !== void 0 || updates.summary !== void 0 || updates.properties !== void 0;
672
+ const nextSummary = updates.summary === void 0 ? existing.summary : updates.summary ?? 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;
367
675
  const updated = await this.storage.updateEntity(this.repositoryId, entityId, {
676
+ entityType: typeChanged ? updates.entityType : void 0,
368
677
  label: updates.label,
678
+ slug: newSlug,
369
679
  summary: updates.summary,
370
680
  properties: updates.properties ? mergedProperties : void 0,
371
681
  data: updates.data,
@@ -436,18 +746,33 @@ var EntityManager = class {
436
746
  offset: result.offset
437
747
  };
438
748
  }
439
- /** Delete an entity */
749
+ /** Delete an entity — throws EntityNotFoundError if it does not exist */
440
750
  async delete(entityId) {
441
751
  const existing = await this.storage.getEntity(this.repositoryId, entityId);
442
752
  if (!existing) {
443
753
  throw new EntityNotFoundError(entityId);
444
754
  }
445
- const hookResult = await this.eventBus.emitHook("entity:deleting", { id: entityId });
755
+ const hookResult = await this.eventBus.emitHook("entity:deleting", { ids: [entityId] });
446
756
  if (hookResult.cancelled) {
447
757
  throw new OperationCancelledError("Entity deletion", hookResult.reason ?? "cancelled by hook");
448
758
  }
449
759
  await this.storage.deleteEntity(this.repositoryId, entityId);
450
- await this.eventBus.emit("entity:deleted", { id: entityId });
760
+ await this.eventBus.emit("entity:deleted", { ids: [entityId] });
761
+ }
762
+ /** Delete multiple entities in a single batch operation */
763
+ async deleteMany(ids) {
764
+ const hookResult = await this.eventBus.emitHook("entity:deleting", { ids });
765
+ if (hookResult.cancelled) {
766
+ throw new OperationCancelledError("Entity deletion", hookResult.reason ?? "cancelled by hook");
767
+ }
768
+ const { deleted, notFound } = await this.storage.deleteEntities(this.repositoryId, ids);
769
+ if (deleted.length > 0) {
770
+ await this.eventBus.emit("entity:deleted", { ids: deleted });
771
+ }
772
+ return {
773
+ deleted,
774
+ failed: notFound.map((id) => ({ id, error: `Entity '${id}' not found` }))
775
+ };
451
776
  }
452
777
  /**
453
778
  * Re-embed a specific set of entities using the current EmbeddingProvider.
@@ -460,7 +785,12 @@ var EntityManager = class {
460
785
  const maxRetries = options?.maxRetries ?? 3;
461
786
  const storedMap = await this.storage.getEntities(this.repositoryId, entityIds);
462
787
  const entries = Array.from(storedMap.entries());
463
- 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
+ });
464
794
  let embeddings;
465
795
  let attempt = 0;
466
796
  while (true) {
@@ -562,10 +892,13 @@ var EntityManager = class {
562
892
  dimensions: this.embedding.dimensions()
563
893
  };
564
894
  }
565
- /** Generate an embedding vector from label + summary if a provider is available */
566
- 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) {
567
897
  if (!this.embedding) return void 0;
568
- const text = [label, summary ?? ""].join(" ");
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(" ");
569
902
  return this.embedding.embed(text);
570
903
  }
571
904
  };
@@ -602,57 +935,6 @@ function storedToBrief(stored) {
602
935
  };
603
936
  }
604
937
 
605
- // src/vocabulary/similarity.ts
606
- function jaroSimilarity(a, b) {
607
- if (a === b) return 1;
608
- if (a.length === 0 || b.length === 0) return 0;
609
- const matchWindow = Math.max(Math.floor(Math.max(a.length, b.length) / 2) - 1, 0);
610
- const aMatches = new Array(a.length).fill(false);
611
- const bMatches = new Array(b.length).fill(false);
612
- let matches = 0;
613
- let transpositions = 0;
614
- for (let i = 0; i < a.length; i++) {
615
- const start = Math.max(0, i - matchWindow);
616
- const end = Math.min(i + matchWindow + 1, b.length);
617
- for (let j = start; j < end; j++) {
618
- if (bMatches[j] || a[i] !== b[j]) continue;
619
- aMatches[i] = true;
620
- bMatches[j] = true;
621
- matches++;
622
- break;
623
- }
624
- }
625
- if (matches === 0) return 0;
626
- let k = 0;
627
- for (let i = 0; i < a.length; i++) {
628
- if (!aMatches[i]) continue;
629
- while (!bMatches[k]) k++;
630
- if (a[i] !== b[k]) transpositions++;
631
- k++;
632
- }
633
- return (matches / a.length + matches / b.length + (matches - transpositions / 2) / matches) / 3;
634
- }
635
- function jaroWinklerSimilarity(a, b, scalingFactor = 0.1) {
636
- const jaro = jaroSimilarity(a, b);
637
- if (jaro === 0) return 0;
638
- const prefixLength = Math.min(4, Math.min(a.length, b.length));
639
- let commonPrefix = 0;
640
- for (let i = 0; i < prefixLength; i++) {
641
- if (a[i] === b[i]) {
642
- commonPrefix++;
643
- } else {
644
- break;
645
- }
646
- }
647
- return jaro + commonPrefix * Math.min(scalingFactor, 0.25) * (1 - jaro);
648
- }
649
- function normalizeTypeName(name) {
650
- return name.toLowerCase().trim().replace(/[-\s.]+/g, "_").replace(/[^a-z0-9_]/g, "");
651
- }
652
- function toScreamingSnakeCase(name) {
653
- 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();
654
- }
655
-
656
938
  // src/relationships/RelationshipManager.ts
657
939
  var RelationshipManager = class {
658
940
  constructor(repositoryId, vocabularyEngine, provenanceTracker, eventBus, storage) {
@@ -671,6 +953,9 @@ var RelationshipManager = class {
671
953
  async create(inputs) {
672
954
  const results = [];
673
955
  for (const input of inputs) {
956
+ if (input.sourceEntityId === input.targetEntityId) {
957
+ throw new SelfReferentialRelationshipError(input.sourceEntityId, input.relationshipType);
958
+ }
674
959
  const sourceEntity = await this.storage.getEntity(this.repositoryId, input.sourceEntityId);
675
960
  if (!sourceEntity) {
676
961
  throw new EntityNotFoundError(input.sourceEntityId);
@@ -721,21 +1006,20 @@ var RelationshipManager = class {
721
1006
  }
722
1007
  return results;
723
1008
  }
724
- /** Remove a relationship by ID */
725
- async remove(relationshipId) {
726
- const existing = await this.storage.getRelationship(this.repositoryId, relationshipId);
727
- if (!existing) {
728
- throw new RelationshipNotFoundError(relationshipId);
729
- }
730
- const hookResult = await this.eventBus.emitHook("relationship:removing", { id: relationshipId });
1009
+ /** Remove one or more relationships in a single batch storage operation */
1010
+ async removeMany(ids) {
1011
+ const hookResult = await this.eventBus.emitHook("relationship:removing", { ids });
731
1012
  if (hookResult.cancelled) {
732
- throw new OperationCancelledError(
733
- "Relationship removal",
734
- hookResult.reason ?? "cancelled by hook"
735
- );
1013
+ throw new OperationCancelledError("Relationship removal", hookResult.reason ?? "cancelled by hook");
1014
+ }
1015
+ const { deleted, notFound } = await this.storage.deleteRelationships(this.repositoryId, ids);
1016
+ if (deleted.length > 0) {
1017
+ await this.eventBus.emit("relationship:removed", { ids: deleted });
736
1018
  }
737
- await this.storage.deleteRelationship(this.repositoryId, relationshipId);
738
- await this.eventBus.emit("relationship:removed", { id: relationshipId });
1019
+ return {
1020
+ removed: deleted,
1021
+ failed: notFound.map((id) => ({ id, error: `Relationship '${id}' not found` }))
1022
+ };
739
1023
  }
740
1024
  /** Get relationships for an entity with filtering */
741
1025
  async getForEntity(entityId, options) {
@@ -1475,6 +1759,7 @@ var SearchOrchestrator = class {
1475
1759
  search;
1476
1760
  embedding;
1477
1761
  eventBus;
1762
+ vocabularyEngine;
1478
1763
  defaultSimilarityThreshold;
1479
1764
  conceptSearchScanLimit;
1480
1765
  constructor(config) {
@@ -1483,9 +1768,14 @@ var SearchOrchestrator = class {
1483
1768
  this.search = config.search;
1484
1769
  this.embedding = config.embedding;
1485
1770
  this.eventBus = config.eventBus;
1771
+ this.vocabularyEngine = config.vocabularyEngine;
1486
1772
  this.defaultSimilarityThreshold = config.defaultSimilarityThreshold ?? 0.5;
1487
1773
  this.conceptSearchScanLimit = config.conceptSearchScanLimit ?? 1e3;
1488
1774
  }
1775
+ /** Swap the embedding provider, e.g. after a repository re-embed changes the model or dimensionality */
1776
+ setEmbeddingProvider(embedding) {
1777
+ this.embedding = embedding;
1778
+ }
1489
1779
  /**
1490
1780
  * Find entities by label, type, and property filters.
1491
1781
  * When a SearchProvider is available, its results are merged with
@@ -1553,7 +1843,7 @@ var SearchOrchestrator = class {
1553
1843
  if (entity.embedding && entity.embedding.length > 0) {
1554
1844
  score = this.cosineSimilarity(queryEmbedding, entity.embedding);
1555
1845
  } else {
1556
- const entityText = [entity.label, entity.summary ?? ""].join(" ");
1846
+ const entityText = await this.buildEmbeddingText(entity.entityType, entity.label, entity.summary, entity.properties);
1557
1847
  const entityEmbedding = await this.embedding.embed(entityText);
1558
1848
  score = this.cosineSimilarity(queryEmbedding, entityEmbedding);
1559
1849
  }
@@ -1612,6 +1902,22 @@ var SearchOrchestrator = class {
1612
1902
  offset
1613
1903
  };
1614
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
+ }
1615
1921
  /** Cosine similarity between two vectors */
1616
1922
  cosineSimilarity(a, b) {
1617
1923
  if (this.embedding?.similarity) {
@@ -1638,6 +1944,177 @@ var SearchOrchestrator = class {
1638
1944
  }
1639
1945
  };
1640
1946
 
1947
+ // src/validation/RepositoryValidator.ts
1948
+ var DEFAULT_TAKE = 200;
1949
+ var RepositoryValidator = class {
1950
+ constructor(repositoryId, storage, vocabularyEngine) {
1951
+ this.repositoryId = repositoryId;
1952
+ this.storage = storage;
1953
+ this.vocabularyEngine = vocabularyEngine;
1954
+ }
1955
+ repositoryId;
1956
+ storage;
1957
+ vocabularyEngine;
1958
+ async validateEntities(options) {
1959
+ const offset = options?.offset ?? 0;
1960
+ const take = options?.take ?? DEFAULT_TAKE;
1961
+ const delayMs = options?.delayBetweenChunksMs ?? 0;
1962
+ const vocabulary = await this.vocabularyEngine.getVocabulary();
1963
+ const issues = [];
1964
+ let scanned = 0;
1965
+ let skipped = 0;
1966
+ let stoppedEarly = false;
1967
+ for await (const chunk of this.storage.exportAll(this.repositoryId)) {
1968
+ if (chunk.type !== "entities") {
1969
+ break;
1970
+ }
1971
+ const data = chunk.data;
1972
+ for (const entity of data) {
1973
+ scanned++;
1974
+ const result = validateEntity(
1975
+ {
1976
+ entityType: entity.entityType,
1977
+ label: entity.label,
1978
+ properties: entity.properties
1979
+ },
1980
+ vocabulary
1981
+ );
1982
+ if (!result.valid) {
1983
+ if (skipped < offset) {
1984
+ skipped++;
1985
+ continue;
1986
+ }
1987
+ issues.push({
1988
+ entityId: entity.id,
1989
+ slug: entity.slug,
1990
+ entityType: entity.entityType,
1991
+ label: entity.label,
1992
+ errors: result.errors
1993
+ });
1994
+ if (issues.length >= take) {
1995
+ stoppedEarly = true;
1996
+ break;
1997
+ }
1998
+ }
1999
+ }
2000
+ if (stoppedEarly) break;
2001
+ if (delayMs > 0 && !chunk.isLast) {
2002
+ await new Promise((resolve) => {
2003
+ setTimeout(resolve, delayMs);
2004
+ });
2005
+ }
2006
+ }
2007
+ return {
2008
+ issues,
2009
+ scanned,
2010
+ nextOffset: offset + issues.length,
2011
+ done: !stoppedEarly
2012
+ };
2013
+ }
2014
+ async validateRelationships(options) {
2015
+ const offset = options?.offset ?? 0;
2016
+ const take = options?.take ?? DEFAULT_TAKE;
2017
+ const delayMs = options?.delayBetweenChunksMs ?? 0;
2018
+ const vocabulary = await this.vocabularyEngine.getVocabulary();
2019
+ const entityMap = /* @__PURE__ */ new Map();
2020
+ const issues = [];
2021
+ let scanned = 0;
2022
+ let skipped = 0;
2023
+ let stoppedEarly = false;
2024
+ for await (const chunk of this.storage.exportAll(this.repositoryId)) {
2025
+ if (chunk.type === "entities") {
2026
+ for (const entity of chunk.data) {
2027
+ entityMap.set(entity.id, {
2028
+ entityType: entity.entityType,
2029
+ label: entity.label,
2030
+ slug: entity.slug
2031
+ });
2032
+ }
2033
+ if (delayMs > 0 && !chunk.isLast) {
2034
+ await new Promise((resolve) => {
2035
+ setTimeout(resolve, delayMs);
2036
+ });
2037
+ }
2038
+ continue;
2039
+ }
2040
+ const data = chunk.data;
2041
+ for (const rel of data) {
2042
+ scanned++;
2043
+ const src = entityMap.get(rel.sourceEntityId);
2044
+ const tgt = entityMap.get(rel.targetEntityId);
2045
+ const errors = [];
2046
+ if (!src) {
2047
+ errors.push({
2048
+ field: "sourceEntityId",
2049
+ message: `Source entity "${rel.sourceEntityId}" does not exist in this repository`
2050
+ });
2051
+ }
2052
+ if (!tgt) {
2053
+ errors.push({
2054
+ field: "targetEntityId",
2055
+ message: `Target entity "${rel.targetEntityId}" does not exist in this repository`
2056
+ });
2057
+ }
2058
+ if (rel.sourceEntityId === rel.targetEntityId) {
2059
+ errors.push({
2060
+ field: "targetEntityId",
2061
+ message: `Self-referential relationship: source and target are the same entity "${rel.sourceEntityId}"`,
2062
+ suggestion: "Remove the relationship, or repoint it to a different entity."
2063
+ });
2064
+ }
2065
+ if (src && tgt) {
2066
+ const result = validateRelationship(
2067
+ {
2068
+ relationshipType: rel.relationshipType,
2069
+ sourceEntityId: rel.sourceEntityId,
2070
+ targetEntityId: rel.targetEntityId,
2071
+ properties: rel.properties
2072
+ },
2073
+ vocabulary,
2074
+ src.entityType,
2075
+ tgt.entityType
2076
+ );
2077
+ errors.push(...result.errors);
2078
+ }
2079
+ if (errors.length > 0) {
2080
+ if (skipped < offset) {
2081
+ skipped++;
2082
+ continue;
2083
+ }
2084
+ issues.push({
2085
+ relationshipId: rel.id,
2086
+ relationshipType: rel.relationshipType,
2087
+ sourceEntityId: rel.sourceEntityId,
2088
+ targetEntityId: rel.targetEntityId,
2089
+ sourceLabel: src?.label,
2090
+ targetLabel: tgt?.label,
2091
+ sourceEntityType: src?.entityType,
2092
+ targetEntityType: tgt?.entityType,
2093
+ errors
2094
+ });
2095
+ if (issues.length >= take) {
2096
+ stoppedEarly = true;
2097
+ break;
2098
+ }
2099
+ }
2100
+ }
2101
+ if (stoppedEarly) break;
2102
+ if (delayMs > 0 && !chunk.isLast) {
2103
+ await new Promise((resolve) => {
2104
+ setTimeout(resolve, delayMs);
2105
+ });
2106
+ }
2107
+ }
2108
+ return {
2109
+ issues,
2110
+ scanned,
2111
+ nextOffset: offset + issues.length,
2112
+ done: !stoppedEarly,
2113
+ entitiesInMap: entityMap.size
2114
+ };
2115
+ }
2116
+ };
2117
+
1641
2118
  // src/core/MemoryRepository.ts
1642
2119
  var MemoryRepository = class {
1643
2120
  repositoryId;
@@ -1650,6 +2127,8 @@ var MemoryRepository = class {
1650
2127
  relationshipManager;
1651
2128
  graphTraversal;
1652
2129
  searchOrchestrator;
2130
+ embeddingFactory;
2131
+ embedding;
1653
2132
  constructor(config) {
1654
2133
  this.repositoryId = config.repositoryId;
1655
2134
  this.storage = config.storage;
@@ -1657,6 +2136,8 @@ var MemoryRepository = class {
1657
2136
  this.vocabularyEngine = config.vocabularyEngine;
1658
2137
  this.provenanceTracker = config.provenanceTracker;
1659
2138
  this.eventBus = config.eventBus;
2139
+ this.embedding = config.embedding;
2140
+ this.embeddingFactory = config.embeddingFactory;
1660
2141
  this.entityManager = new EntityManager(
1661
2142
  config.repositoryId,
1662
2143
  config.vocabularyEngine,
@@ -1684,6 +2165,7 @@ var MemoryRepository = class {
1684
2165
  search: config.search,
1685
2166
  embedding: config.embedding,
1686
2167
  eventBus: config.eventBus,
2168
+ vocabularyEngine: config.vocabularyEngine,
1687
2169
  defaultSimilarityThreshold: config.vocabularyEngine.getGovernanceConfig().defaultSimilarityThreshold
1688
2170
  });
1689
2171
  }
@@ -1742,11 +2224,14 @@ var MemoryRepository = class {
1742
2224
  async findEntities(query) {
1743
2225
  return this.searchOrchestrator.findEntities(query);
1744
2226
  }
1745
- async deleteEntity(entityId) {
1746
- await this.entityManager.delete(entityId);
1747
- if (this.search) {
1748
- await this.search.removeEntity(this.repositoryId, entityId);
2227
+ async deleteEntities(ids) {
2228
+ const result = await this.entityManager.deleteMany(ids);
2229
+ if (this.search && result.deleted.length > 0) {
2230
+ for (const id of result.deleted) {
2231
+ await this.search.removeEntity(this.repositoryId, id);
2232
+ }
1749
2233
  }
2234
+ return result;
1750
2235
  }
1751
2236
  // ─── Re-embedding ──────────────────────────────────────────────────
1752
2237
  /** Re-embed specific entities using the current EmbeddingProvider */
@@ -1754,11 +2239,45 @@ var MemoryRepository = class {
1754
2239
  return this.entityManager.reembedEntities(entityIds);
1755
2240
  }
1756
2241
  /**
1757
- * Re-embed all entities in the repository using the current EmbeddingProvider.
2242
+ * Re-embed all entities in the repository. If `model` or `dimensions` is provided,
2243
+ * the repository's embedding configuration is updated to the new values before
2244
+ * re-embedding, so all subsequent entity writes use the new model/dimensionality.
2245
+ *
1758
2246
  * Processes in batches with optional rate limiting. Emits reembed:* events for progress tracking.
1759
2247
  * Updates repository metadata with the new model ID and dimensions on completion.
1760
2248
  */
1761
2249
  async reembedAll(options) {
2250
+ if (options?.model !== void 0 || options?.dimensions !== void 0) {
2251
+ if (!this.embeddingFactory) {
2252
+ throw new Error(
2253
+ "Cannot change embedding model or dimensions: DeepMemory was constructed without an embeddingFactory. Provide embeddingFactory in DeepMemoryConfig to enable per-repository embedding reconfiguration."
2254
+ );
2255
+ }
2256
+ const currentModel = this.embedding?.modelId();
2257
+ let currentDimensions;
2258
+ try {
2259
+ currentDimensions = this.embedding?.dimensions();
2260
+ } catch {
2261
+ currentDimensions = void 0;
2262
+ }
2263
+ const nextModel = options.model ?? currentModel;
2264
+ const nextDimensions = options.dimensions ?? currentDimensions;
2265
+ if (!nextModel || nextDimensions === void 0) {
2266
+ throw new Error(
2267
+ `Cannot rebuild embedding provider: model and dimensions must both be known. Got model="${nextModel ?? "undefined"}", dimensions=${nextDimensions ?? "undefined"}.`
2268
+ );
2269
+ }
2270
+ const newProvider = this.embeddingFactory({ model: nextModel, dimensions: nextDimensions });
2271
+ this.embedding = newProvider;
2272
+ this.entityManager.setEmbeddingProvider(newProvider);
2273
+ this.searchOrchestrator.setEmbeddingProvider(newProvider);
2274
+ await this.storage.updateRepository(this.repositoryId, {
2275
+ metadata: {
2276
+ embeddingModelId: nextModel,
2277
+ embeddingDimensions: nextDimensions
2278
+ }
2279
+ });
2280
+ }
1762
2281
  const stats = await this.getStats();
1763
2282
  await this.eventBus.emit("reembed:started", {
1764
2283
  repositoryId: this.repositoryId,
@@ -1804,8 +2323,8 @@ var MemoryRepository = class {
1804
2323
  async createRelationships(inputs) {
1805
2324
  return this.relationshipManager.create(inputs);
1806
2325
  }
1807
- async removeRelationship(relationshipId) {
1808
- return this.relationshipManager.remove(relationshipId);
2326
+ async removeRelationships(ids) {
2327
+ return this.relationshipManager.removeMany(ids);
1809
2328
  }
1810
2329
  async getRelationships(entityId, options) {
1811
2330
  return this.relationshipManager.getForEntity(entityId, options);
@@ -1991,6 +2510,34 @@ var MemoryRepository = class {
1991
2510
  async getStats() {
1992
2511
  return this.storage.getRepositoryStats(this.repositoryId);
1993
2512
  }
2513
+ // ─── Validation ────────────────────────────────────────────────────
2514
+ /**
2515
+ * Audit a window of entities against the current vocabulary. Returns a single
2516
+ * page; callers loop until `done` is true. Paging is over scanned entities,
2517
+ * not issues. Does not mutate anything.
2518
+ */
2519
+ async validateEntities(options) {
2520
+ const validator = new RepositoryValidator(
2521
+ this.repositoryId,
2522
+ this.storage,
2523
+ this.vocabularyEngine
2524
+ );
2525
+ return validator.validateEntities(options);
2526
+ }
2527
+ /**
2528
+ * Audit a window of relationships against the current vocabulary. Returns a
2529
+ * single page; callers loop until `done` is true. The full entity set is
2530
+ * loaded once per call to resolve orphan and type-mismatch checks, then
2531
+ * offset/take is applied to the relationship stream. Does not mutate anything.
2532
+ */
2533
+ async validateRelationships(options) {
2534
+ const validator = new RepositoryValidator(
2535
+ this.repositoryId,
2536
+ this.storage,
2537
+ this.vocabularyEngine
2538
+ );
2539
+ return validator.validateRelationships(options);
2540
+ }
1994
2541
  // ─── Bulk Operations ──────────────────────────────────────────────
1995
2542
  /** Delete all entities and relationships in this repository, preserving the repository and vocabulary */
1996
2543
  async deleteAllContents() {
@@ -2564,195 +3111,6 @@ function generateChangeId() {
2564
3111
  return `change_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
2565
3112
  }
2566
3113
 
2567
- // src/vocabulary/VocabularyValidator.ts
2568
- function ok() {
2569
- return { valid: true, errors: [] };
2570
- }
2571
- function fail(...errors) {
2572
- return { valid: false, errors };
2573
- }
2574
- function merge(...results) {
2575
- const errors = results.flatMap((r) => r.errors);
2576
- return { valid: errors.length === 0, errors };
2577
- }
2578
- function findClosestType(typeName, types) {
2579
- if (types.length === 0) return void 0;
2580
- const lower = typeName.toLowerCase();
2581
- const match = types.find(
2582
- (t) => t.type.toLowerCase().includes(lower) || lower.includes(t.type.toLowerCase())
2583
- );
2584
- return match?.type;
2585
- }
2586
- function validatePropertyValue(name, value, schema) {
2587
- if (value === void 0 || value === null) {
2588
- if (schema.required) {
2589
- return fail({
2590
- field: `properties.${name}`,
2591
- message: `Required property "${name}" is missing`
2592
- });
2593
- }
2594
- return ok();
2595
- }
2596
- switch (schema.type) {
2597
- case "string":
2598
- if (typeof value !== "string") {
2599
- return fail({
2600
- field: `properties.${name}`,
2601
- message: `Property "${name}" must be a string, got ${typeof value}`
2602
- });
2603
- }
2604
- break;
2605
- case "number":
2606
- if (typeof value !== "number" || Number.isNaN(value)) {
2607
- return fail({
2608
- field: `properties.${name}`,
2609
- message: `Property "${name}" must be a number, got ${typeof value}`
2610
- });
2611
- }
2612
- break;
2613
- case "boolean":
2614
- if (typeof value !== "boolean") {
2615
- return fail({
2616
- field: `properties.${name}`,
2617
- message: `Property "${name}" must be a boolean, got ${typeof value}`
2618
- });
2619
- }
2620
- break;
2621
- case "date":
2622
- if (typeof value !== "string" || Number.isNaN(Date.parse(value))) {
2623
- return fail({
2624
- field: `properties.${name}`,
2625
- message: `Property "${name}" must be a valid ISO date string`
2626
- });
2627
- }
2628
- break;
2629
- case "enum":
2630
- if (typeof value !== "string") {
2631
- return fail({
2632
- field: `properties.${name}`,
2633
- message: `Property "${name}" must be a string (enum value), got ${typeof value}`
2634
- });
2635
- }
2636
- if (schema.enumValues && !schema.enumValues.includes(value)) {
2637
- return fail({
2638
- field: `properties.${name}`,
2639
- message: `Property "${name}" must be one of: ${schema.enumValues.join(", ")}`,
2640
- suggestion: `Valid values: ${schema.enumValues.join(", ")}`
2641
- });
2642
- }
2643
- break;
2644
- }
2645
- return ok();
2646
- }
2647
- function validateProperties(properties, schemas, context) {
2648
- const results = [];
2649
- const providedProperties = properties ?? {};
2650
- for (const schema of schemas) {
2651
- const value = providedProperties[schema.name];
2652
- results.push(validatePropertyValue(schema.name, value, schema));
2653
- }
2654
- const knownNames = new Set(schemas.map((s) => s.name));
2655
- for (const key of Object.keys(providedProperties)) {
2656
- if (!knownNames.has(key)) {
2657
- results.push(
2658
- fail({
2659
- field: `properties.${key}`,
2660
- message: `Unknown property "${key}" on ${context}`,
2661
- suggestion: `Known properties: ${schemas.map((s) => s.name).join(", ") || "none"}`
2662
- })
2663
- );
2664
- }
2665
- }
2666
- return merge(...results);
2667
- }
2668
- function validateEntity(input, vocabulary) {
2669
- const entityTypeDef = vocabulary.entityTypes.find((et) => et.type === input.entityType);
2670
- if (!entityTypeDef) {
2671
- const closest = findClosestType(input.entityType, vocabulary.entityTypes);
2672
- return fail({
2673
- field: "entityType",
2674
- message: `Entity type "${input.entityType}" does not exist in the vocabulary`,
2675
- suggestion: closest ? `Did you mean "${closest}"?` : `Available types: ${vocabulary.entityTypes.map((et) => et.type).join(", ") || "none"}`
2676
- });
2677
- }
2678
- if (!input.label || input.label.trim().length === 0) {
2679
- return fail({
2680
- field: "label",
2681
- message: "Entity label is required and cannot be empty"
2682
- });
2683
- }
2684
- return validateProperties(input.properties, entityTypeDef.properties, input.entityType);
2685
- }
2686
- function validateEntityUpdate(input, entityTypeDef) {
2687
- if (input.label !== void 0 && input.label.trim().length === 0) {
2688
- return fail({
2689
- field: "label",
2690
- message: "Entity label cannot be empty"
2691
- });
2692
- }
2693
- if (input.properties) {
2694
- const results = [];
2695
- for (const [key, value] of Object.entries(input.properties)) {
2696
- const schema = entityTypeDef.properties.find((p) => p.name === key);
2697
- if (!schema) {
2698
- results.push(
2699
- fail({
2700
- field: `properties.${key}`,
2701
- message: `Unknown property "${key}" on ${entityTypeDef.type}`,
2702
- suggestion: `Known properties: ${entityTypeDef.properties.map((p) => p.name).join(", ") || "none"}`
2703
- })
2704
- );
2705
- } else {
2706
- results.push(validatePropertyValue(key, value, schema));
2707
- }
2708
- }
2709
- return merge(...results);
2710
- }
2711
- return ok();
2712
- }
2713
- function validateRelationship(input, vocabulary, sourceEntityType, targetEntityType) {
2714
- const normalizedType = toScreamingSnakeCase(input.relationshipType);
2715
- const relTypeDef = vocabulary.relationshipTypes.find(
2716
- (rt) => rt.type === normalizedType
2717
- );
2718
- if (!relTypeDef) {
2719
- const closest = findClosestType(input.relationshipType, vocabulary.relationshipTypes);
2720
- return fail({
2721
- field: "relationshipType",
2722
- message: `Relationship type "${input.relationshipType}" does not exist in the vocabulary`,
2723
- suggestion: closest ? `Did you mean "${closest}"?` : `Available types: ${vocabulary.relationshipTypes.map((rt) => rt.type).join(", ") || "none"}`
2724
- });
2725
- }
2726
- const results = [];
2727
- if (relTypeDef.allowedSourceTypes.length > 0 && !relTypeDef.allowedSourceTypes.includes(sourceEntityType)) {
2728
- results.push(
2729
- fail({
2730
- field: "sourceEntityId",
2731
- message: `Entity type "${sourceEntityType}" is not allowed as source for relationship type "${input.relationshipType}"`,
2732
- suggestion: `Allowed source types: ${relTypeDef.allowedSourceTypes.join(", ")}`
2733
- })
2734
- );
2735
- }
2736
- if (relTypeDef.allowedTargetTypes.length > 0 && !relTypeDef.allowedTargetTypes.includes(targetEntityType)) {
2737
- results.push(
2738
- fail({
2739
- field: "targetEntityId",
2740
- message: `Entity type "${targetEntityType}" is not allowed as target for relationship type "${input.relationshipType}"`,
2741
- suggestion: `Allowed target types: ${relTypeDef.allowedTargetTypes.join(", ")}`
2742
- })
2743
- );
2744
- }
2745
- if (relTypeDef.properties) {
2746
- results.push(
2747
- validateProperties(input.properties, relTypeDef.properties, input.relationshipType)
2748
- );
2749
- }
2750
- return merge(...results);
2751
- }
2752
- function getEntityTypeDef(entityType, vocabulary) {
2753
- return vocabulary.entityTypes.find((et) => et.type === entityType) ?? null;
2754
- }
2755
-
2756
3114
  // src/core/VocabularyEngine.ts
2757
3115
  var VocabularyEngine = class {
2758
3116
  repositoryId;
@@ -2814,7 +3172,7 @@ var VocabularyEngine = class {
2814
3172
  ]
2815
3173
  };
2816
3174
  }
2817
- return validateEntityUpdate(input, typeDef);
3175
+ return validateEntityUpdate(input, typeDef, vocabulary);
2818
3176
  }
2819
3177
  /** Validate a relationship creation input against the vocabulary */
2820
3178
  async validateRelationship(input, sourceEntityType, targetEntityType) {
@@ -2851,6 +3209,10 @@ var VocabularyEngine = class {
2851
3209
  }
2852
3210
  }
2853
3211
  }
3212
+ const propertySchemaErrors = this.validateProposalPropertySchemas(proposal);
3213
+ if (propertySchemaErrors) {
3214
+ return propertySchemaErrors;
3215
+ }
2854
3216
  const { result, updatedVocabulary } = processProposal(
2855
3217
  vocabulary,
2856
3218
  proposal,
@@ -2888,6 +3250,37 @@ var VocabularyEngine = class {
2888
3250
  }
2889
3251
  return { deletedEntities: 0, deletedRelationships: 0 };
2890
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
+ }
2891
3284
  getExistingTypesForProposal(proposal, vocabulary) {
2892
3285
  if (proposal.proposalType === "entity_type") {
2893
3286
  return vocabulary.entityTypes.map((et) => ({
@@ -3038,7 +3431,7 @@ var EventBus = class {
3038
3431
  };
3039
3432
 
3040
3433
  // src/portability/RepositoryExporter.ts
3041
- var LIBRARY_VERSION = true ? "0.6.1" : "0.1.0";
3434
+ var LIBRARY_VERSION = true ? "0.8.0" : "0.1.0";
3042
3435
  var RepositoryExporter = class {
3043
3436
  storage;
3044
3437
  provenance;
@@ -3688,7 +4081,9 @@ function isValidUuid(value) {
3688
4081
  var DeepMemory = class {
3689
4082
  storage;
3690
4083
  search;
3691
- embedding;
4084
+ embeddingFactory;
4085
+ defaultEmbeddingModel;
4086
+ defaultEmbeddingDimensions;
3692
4087
  /** Reserved for future distributed locking support */
3693
4088
  lock;
3694
4089
  graphTraversalProvider;
@@ -3698,12 +4093,22 @@ var DeepMemory = class {
3698
4093
  constructor(config) {
3699
4094
  this.storage = config.storage;
3700
4095
  this.search = config.search;
3701
- this.embedding = config.embedding;
4096
+ this.embeddingFactory = config.embeddingFactory;
4097
+ this.defaultEmbeddingModel = config.defaultEmbeddingModel;
4098
+ this.defaultEmbeddingDimensions = config.defaultEmbeddingDimensions;
3702
4099
  this.lock = config.lock;
3703
4100
  this.graphTraversalProvider = config.graphTraversal;
3704
4101
  this.provenance = new ProvenanceTracker(config.provenance);
3705
4102
  this.globalEventBus = new EventBus(config.provenance);
3706
4103
  }
4104
+ /**
4105
+ * Build an embedding provider for a repository given its stored model + dimensions.
4106
+ * Returns undefined when no factory is configured or when model/dimensions are missing.
4107
+ */
4108
+ buildEmbeddingForRepository(model, dimensions) {
4109
+ if (!this.embeddingFactory || !model || dimensions === void 0) return void 0;
4110
+ return this.embeddingFactory({ model, dimensions });
4111
+ }
3707
4112
  /** Initialize the storage provider and optional providers (call once before use) */
3708
4113
  async ensureInitialized() {
3709
4114
  if (!this.initialized) {
@@ -3755,16 +4160,11 @@ var DeepMemory = class {
3755
4160
  const now = (/* @__PURE__ */ new Date()).toISOString();
3756
4161
  const governanceConfig = config.governance ?? { mode: "open" };
3757
4162
  const metadata = { ...config.metadata };
3758
- if (this.embedding) {
3759
- if (!metadata.embeddingModelId) {
3760
- metadata.embeddingModelId = this.embedding.modelId();
3761
- }
3762
- if (metadata.embeddingDimensions === void 0) {
3763
- try {
3764
- metadata.embeddingDimensions = this.embedding.dimensions();
3765
- } catch {
3766
- }
3767
- }
4163
+ if (!metadata.embeddingModelId && this.defaultEmbeddingModel) {
4164
+ metadata.embeddingModelId = this.defaultEmbeddingModel;
4165
+ }
4166
+ if (metadata.embeddingDimensions === void 0 && this.defaultEmbeddingDimensions !== void 0) {
4167
+ metadata.embeddingDimensions = this.defaultEmbeddingDimensions;
3768
4168
  }
3769
4169
  await this.storage.createRepository({
3770
4170
  repositoryId,
@@ -3780,18 +4180,23 @@ var DeepMemory = class {
3780
4180
  });
3781
4181
  const vocabulary = config.vocabulary ? buildVocabulary(config.vocabulary, context.actorId) : createEmptyVocabulary(context.actorId);
3782
4182
  await this.storage.saveVocabulary(repositoryId, vocabulary);
4183
+ const embedding = this.buildEmbeddingForRepository(
4184
+ metadata.embeddingModelId,
4185
+ metadata.embeddingDimensions
4186
+ );
3783
4187
  const eventBus = new EventBus(context, repositoryId);
3784
4188
  const vocabularyEngine = new VocabularyEngine({
3785
4189
  repositoryId,
3786
4190
  storageProvider: this.storage,
3787
4191
  governanceConfig,
3788
- embeddingProvider: this.embedding
4192
+ embeddingProvider: embedding
3789
4193
  });
3790
4194
  const repo = new MemoryRepository({
3791
4195
  repositoryId,
3792
4196
  storage: this.storage,
3793
4197
  search: this.search,
3794
- embedding: this.embedding,
4198
+ embedding,
4199
+ embeddingFactory: this.embeddingFactory,
3795
4200
  graphTraversal: this.graphTraversalProvider,
3796
4201
  vocabularyEngine,
3797
4202
  provenanceTracker: this.provenance,
@@ -3813,17 +4218,22 @@ var DeepMemory = class {
3813
4218
  }
3814
4219
  const context = this.provenance.getContext();
3815
4220
  const eventBus = new EventBus(context, repositoryId);
4221
+ const embedding = this.buildEmbeddingForRepository(
4222
+ storedRepo.metadata?.embeddingModelId ?? this.defaultEmbeddingModel,
4223
+ storedRepo.metadata?.embeddingDimensions ?? this.defaultEmbeddingDimensions
4224
+ );
3816
4225
  const vocabularyEngine = new VocabularyEngine({
3817
4226
  repositoryId,
3818
4227
  storageProvider: this.storage,
3819
4228
  governanceConfig: storedRepo.governanceConfig,
3820
- embeddingProvider: this.embedding
4229
+ embeddingProvider: embedding
3821
4230
  });
3822
4231
  const repo = new MemoryRepository({
3823
4232
  repositoryId,
3824
4233
  storage: this.storage,
3825
4234
  search: this.search,
3826
- embedding: this.embedding,
4235
+ embedding,
4236
+ embeddingFactory: this.embeddingFactory,
3827
4237
  graphTraversal: this.graphTraversalProvider,
3828
4238
  vocabularyEngine,
3829
4239
  provenanceTracker: this.provenance,
@@ -4545,11 +4955,12 @@ var InMemoryStorageProvider = class {
4545
4955
  }
4546
4956
  const updated = {
4547
4957
  ...existing,
4958
+ entityType: updates.entityType ?? existing.entityType,
4548
4959
  label: updates.label ?? existing.label,
4549
- summary: updates.summary !== void 0 ? updates.summary : existing.summary,
4960
+ summary: updates.summary === void 0 ? existing.summary : updates.summary ?? void 0,
4550
4961
  properties: updates.properties ?? existing.properties,
4551
- data: updates.data !== void 0 ? updates.data : existing.data,
4552
- dataFormat: updates.dataFormat !== void 0 ? updates.dataFormat : existing.dataFormat,
4962
+ data: updates.data === void 0 ? existing.data : updates.data ?? void 0,
4963
+ dataFormat: updates.dataFormat === void 0 ? existing.dataFormat : updates.dataFormat ?? void 0,
4553
4964
  provenance: updates.provenance,
4554
4965
  embedding: updates.embedding ?? existing.embedding
4555
4966
  };
@@ -4676,6 +5087,29 @@ var InMemoryStorageProvider = class {
4676
5087
  offset
4677
5088
  };
4678
5089
  }
5090
+ async deleteEntities(repositoryId, ids) {
5091
+ const store = this.getStore(repositoryId);
5092
+ const deleted = [];
5093
+ const notFound = [];
5094
+ const deletedSet = /* @__PURE__ */ new Set();
5095
+ for (const id of ids) {
5096
+ const existing = store.entities.get(id);
5097
+ if (!existing) {
5098
+ notFound.push(id);
5099
+ continue;
5100
+ }
5101
+ store.slugIndex.delete(existing.slug);
5102
+ store.entities.delete(id);
5103
+ deleted.push(id);
5104
+ deletedSet.add(id);
5105
+ }
5106
+ for (const [relId, rel] of store.relationships) {
5107
+ if (deletedSet.has(rel.sourceEntityId) || deletedSet.has(rel.targetEntityId)) {
5108
+ store.relationships.delete(relId);
5109
+ }
5110
+ }
5111
+ return { deleted, notFound };
5112
+ }
4679
5113
  async deleteRelationship(repositoryId, relationshipId) {
4680
5114
  const store = this.getStore(repositoryId);
4681
5115
  if (!store.relationships.has(relationshipId)) {
@@ -4683,6 +5117,20 @@ var InMemoryStorageProvider = class {
4683
5117
  }
4684
5118
  store.relationships.delete(relationshipId);
4685
5119
  }
5120
+ async deleteRelationships(repositoryId, ids) {
5121
+ const store = this.getStore(repositoryId);
5122
+ const deleted = [];
5123
+ const notFound = [];
5124
+ for (const id of ids) {
5125
+ if (store.relationships.has(id)) {
5126
+ store.relationships.delete(id);
5127
+ deleted.push(id);
5128
+ } else {
5129
+ notFound.push(id);
5130
+ }
5131
+ }
5132
+ return { deleted, notFound };
5133
+ }
4686
5134
  async deleteRelationshipsByType(repositoryId, relationshipType) {
4687
5135
  const store = this.getStore(repositoryId);
4688
5136
  let deletedRelationships = 0;
@@ -5093,6 +5541,8 @@ export {
5093
5541
  RelationshipConstraintError,
5094
5542
  RelationshipNotFoundError,
5095
5543
  RepositoryNotFoundError,
5544
+ RepositoryValidator,
5545
+ SelfReferentialRelationshipError,
5096
5546
  TraversalTimeoutError,
5097
5547
  TraversalValidationError,
5098
5548
  TraversalVocabularyError,