@utaba/deep-memory 0.6.1 → 0.7.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) {
@@ -283,6 +297,10 @@ var EntityManager = class {
283
297
  eventBus;
284
298
  storage;
285
299
  embedding;
300
+ /** Swap the embedding provider, e.g. after a repository re-embed changes the model or dimensionality */
301
+ setEmbeddingProvider(embedding) {
302
+ this.embedding = embedding;
303
+ }
286
304
  /** Create one or more entities with vocabulary validation, ID generation, provenance, and events */
287
305
  async create(inputs) {
288
306
  const results = [];
@@ -358,14 +376,41 @@ var EntityManager = class {
358
376
  throw new OperationCancelledError("Entity update", hookResult.reason ?? "cancelled by hook");
359
377
  }
360
378
  const provenance = this.provenanceTracker.stampUpdate(existing.provenance);
361
- const mergedProperties = updates.properties ? { ...existing.properties, ...updates.properties } : existing.properties;
379
+ let mergedProperties = existing.properties;
380
+ if (updates.properties) {
381
+ const merged = { ...existing.properties };
382
+ for (const [key, value] of Object.entries(updates.properties)) {
383
+ if (value === null) {
384
+ delete merged[key];
385
+ } else {
386
+ merged[key] = value;
387
+ }
388
+ }
389
+ mergedProperties = merged;
390
+ }
391
+ const typeChanged = updates.entityType !== void 0 && updates.entityType !== existing.entityType;
392
+ const labelChanged = updates.label !== void 0 && updates.label !== existing.label;
393
+ let newSlug;
394
+ if (typeChanged || labelChanged) {
395
+ const nextType = updates.entityType ?? existing.entityType;
396
+ const nextLabel = updates.label ?? existing.label;
397
+ newSlug = await generateUniqueSlug(
398
+ nextType,
399
+ nextLabel,
400
+ async (candidateSlug) => {
401
+ if (candidateSlug === existing.slug) return false;
402
+ const other = await this.storage.getEntityBySlug(this.repositoryId, candidateSlug);
403
+ return other !== null;
404
+ }
405
+ );
406
+ }
362
407
  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;
408
+ const nextSummary = updates.summary === void 0 ? existing.summary : updates.summary ?? void 0;
409
+ const entityEmbedding = needsReembed ? await this.generateEmbedding(updates.label ?? existing.label, nextSummary) : void 0;
367
410
  const updated = await this.storage.updateEntity(this.repositoryId, entityId, {
411
+ entityType: typeChanged ? updates.entityType : void 0,
368
412
  label: updates.label,
413
+ slug: newSlug,
369
414
  summary: updates.summary,
370
415
  properties: updates.properties ? mergedProperties : void 0,
371
416
  data: updates.data,
@@ -436,18 +481,33 @@ var EntityManager = class {
436
481
  offset: result.offset
437
482
  };
438
483
  }
439
- /** Delete an entity */
484
+ /** Delete an entity — throws EntityNotFoundError if it does not exist */
440
485
  async delete(entityId) {
441
486
  const existing = await this.storage.getEntity(this.repositoryId, entityId);
442
487
  if (!existing) {
443
488
  throw new EntityNotFoundError(entityId);
444
489
  }
445
- const hookResult = await this.eventBus.emitHook("entity:deleting", { id: entityId });
490
+ const hookResult = await this.eventBus.emitHook("entity:deleting", { ids: [entityId] });
446
491
  if (hookResult.cancelled) {
447
492
  throw new OperationCancelledError("Entity deletion", hookResult.reason ?? "cancelled by hook");
448
493
  }
449
494
  await this.storage.deleteEntity(this.repositoryId, entityId);
450
- await this.eventBus.emit("entity:deleted", { id: entityId });
495
+ await this.eventBus.emit("entity:deleted", { ids: [entityId] });
496
+ }
497
+ /** Delete multiple entities in a single batch operation */
498
+ async deleteMany(ids) {
499
+ const hookResult = await this.eventBus.emitHook("entity:deleting", { ids });
500
+ if (hookResult.cancelled) {
501
+ throw new OperationCancelledError("Entity deletion", hookResult.reason ?? "cancelled by hook");
502
+ }
503
+ const { deleted, notFound } = await this.storage.deleteEntities(this.repositoryId, ids);
504
+ if (deleted.length > 0) {
505
+ await this.eventBus.emit("entity:deleted", { ids: deleted });
506
+ }
507
+ return {
508
+ deleted,
509
+ failed: notFound.map((id) => ({ id, error: `Entity '${id}' not found` }))
510
+ };
451
511
  }
452
512
  /**
453
513
  * Re-embed a specific set of entities using the current EmbeddingProvider.
@@ -671,6 +731,9 @@ var RelationshipManager = class {
671
731
  async create(inputs) {
672
732
  const results = [];
673
733
  for (const input of inputs) {
734
+ if (input.sourceEntityId === input.targetEntityId) {
735
+ throw new SelfReferentialRelationshipError(input.sourceEntityId, input.relationshipType);
736
+ }
674
737
  const sourceEntity = await this.storage.getEntity(this.repositoryId, input.sourceEntityId);
675
738
  if (!sourceEntity) {
676
739
  throw new EntityNotFoundError(input.sourceEntityId);
@@ -721,21 +784,20 @@ var RelationshipManager = class {
721
784
  }
722
785
  return results;
723
786
  }
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 });
787
+ /** Remove one or more relationships in a single batch storage operation */
788
+ async removeMany(ids) {
789
+ const hookResult = await this.eventBus.emitHook("relationship:removing", { ids });
731
790
  if (hookResult.cancelled) {
732
- throw new OperationCancelledError(
733
- "Relationship removal",
734
- hookResult.reason ?? "cancelled by hook"
735
- );
791
+ throw new OperationCancelledError("Relationship removal", hookResult.reason ?? "cancelled by hook");
792
+ }
793
+ const { deleted, notFound } = await this.storage.deleteRelationships(this.repositoryId, ids);
794
+ if (deleted.length > 0) {
795
+ await this.eventBus.emit("relationship:removed", { ids: deleted });
736
796
  }
737
- await this.storage.deleteRelationship(this.repositoryId, relationshipId);
738
- await this.eventBus.emit("relationship:removed", { id: relationshipId });
797
+ return {
798
+ removed: deleted,
799
+ failed: notFound.map((id) => ({ id, error: `Relationship '${id}' not found` }))
800
+ };
739
801
  }
740
802
  /** Get relationships for an entity with filtering */
741
803
  async getForEntity(entityId, options) {
@@ -1486,6 +1548,10 @@ var SearchOrchestrator = class {
1486
1548
  this.defaultSimilarityThreshold = config.defaultSimilarityThreshold ?? 0.5;
1487
1549
  this.conceptSearchScanLimit = config.conceptSearchScanLimit ?? 1e3;
1488
1550
  }
1551
+ /** Swap the embedding provider, e.g. after a repository re-embed changes the model or dimensionality */
1552
+ setEmbeddingProvider(embedding) {
1553
+ this.embedding = embedding;
1554
+ }
1489
1555
  /**
1490
1556
  * Find entities by label, type, and property filters.
1491
1557
  * When a SearchProvider is available, its results are merged with
@@ -1638,6 +1704,381 @@ var SearchOrchestrator = class {
1638
1704
  }
1639
1705
  };
1640
1706
 
1707
+ // src/vocabulary/VocabularyValidator.ts
1708
+ function ok() {
1709
+ return { valid: true, errors: [] };
1710
+ }
1711
+ function fail(...errors) {
1712
+ return { valid: false, errors };
1713
+ }
1714
+ function merge(...results) {
1715
+ const errors = results.flatMap((r) => r.errors);
1716
+ return { valid: errors.length === 0, errors };
1717
+ }
1718
+ function findClosestType(typeName, types) {
1719
+ if (types.length === 0) return void 0;
1720
+ const lower = typeName.toLowerCase();
1721
+ const match = types.find(
1722
+ (t) => t.type.toLowerCase().includes(lower) || lower.includes(t.type.toLowerCase())
1723
+ );
1724
+ return match?.type;
1725
+ }
1726
+ function validatePropertyValue(name, value, schema) {
1727
+ if (value === void 0 || value === null) {
1728
+ if (schema.required) {
1729
+ return fail({
1730
+ field: `properties.${name}`,
1731
+ message: `Required property "${name}" is missing`
1732
+ });
1733
+ }
1734
+ return ok();
1735
+ }
1736
+ switch (schema.type) {
1737
+ case "string":
1738
+ if (typeof value !== "string") {
1739
+ return fail({
1740
+ field: `properties.${name}`,
1741
+ message: `Property "${name}" must be a string, got ${typeof value}`
1742
+ });
1743
+ }
1744
+ break;
1745
+ case "number":
1746
+ if (typeof value !== "number" || Number.isNaN(value)) {
1747
+ return fail({
1748
+ field: `properties.${name}`,
1749
+ message: `Property "${name}" must be a number, got ${typeof value}`
1750
+ });
1751
+ }
1752
+ break;
1753
+ case "boolean":
1754
+ if (typeof value !== "boolean") {
1755
+ return fail({
1756
+ field: `properties.${name}`,
1757
+ message: `Property "${name}" must be a boolean, got ${typeof value}`
1758
+ });
1759
+ }
1760
+ break;
1761
+ case "date":
1762
+ if (typeof value !== "string" || Number.isNaN(Date.parse(value))) {
1763
+ return fail({
1764
+ field: `properties.${name}`,
1765
+ message: `Property "${name}" must be a valid ISO date string`
1766
+ });
1767
+ }
1768
+ break;
1769
+ case "enum":
1770
+ if (typeof value !== "string") {
1771
+ return fail({
1772
+ field: `properties.${name}`,
1773
+ message: `Property "${name}" must be a string (enum value), got ${typeof value}`
1774
+ });
1775
+ }
1776
+ if (schema.enumValues && !schema.enumValues.includes(value)) {
1777
+ return fail({
1778
+ field: `properties.${name}`,
1779
+ message: `Property "${name}" must be one of: ${schema.enumValues.join(", ")}`,
1780
+ suggestion: `Valid values: ${schema.enumValues.join(", ")}`
1781
+ });
1782
+ }
1783
+ break;
1784
+ }
1785
+ return ok();
1786
+ }
1787
+ function validateProperties(properties, schemas) {
1788
+ const results = [];
1789
+ const providedProperties = properties ?? {};
1790
+ for (const schema of schemas) {
1791
+ const value = providedProperties[schema.name];
1792
+ results.push(validatePropertyValue(schema.name, value, schema));
1793
+ }
1794
+ if (schemas.length > 0) {
1795
+ const knownNames = new Set(schemas.map((s) => s.name));
1796
+ for (const key of Object.keys(providedProperties)) {
1797
+ if (!knownNames.has(key)) {
1798
+ results.push(fail({
1799
+ field: `properties.${key}`,
1800
+ message: `Unknown property "${key}"`,
1801
+ suggestion: `Known properties: ${schemas.map((s) => s.name).join(", ")}`
1802
+ }));
1803
+ }
1804
+ }
1805
+ }
1806
+ return merge(...results);
1807
+ }
1808
+ function validateEntity(input, vocabulary) {
1809
+ const entityTypeDef = vocabulary.entityTypes.find((et) => et.type === input.entityType);
1810
+ if (!entityTypeDef) {
1811
+ const closest = findClosestType(input.entityType, vocabulary.entityTypes);
1812
+ return fail({
1813
+ field: "entityType",
1814
+ message: `Entity type "${input.entityType}" does not exist in the vocabulary`,
1815
+ suggestion: closest ? `Did you mean "${closest}"?` : `Available types: ${vocabulary.entityTypes.map((et) => et.type).join(", ") || "none"}`
1816
+ });
1817
+ }
1818
+ if (!input.label || input.label.trim().length === 0) {
1819
+ return fail({
1820
+ field: "label",
1821
+ message: "Entity label is required and cannot be empty"
1822
+ });
1823
+ }
1824
+ return validateProperties(input.properties, entityTypeDef.properties);
1825
+ }
1826
+ function validateEntityUpdate(input, currentEntityTypeDef, vocabulary) {
1827
+ if (input.label !== void 0 && input.label.trim().length === 0) {
1828
+ return fail({
1829
+ field: "label",
1830
+ message: "Entity label cannot be empty"
1831
+ });
1832
+ }
1833
+ let targetTypeDef = currentEntityTypeDef;
1834
+ if (input.entityType !== void 0 && input.entityType !== currentEntityTypeDef.type) {
1835
+ const newTypeDef = vocabulary.entityTypes.find((et) => et.type === input.entityType);
1836
+ if (!newTypeDef) {
1837
+ const closest = findClosestType(input.entityType, vocabulary.entityTypes);
1838
+ return fail({
1839
+ field: "entityType",
1840
+ message: `Entity type "${input.entityType}" does not exist in the vocabulary`,
1841
+ suggestion: closest ? `Did you mean "${closest}"?` : `Available types: ${vocabulary.entityTypes.map((et) => et.type).join(", ") || "none"}`
1842
+ });
1843
+ }
1844
+ targetTypeDef = newTypeDef;
1845
+ }
1846
+ if (input.properties) {
1847
+ const results = [];
1848
+ const hasDefinedProps = targetTypeDef.properties.length > 0;
1849
+ for (const [key, value] of Object.entries(input.properties)) {
1850
+ if (value === null) continue;
1851
+ const schema = targetTypeDef.properties.find((p) => p.name === key);
1852
+ if (!schema) {
1853
+ if (hasDefinedProps) {
1854
+ results.push(fail({
1855
+ field: `properties.${key}`,
1856
+ message: `Unknown property "${key}"`,
1857
+ suggestion: `Known properties: ${targetTypeDef.properties.map((p) => p.name).join(", ")}`
1858
+ }));
1859
+ }
1860
+ } else {
1861
+ results.push(validatePropertyValue(key, value, schema));
1862
+ }
1863
+ }
1864
+ return merge(...results);
1865
+ }
1866
+ return ok();
1867
+ }
1868
+ function validateRelationship(input, vocabulary, sourceEntityType, targetEntityType) {
1869
+ const normalizedType = toScreamingSnakeCase(input.relationshipType);
1870
+ const relTypeDef = vocabulary.relationshipTypes.find(
1871
+ (rt) => rt.type === normalizedType
1872
+ );
1873
+ if (!relTypeDef) {
1874
+ const closest = findClosestType(input.relationshipType, vocabulary.relationshipTypes);
1875
+ return fail({
1876
+ field: "relationshipType",
1877
+ message: `Relationship type "${input.relationshipType}" does not exist in the vocabulary`,
1878
+ suggestion: closest ? `Did you mean "${closest}"?` : `Available types: ${vocabulary.relationshipTypes.map((rt) => rt.type).join(", ") || "none"}`
1879
+ });
1880
+ }
1881
+ const results = [];
1882
+ if (relTypeDef.allowedSourceTypes.length > 0 && !relTypeDef.allowedSourceTypes.includes(sourceEntityType)) {
1883
+ results.push(
1884
+ fail({
1885
+ field: "sourceEntityId",
1886
+ message: `Entity type "${sourceEntityType}" is not allowed as source for relationship type "${input.relationshipType}"`,
1887
+ suggestion: `Allowed source types: ${relTypeDef.allowedSourceTypes.join(", ")}`
1888
+ })
1889
+ );
1890
+ }
1891
+ if (relTypeDef.allowedTargetTypes.length > 0 && !relTypeDef.allowedTargetTypes.includes(targetEntityType)) {
1892
+ results.push(
1893
+ fail({
1894
+ field: "targetEntityId",
1895
+ message: `Entity type "${targetEntityType}" is not allowed as target for relationship type "${input.relationshipType}"`,
1896
+ suggestion: `Allowed target types: ${relTypeDef.allowedTargetTypes.join(", ")}`
1897
+ })
1898
+ );
1899
+ }
1900
+ if (relTypeDef.properties) {
1901
+ results.push(
1902
+ validateProperties(input.properties, relTypeDef.properties)
1903
+ );
1904
+ }
1905
+ return merge(...results);
1906
+ }
1907
+ function getEntityTypeDef(entityType, vocabulary) {
1908
+ return vocabulary.entityTypes.find((et) => et.type === entityType) ?? null;
1909
+ }
1910
+
1911
+ // src/validation/RepositoryValidator.ts
1912
+ var DEFAULT_TAKE = 200;
1913
+ var RepositoryValidator = class {
1914
+ constructor(repositoryId, storage, vocabularyEngine) {
1915
+ this.repositoryId = repositoryId;
1916
+ this.storage = storage;
1917
+ this.vocabularyEngine = vocabularyEngine;
1918
+ }
1919
+ repositoryId;
1920
+ storage;
1921
+ vocabularyEngine;
1922
+ async validateEntities(options) {
1923
+ const offset = options?.offset ?? 0;
1924
+ const take = options?.take ?? DEFAULT_TAKE;
1925
+ const delayMs = options?.delayBetweenChunksMs ?? 0;
1926
+ const vocabulary = await this.vocabularyEngine.getVocabulary();
1927
+ const issues = [];
1928
+ let scanned = 0;
1929
+ let skipped = 0;
1930
+ let stoppedEarly = false;
1931
+ for await (const chunk of this.storage.exportAll(this.repositoryId)) {
1932
+ if (chunk.type !== "entities") {
1933
+ break;
1934
+ }
1935
+ const data = chunk.data;
1936
+ for (const entity of data) {
1937
+ scanned++;
1938
+ const result = validateEntity(
1939
+ {
1940
+ entityType: entity.entityType,
1941
+ label: entity.label,
1942
+ properties: entity.properties
1943
+ },
1944
+ vocabulary
1945
+ );
1946
+ if (!result.valid) {
1947
+ if (skipped < offset) {
1948
+ skipped++;
1949
+ continue;
1950
+ }
1951
+ issues.push({
1952
+ entityId: entity.id,
1953
+ slug: entity.slug,
1954
+ entityType: entity.entityType,
1955
+ label: entity.label,
1956
+ errors: result.errors
1957
+ });
1958
+ if (issues.length >= take) {
1959
+ stoppedEarly = true;
1960
+ break;
1961
+ }
1962
+ }
1963
+ }
1964
+ if (stoppedEarly) break;
1965
+ if (delayMs > 0 && !chunk.isLast) {
1966
+ await new Promise((resolve) => {
1967
+ setTimeout(resolve, delayMs);
1968
+ });
1969
+ }
1970
+ }
1971
+ return {
1972
+ issues,
1973
+ scanned,
1974
+ nextOffset: offset + issues.length,
1975
+ done: !stoppedEarly
1976
+ };
1977
+ }
1978
+ async validateRelationships(options) {
1979
+ const offset = options?.offset ?? 0;
1980
+ const take = options?.take ?? DEFAULT_TAKE;
1981
+ const delayMs = options?.delayBetweenChunksMs ?? 0;
1982
+ const vocabulary = await this.vocabularyEngine.getVocabulary();
1983
+ const entityMap = /* @__PURE__ */ new Map();
1984
+ const issues = [];
1985
+ let scanned = 0;
1986
+ let skipped = 0;
1987
+ let stoppedEarly = false;
1988
+ for await (const chunk of this.storage.exportAll(this.repositoryId)) {
1989
+ if (chunk.type === "entities") {
1990
+ for (const entity of chunk.data) {
1991
+ entityMap.set(entity.id, {
1992
+ entityType: entity.entityType,
1993
+ label: entity.label,
1994
+ slug: entity.slug
1995
+ });
1996
+ }
1997
+ if (delayMs > 0 && !chunk.isLast) {
1998
+ await new Promise((resolve) => {
1999
+ setTimeout(resolve, delayMs);
2000
+ });
2001
+ }
2002
+ continue;
2003
+ }
2004
+ const data = chunk.data;
2005
+ for (const rel of data) {
2006
+ scanned++;
2007
+ const src = entityMap.get(rel.sourceEntityId);
2008
+ const tgt = entityMap.get(rel.targetEntityId);
2009
+ const errors = [];
2010
+ if (!src) {
2011
+ errors.push({
2012
+ field: "sourceEntityId",
2013
+ message: `Source entity "${rel.sourceEntityId}" does not exist in this repository`
2014
+ });
2015
+ }
2016
+ if (!tgt) {
2017
+ errors.push({
2018
+ field: "targetEntityId",
2019
+ message: `Target entity "${rel.targetEntityId}" does not exist in this repository`
2020
+ });
2021
+ }
2022
+ if (rel.sourceEntityId === rel.targetEntityId) {
2023
+ errors.push({
2024
+ field: "targetEntityId",
2025
+ message: `Self-referential relationship: source and target are the same entity "${rel.sourceEntityId}"`,
2026
+ suggestion: "Remove the relationship, or repoint it to a different entity."
2027
+ });
2028
+ }
2029
+ if (src && tgt) {
2030
+ const result = validateRelationship(
2031
+ {
2032
+ relationshipType: rel.relationshipType,
2033
+ sourceEntityId: rel.sourceEntityId,
2034
+ targetEntityId: rel.targetEntityId,
2035
+ properties: rel.properties
2036
+ },
2037
+ vocabulary,
2038
+ src.entityType,
2039
+ tgt.entityType
2040
+ );
2041
+ errors.push(...result.errors);
2042
+ }
2043
+ if (errors.length > 0) {
2044
+ if (skipped < offset) {
2045
+ skipped++;
2046
+ continue;
2047
+ }
2048
+ issues.push({
2049
+ relationshipId: rel.id,
2050
+ relationshipType: rel.relationshipType,
2051
+ sourceEntityId: rel.sourceEntityId,
2052
+ targetEntityId: rel.targetEntityId,
2053
+ sourceLabel: src?.label,
2054
+ targetLabel: tgt?.label,
2055
+ sourceEntityType: src?.entityType,
2056
+ targetEntityType: tgt?.entityType,
2057
+ errors
2058
+ });
2059
+ if (issues.length >= take) {
2060
+ stoppedEarly = true;
2061
+ break;
2062
+ }
2063
+ }
2064
+ }
2065
+ if (stoppedEarly) break;
2066
+ if (delayMs > 0 && !chunk.isLast) {
2067
+ await new Promise((resolve) => {
2068
+ setTimeout(resolve, delayMs);
2069
+ });
2070
+ }
2071
+ }
2072
+ return {
2073
+ issues,
2074
+ scanned,
2075
+ nextOffset: offset + issues.length,
2076
+ done: !stoppedEarly,
2077
+ entitiesInMap: entityMap.size
2078
+ };
2079
+ }
2080
+ };
2081
+
1641
2082
  // src/core/MemoryRepository.ts
1642
2083
  var MemoryRepository = class {
1643
2084
  repositoryId;
@@ -1650,6 +2091,8 @@ var MemoryRepository = class {
1650
2091
  relationshipManager;
1651
2092
  graphTraversal;
1652
2093
  searchOrchestrator;
2094
+ embeddingFactory;
2095
+ embedding;
1653
2096
  constructor(config) {
1654
2097
  this.repositoryId = config.repositoryId;
1655
2098
  this.storage = config.storage;
@@ -1657,6 +2100,8 @@ var MemoryRepository = class {
1657
2100
  this.vocabularyEngine = config.vocabularyEngine;
1658
2101
  this.provenanceTracker = config.provenanceTracker;
1659
2102
  this.eventBus = config.eventBus;
2103
+ this.embedding = config.embedding;
2104
+ this.embeddingFactory = config.embeddingFactory;
1660
2105
  this.entityManager = new EntityManager(
1661
2106
  config.repositoryId,
1662
2107
  config.vocabularyEngine,
@@ -1742,11 +2187,14 @@ var MemoryRepository = class {
1742
2187
  async findEntities(query) {
1743
2188
  return this.searchOrchestrator.findEntities(query);
1744
2189
  }
1745
- async deleteEntity(entityId) {
1746
- await this.entityManager.delete(entityId);
1747
- if (this.search) {
1748
- await this.search.removeEntity(this.repositoryId, entityId);
2190
+ async deleteEntities(ids) {
2191
+ const result = await this.entityManager.deleteMany(ids);
2192
+ if (this.search && result.deleted.length > 0) {
2193
+ for (const id of result.deleted) {
2194
+ await this.search.removeEntity(this.repositoryId, id);
2195
+ }
1749
2196
  }
2197
+ return result;
1750
2198
  }
1751
2199
  // ─── Re-embedding ──────────────────────────────────────────────────
1752
2200
  /** Re-embed specific entities using the current EmbeddingProvider */
@@ -1754,11 +2202,45 @@ var MemoryRepository = class {
1754
2202
  return this.entityManager.reembedEntities(entityIds);
1755
2203
  }
1756
2204
  /**
1757
- * Re-embed all entities in the repository using the current EmbeddingProvider.
2205
+ * Re-embed all entities in the repository. If `model` or `dimensions` is provided,
2206
+ * the repository's embedding configuration is updated to the new values before
2207
+ * re-embedding, so all subsequent entity writes use the new model/dimensionality.
2208
+ *
1758
2209
  * Processes in batches with optional rate limiting. Emits reembed:* events for progress tracking.
1759
2210
  * Updates repository metadata with the new model ID and dimensions on completion.
1760
2211
  */
1761
2212
  async reembedAll(options) {
2213
+ if (options?.model !== void 0 || options?.dimensions !== void 0) {
2214
+ if (!this.embeddingFactory) {
2215
+ throw new Error(
2216
+ "Cannot change embedding model or dimensions: DeepMemory was constructed without an embeddingFactory. Provide embeddingFactory in DeepMemoryConfig to enable per-repository embedding reconfiguration."
2217
+ );
2218
+ }
2219
+ const currentModel = this.embedding?.modelId();
2220
+ let currentDimensions;
2221
+ try {
2222
+ currentDimensions = this.embedding?.dimensions();
2223
+ } catch {
2224
+ currentDimensions = void 0;
2225
+ }
2226
+ const nextModel = options.model ?? currentModel;
2227
+ const nextDimensions = options.dimensions ?? currentDimensions;
2228
+ if (!nextModel || nextDimensions === void 0) {
2229
+ throw new Error(
2230
+ `Cannot rebuild embedding provider: model and dimensions must both be known. Got model="${nextModel ?? "undefined"}", dimensions=${nextDimensions ?? "undefined"}.`
2231
+ );
2232
+ }
2233
+ const newProvider = this.embeddingFactory({ model: nextModel, dimensions: nextDimensions });
2234
+ this.embedding = newProvider;
2235
+ this.entityManager.setEmbeddingProvider(newProvider);
2236
+ this.searchOrchestrator.setEmbeddingProvider(newProvider);
2237
+ await this.storage.updateRepository(this.repositoryId, {
2238
+ metadata: {
2239
+ embeddingModelId: nextModel,
2240
+ embeddingDimensions: nextDimensions
2241
+ }
2242
+ });
2243
+ }
1762
2244
  const stats = await this.getStats();
1763
2245
  await this.eventBus.emit("reembed:started", {
1764
2246
  repositoryId: this.repositoryId,
@@ -1804,8 +2286,8 @@ var MemoryRepository = class {
1804
2286
  async createRelationships(inputs) {
1805
2287
  return this.relationshipManager.create(inputs);
1806
2288
  }
1807
- async removeRelationship(relationshipId) {
1808
- return this.relationshipManager.remove(relationshipId);
2289
+ async removeRelationships(ids) {
2290
+ return this.relationshipManager.removeMany(ids);
1809
2291
  }
1810
2292
  async getRelationships(entityId, options) {
1811
2293
  return this.relationshipManager.getForEntity(entityId, options);
@@ -1991,6 +2473,34 @@ var MemoryRepository = class {
1991
2473
  async getStats() {
1992
2474
  return this.storage.getRepositoryStats(this.repositoryId);
1993
2475
  }
2476
+ // ─── Validation ────────────────────────────────────────────────────
2477
+ /**
2478
+ * Audit a window of entities against the current vocabulary. Returns a single
2479
+ * page; callers loop until `done` is true. Paging is over scanned entities,
2480
+ * not issues. Does not mutate anything.
2481
+ */
2482
+ async validateEntities(options) {
2483
+ const validator = new RepositoryValidator(
2484
+ this.repositoryId,
2485
+ this.storage,
2486
+ this.vocabularyEngine
2487
+ );
2488
+ return validator.validateEntities(options);
2489
+ }
2490
+ /**
2491
+ * Audit a window of relationships against the current vocabulary. Returns a
2492
+ * single page; callers loop until `done` is true. The full entity set is
2493
+ * loaded once per call to resolve orphan and type-mismatch checks, then
2494
+ * offset/take is applied to the relationship stream. Does not mutate anything.
2495
+ */
2496
+ async validateRelationships(options) {
2497
+ const validator = new RepositoryValidator(
2498
+ this.repositoryId,
2499
+ this.storage,
2500
+ this.vocabularyEngine
2501
+ );
2502
+ return validator.validateRelationships(options);
2503
+ }
1994
2504
  // ─── Bulk Operations ──────────────────────────────────────────────
1995
2505
  /** Delete all entities and relationships in this repository, preserving the repository and vocabulary */
1996
2506
  async deleteAllContents() {
@@ -2564,195 +3074,6 @@ function generateChangeId() {
2564
3074
  return `change_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
2565
3075
  }
2566
3076
 
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
3077
  // src/core/VocabularyEngine.ts
2757
3078
  var VocabularyEngine = class {
2758
3079
  repositoryId;
@@ -2814,7 +3135,7 @@ var VocabularyEngine = class {
2814
3135
  ]
2815
3136
  };
2816
3137
  }
2817
- return validateEntityUpdate(input, typeDef);
3138
+ return validateEntityUpdate(input, typeDef, vocabulary);
2818
3139
  }
2819
3140
  /** Validate a relationship creation input against the vocabulary */
2820
3141
  async validateRelationship(input, sourceEntityType, targetEntityType) {
@@ -3038,7 +3359,7 @@ var EventBus = class {
3038
3359
  };
3039
3360
 
3040
3361
  // src/portability/RepositoryExporter.ts
3041
- var LIBRARY_VERSION = true ? "0.6.1" : "0.1.0";
3362
+ var LIBRARY_VERSION = true ? "0.7.0" : "0.1.0";
3042
3363
  var RepositoryExporter = class {
3043
3364
  storage;
3044
3365
  provenance;
@@ -3688,7 +4009,9 @@ function isValidUuid(value) {
3688
4009
  var DeepMemory = class {
3689
4010
  storage;
3690
4011
  search;
3691
- embedding;
4012
+ embeddingFactory;
4013
+ defaultEmbeddingModel;
4014
+ defaultEmbeddingDimensions;
3692
4015
  /** Reserved for future distributed locking support */
3693
4016
  lock;
3694
4017
  graphTraversalProvider;
@@ -3698,12 +4021,22 @@ var DeepMemory = class {
3698
4021
  constructor(config) {
3699
4022
  this.storage = config.storage;
3700
4023
  this.search = config.search;
3701
- this.embedding = config.embedding;
4024
+ this.embeddingFactory = config.embeddingFactory;
4025
+ this.defaultEmbeddingModel = config.defaultEmbeddingModel;
4026
+ this.defaultEmbeddingDimensions = config.defaultEmbeddingDimensions;
3702
4027
  this.lock = config.lock;
3703
4028
  this.graphTraversalProvider = config.graphTraversal;
3704
4029
  this.provenance = new ProvenanceTracker(config.provenance);
3705
4030
  this.globalEventBus = new EventBus(config.provenance);
3706
4031
  }
4032
+ /**
4033
+ * Build an embedding provider for a repository given its stored model + dimensions.
4034
+ * Returns undefined when no factory is configured or when model/dimensions are missing.
4035
+ */
4036
+ buildEmbeddingForRepository(model, dimensions) {
4037
+ if (!this.embeddingFactory || !model || dimensions === void 0) return void 0;
4038
+ return this.embeddingFactory({ model, dimensions });
4039
+ }
3707
4040
  /** Initialize the storage provider and optional providers (call once before use) */
3708
4041
  async ensureInitialized() {
3709
4042
  if (!this.initialized) {
@@ -3755,16 +4088,11 @@ var DeepMemory = class {
3755
4088
  const now = (/* @__PURE__ */ new Date()).toISOString();
3756
4089
  const governanceConfig = config.governance ?? { mode: "open" };
3757
4090
  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
- }
4091
+ if (!metadata.embeddingModelId && this.defaultEmbeddingModel) {
4092
+ metadata.embeddingModelId = this.defaultEmbeddingModel;
4093
+ }
4094
+ if (metadata.embeddingDimensions === void 0 && this.defaultEmbeddingDimensions !== void 0) {
4095
+ metadata.embeddingDimensions = this.defaultEmbeddingDimensions;
3768
4096
  }
3769
4097
  await this.storage.createRepository({
3770
4098
  repositoryId,
@@ -3780,18 +4108,23 @@ var DeepMemory = class {
3780
4108
  });
3781
4109
  const vocabulary = config.vocabulary ? buildVocabulary(config.vocabulary, context.actorId) : createEmptyVocabulary(context.actorId);
3782
4110
  await this.storage.saveVocabulary(repositoryId, vocabulary);
4111
+ const embedding = this.buildEmbeddingForRepository(
4112
+ metadata.embeddingModelId,
4113
+ metadata.embeddingDimensions
4114
+ );
3783
4115
  const eventBus = new EventBus(context, repositoryId);
3784
4116
  const vocabularyEngine = new VocabularyEngine({
3785
4117
  repositoryId,
3786
4118
  storageProvider: this.storage,
3787
4119
  governanceConfig,
3788
- embeddingProvider: this.embedding
4120
+ embeddingProvider: embedding
3789
4121
  });
3790
4122
  const repo = new MemoryRepository({
3791
4123
  repositoryId,
3792
4124
  storage: this.storage,
3793
4125
  search: this.search,
3794
- embedding: this.embedding,
4126
+ embedding,
4127
+ embeddingFactory: this.embeddingFactory,
3795
4128
  graphTraversal: this.graphTraversalProvider,
3796
4129
  vocabularyEngine,
3797
4130
  provenanceTracker: this.provenance,
@@ -3813,17 +4146,22 @@ var DeepMemory = class {
3813
4146
  }
3814
4147
  const context = this.provenance.getContext();
3815
4148
  const eventBus = new EventBus(context, repositoryId);
4149
+ const embedding = this.buildEmbeddingForRepository(
4150
+ storedRepo.metadata?.embeddingModelId ?? this.defaultEmbeddingModel,
4151
+ storedRepo.metadata?.embeddingDimensions ?? this.defaultEmbeddingDimensions
4152
+ );
3816
4153
  const vocabularyEngine = new VocabularyEngine({
3817
4154
  repositoryId,
3818
4155
  storageProvider: this.storage,
3819
4156
  governanceConfig: storedRepo.governanceConfig,
3820
- embeddingProvider: this.embedding
4157
+ embeddingProvider: embedding
3821
4158
  });
3822
4159
  const repo = new MemoryRepository({
3823
4160
  repositoryId,
3824
4161
  storage: this.storage,
3825
4162
  search: this.search,
3826
- embedding: this.embedding,
4163
+ embedding,
4164
+ embeddingFactory: this.embeddingFactory,
3827
4165
  graphTraversal: this.graphTraversalProvider,
3828
4166
  vocabularyEngine,
3829
4167
  provenanceTracker: this.provenance,
@@ -4545,11 +4883,12 @@ var InMemoryStorageProvider = class {
4545
4883
  }
4546
4884
  const updated = {
4547
4885
  ...existing,
4886
+ entityType: updates.entityType ?? existing.entityType,
4548
4887
  label: updates.label ?? existing.label,
4549
- summary: updates.summary !== void 0 ? updates.summary : existing.summary,
4888
+ summary: updates.summary === void 0 ? existing.summary : updates.summary ?? void 0,
4550
4889
  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,
4890
+ data: updates.data === void 0 ? existing.data : updates.data ?? void 0,
4891
+ dataFormat: updates.dataFormat === void 0 ? existing.dataFormat : updates.dataFormat ?? void 0,
4553
4892
  provenance: updates.provenance,
4554
4893
  embedding: updates.embedding ?? existing.embedding
4555
4894
  };
@@ -4676,6 +5015,29 @@ var InMemoryStorageProvider = class {
4676
5015
  offset
4677
5016
  };
4678
5017
  }
5018
+ async deleteEntities(repositoryId, ids) {
5019
+ const store = this.getStore(repositoryId);
5020
+ const deleted = [];
5021
+ const notFound = [];
5022
+ const deletedSet = /* @__PURE__ */ new Set();
5023
+ for (const id of ids) {
5024
+ const existing = store.entities.get(id);
5025
+ if (!existing) {
5026
+ notFound.push(id);
5027
+ continue;
5028
+ }
5029
+ store.slugIndex.delete(existing.slug);
5030
+ store.entities.delete(id);
5031
+ deleted.push(id);
5032
+ deletedSet.add(id);
5033
+ }
5034
+ for (const [relId, rel] of store.relationships) {
5035
+ if (deletedSet.has(rel.sourceEntityId) || deletedSet.has(rel.targetEntityId)) {
5036
+ store.relationships.delete(relId);
5037
+ }
5038
+ }
5039
+ return { deleted, notFound };
5040
+ }
4679
5041
  async deleteRelationship(repositoryId, relationshipId) {
4680
5042
  const store = this.getStore(repositoryId);
4681
5043
  if (!store.relationships.has(relationshipId)) {
@@ -4683,6 +5045,20 @@ var InMemoryStorageProvider = class {
4683
5045
  }
4684
5046
  store.relationships.delete(relationshipId);
4685
5047
  }
5048
+ async deleteRelationships(repositoryId, ids) {
5049
+ const store = this.getStore(repositoryId);
5050
+ const deleted = [];
5051
+ const notFound = [];
5052
+ for (const id of ids) {
5053
+ if (store.relationships.has(id)) {
5054
+ store.relationships.delete(id);
5055
+ deleted.push(id);
5056
+ } else {
5057
+ notFound.push(id);
5058
+ }
5059
+ }
5060
+ return { deleted, notFound };
5061
+ }
4686
5062
  async deleteRelationshipsByType(repositoryId, relationshipType) {
4687
5063
  const store = this.getStore(repositoryId);
4688
5064
  let deletedRelationships = 0;
@@ -5093,6 +5469,8 @@ export {
5093
5469
  RelationshipConstraintError,
5094
5470
  RelationshipNotFoundError,
5095
5471
  RepositoryNotFoundError,
5472
+ RepositoryValidator,
5473
+ SelfReferentialRelationshipError,
5096
5474
  TraversalTimeoutError,
5097
5475
  TraversalValidationError,
5098
5476
  TraversalVocabularyError,