@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.cjs CHANGED
@@ -43,6 +43,8 @@ __export(src_exports, {
43
43
  RelationshipConstraintError: () => RelationshipConstraintError,
44
44
  RelationshipNotFoundError: () => RelationshipNotFoundError,
45
45
  RepositoryNotFoundError: () => RepositoryNotFoundError,
46
+ RepositoryValidator: () => RepositoryValidator,
47
+ SelfReferentialRelationshipError: () => SelfReferentialRelationshipError,
46
48
  TraversalTimeoutError: () => TraversalTimeoutError,
47
49
  TraversalValidationError: () => TraversalValidationError,
48
50
  TraversalVocabularyError: () => TraversalVocabularyError,
@@ -181,6 +183,20 @@ var RelationshipConstraintError = class extends DeepMemoryError {
181
183
  this.targetType = targetType;
182
184
  }
183
185
  };
186
+ var SelfReferentialRelationshipError = class extends DeepMemoryError {
187
+ entityId;
188
+ relationshipType;
189
+ constructor(entityId, relationshipType) {
190
+ super(
191
+ "SELF_REFERENTIAL_RELATIONSHIP",
192
+ `Self-referential relationship not allowed: "${relationshipType}" from entity "${entityId}" to itself`,
193
+ `Relationships must connect two different entities. Check that sourceEntityId and targetEntityId refer to distinct entities.`
194
+ );
195
+ this.name = "SelfReferentialRelationshipError";
196
+ this.entityId = entityId;
197
+ this.relationshipType = relationshipType;
198
+ }
199
+ };
184
200
  var GovernanceDeniedError = class extends DeepMemoryError {
185
201
  governanceMode;
186
202
  constructor(governanceMode, reason) {
@@ -339,6 +355,10 @@ var EntityManager = class {
339
355
  eventBus;
340
356
  storage;
341
357
  embedding;
358
+ /** Swap the embedding provider, e.g. after a repository re-embed changes the model or dimensionality */
359
+ setEmbeddingProvider(embedding) {
360
+ this.embedding = embedding;
361
+ }
342
362
  /** Create one or more entities with vocabulary validation, ID generation, provenance, and events */
343
363
  async create(inputs) {
344
364
  const results = [];
@@ -414,14 +434,41 @@ var EntityManager = class {
414
434
  throw new OperationCancelledError("Entity update", hookResult.reason ?? "cancelled by hook");
415
435
  }
416
436
  const provenance = this.provenanceTracker.stampUpdate(existing.provenance);
417
- const mergedProperties = updates.properties ? { ...existing.properties, ...updates.properties } : existing.properties;
437
+ let mergedProperties = existing.properties;
438
+ if (updates.properties) {
439
+ const merged = { ...existing.properties };
440
+ for (const [key, value] of Object.entries(updates.properties)) {
441
+ if (value === null) {
442
+ delete merged[key];
443
+ } else {
444
+ merged[key] = value;
445
+ }
446
+ }
447
+ mergedProperties = merged;
448
+ }
449
+ const typeChanged = updates.entityType !== void 0 && updates.entityType !== existing.entityType;
450
+ const labelChanged = updates.label !== void 0 && updates.label !== existing.label;
451
+ let newSlug;
452
+ if (typeChanged || labelChanged) {
453
+ const nextType = updates.entityType ?? existing.entityType;
454
+ const nextLabel = updates.label ?? existing.label;
455
+ newSlug = await generateUniqueSlug(
456
+ nextType,
457
+ nextLabel,
458
+ async (candidateSlug) => {
459
+ if (candidateSlug === existing.slug) return false;
460
+ const other = await this.storage.getEntityBySlug(this.repositoryId, candidateSlug);
461
+ return other !== null;
462
+ }
463
+ );
464
+ }
418
465
  const needsReembed = updates.reembed === true || updates.label !== void 0 || updates.summary !== void 0;
419
- const entityEmbedding = needsReembed ? await this.generateEmbedding(
420
- updates.label ?? existing.label,
421
- updates.summary !== void 0 ? updates.summary : existing.summary
422
- ) : void 0;
466
+ const nextSummary = updates.summary === void 0 ? existing.summary : updates.summary ?? void 0;
467
+ const entityEmbedding = needsReembed ? await this.generateEmbedding(updates.label ?? existing.label, nextSummary) : void 0;
423
468
  const updated = await this.storage.updateEntity(this.repositoryId, entityId, {
469
+ entityType: typeChanged ? updates.entityType : void 0,
424
470
  label: updates.label,
471
+ slug: newSlug,
425
472
  summary: updates.summary,
426
473
  properties: updates.properties ? mergedProperties : void 0,
427
474
  data: updates.data,
@@ -492,18 +539,33 @@ var EntityManager = class {
492
539
  offset: result.offset
493
540
  };
494
541
  }
495
- /** Delete an entity */
542
+ /** Delete an entity — throws EntityNotFoundError if it does not exist */
496
543
  async delete(entityId) {
497
544
  const existing = await this.storage.getEntity(this.repositoryId, entityId);
498
545
  if (!existing) {
499
546
  throw new EntityNotFoundError(entityId);
500
547
  }
501
- const hookResult = await this.eventBus.emitHook("entity:deleting", { id: entityId });
548
+ const hookResult = await this.eventBus.emitHook("entity:deleting", { ids: [entityId] });
502
549
  if (hookResult.cancelled) {
503
550
  throw new OperationCancelledError("Entity deletion", hookResult.reason ?? "cancelled by hook");
504
551
  }
505
552
  await this.storage.deleteEntity(this.repositoryId, entityId);
506
- await this.eventBus.emit("entity:deleted", { id: entityId });
553
+ await this.eventBus.emit("entity:deleted", { ids: [entityId] });
554
+ }
555
+ /** Delete multiple entities in a single batch operation */
556
+ async deleteMany(ids) {
557
+ const hookResult = await this.eventBus.emitHook("entity:deleting", { ids });
558
+ if (hookResult.cancelled) {
559
+ throw new OperationCancelledError("Entity deletion", hookResult.reason ?? "cancelled by hook");
560
+ }
561
+ const { deleted, notFound } = await this.storage.deleteEntities(this.repositoryId, ids);
562
+ if (deleted.length > 0) {
563
+ await this.eventBus.emit("entity:deleted", { ids: deleted });
564
+ }
565
+ return {
566
+ deleted,
567
+ failed: notFound.map((id) => ({ id, error: `Entity '${id}' not found` }))
568
+ };
507
569
  }
508
570
  /**
509
571
  * Re-embed a specific set of entities using the current EmbeddingProvider.
@@ -727,6 +789,9 @@ var RelationshipManager = class {
727
789
  async create(inputs) {
728
790
  const results = [];
729
791
  for (const input of inputs) {
792
+ if (input.sourceEntityId === input.targetEntityId) {
793
+ throw new SelfReferentialRelationshipError(input.sourceEntityId, input.relationshipType);
794
+ }
730
795
  const sourceEntity = await this.storage.getEntity(this.repositoryId, input.sourceEntityId);
731
796
  if (!sourceEntity) {
732
797
  throw new EntityNotFoundError(input.sourceEntityId);
@@ -777,21 +842,20 @@ var RelationshipManager = class {
777
842
  }
778
843
  return results;
779
844
  }
780
- /** Remove a relationship by ID */
781
- async remove(relationshipId) {
782
- const existing = await this.storage.getRelationship(this.repositoryId, relationshipId);
783
- if (!existing) {
784
- throw new RelationshipNotFoundError(relationshipId);
785
- }
786
- const hookResult = await this.eventBus.emitHook("relationship:removing", { id: relationshipId });
845
+ /** Remove one or more relationships in a single batch storage operation */
846
+ async removeMany(ids) {
847
+ const hookResult = await this.eventBus.emitHook("relationship:removing", { ids });
787
848
  if (hookResult.cancelled) {
788
- throw new OperationCancelledError(
789
- "Relationship removal",
790
- hookResult.reason ?? "cancelled by hook"
791
- );
849
+ throw new OperationCancelledError("Relationship removal", hookResult.reason ?? "cancelled by hook");
850
+ }
851
+ const { deleted, notFound } = await this.storage.deleteRelationships(this.repositoryId, ids);
852
+ if (deleted.length > 0) {
853
+ await this.eventBus.emit("relationship:removed", { ids: deleted });
792
854
  }
793
- await this.storage.deleteRelationship(this.repositoryId, relationshipId);
794
- await this.eventBus.emit("relationship:removed", { id: relationshipId });
855
+ return {
856
+ removed: deleted,
857
+ failed: notFound.map((id) => ({ id, error: `Relationship '${id}' not found` }))
858
+ };
795
859
  }
796
860
  /** Get relationships for an entity with filtering */
797
861
  async getForEntity(entityId, options) {
@@ -1542,6 +1606,10 @@ var SearchOrchestrator = class {
1542
1606
  this.defaultSimilarityThreshold = config.defaultSimilarityThreshold ?? 0.5;
1543
1607
  this.conceptSearchScanLimit = config.conceptSearchScanLimit ?? 1e3;
1544
1608
  }
1609
+ /** Swap the embedding provider, e.g. after a repository re-embed changes the model or dimensionality */
1610
+ setEmbeddingProvider(embedding) {
1611
+ this.embedding = embedding;
1612
+ }
1545
1613
  /**
1546
1614
  * Find entities by label, type, and property filters.
1547
1615
  * When a SearchProvider is available, its results are merged with
@@ -1694,6 +1762,381 @@ var SearchOrchestrator = class {
1694
1762
  }
1695
1763
  };
1696
1764
 
1765
+ // src/vocabulary/VocabularyValidator.ts
1766
+ function ok() {
1767
+ return { valid: true, errors: [] };
1768
+ }
1769
+ function fail(...errors) {
1770
+ return { valid: false, errors };
1771
+ }
1772
+ function merge(...results) {
1773
+ const errors = results.flatMap((r) => r.errors);
1774
+ return { valid: errors.length === 0, errors };
1775
+ }
1776
+ function findClosestType(typeName, types) {
1777
+ if (types.length === 0) return void 0;
1778
+ const lower = typeName.toLowerCase();
1779
+ const match = types.find(
1780
+ (t) => t.type.toLowerCase().includes(lower) || lower.includes(t.type.toLowerCase())
1781
+ );
1782
+ return match?.type;
1783
+ }
1784
+ function validatePropertyValue(name, value, schema) {
1785
+ if (value === void 0 || value === null) {
1786
+ if (schema.required) {
1787
+ return fail({
1788
+ field: `properties.${name}`,
1789
+ message: `Required property "${name}" is missing`
1790
+ });
1791
+ }
1792
+ return ok();
1793
+ }
1794
+ switch (schema.type) {
1795
+ case "string":
1796
+ if (typeof value !== "string") {
1797
+ return fail({
1798
+ field: `properties.${name}`,
1799
+ message: `Property "${name}" must be a string, got ${typeof value}`
1800
+ });
1801
+ }
1802
+ break;
1803
+ case "number":
1804
+ if (typeof value !== "number" || Number.isNaN(value)) {
1805
+ return fail({
1806
+ field: `properties.${name}`,
1807
+ message: `Property "${name}" must be a number, got ${typeof value}`
1808
+ });
1809
+ }
1810
+ break;
1811
+ case "boolean":
1812
+ if (typeof value !== "boolean") {
1813
+ return fail({
1814
+ field: `properties.${name}`,
1815
+ message: `Property "${name}" must be a boolean, got ${typeof value}`
1816
+ });
1817
+ }
1818
+ break;
1819
+ case "date":
1820
+ if (typeof value !== "string" || Number.isNaN(Date.parse(value))) {
1821
+ return fail({
1822
+ field: `properties.${name}`,
1823
+ message: `Property "${name}" must be a valid ISO date string`
1824
+ });
1825
+ }
1826
+ break;
1827
+ case "enum":
1828
+ if (typeof value !== "string") {
1829
+ return fail({
1830
+ field: `properties.${name}`,
1831
+ message: `Property "${name}" must be a string (enum value), got ${typeof value}`
1832
+ });
1833
+ }
1834
+ if (schema.enumValues && !schema.enumValues.includes(value)) {
1835
+ return fail({
1836
+ field: `properties.${name}`,
1837
+ message: `Property "${name}" must be one of: ${schema.enumValues.join(", ")}`,
1838
+ suggestion: `Valid values: ${schema.enumValues.join(", ")}`
1839
+ });
1840
+ }
1841
+ break;
1842
+ }
1843
+ return ok();
1844
+ }
1845
+ function validateProperties(properties, schemas) {
1846
+ const results = [];
1847
+ const providedProperties = properties ?? {};
1848
+ for (const schema of schemas) {
1849
+ const value = providedProperties[schema.name];
1850
+ results.push(validatePropertyValue(schema.name, value, schema));
1851
+ }
1852
+ if (schemas.length > 0) {
1853
+ const knownNames = new Set(schemas.map((s) => s.name));
1854
+ for (const key of Object.keys(providedProperties)) {
1855
+ if (!knownNames.has(key)) {
1856
+ results.push(fail({
1857
+ field: `properties.${key}`,
1858
+ message: `Unknown property "${key}"`,
1859
+ suggestion: `Known properties: ${schemas.map((s) => s.name).join(", ")}`
1860
+ }));
1861
+ }
1862
+ }
1863
+ }
1864
+ return merge(...results);
1865
+ }
1866
+ function validateEntity(input, vocabulary) {
1867
+ const entityTypeDef = vocabulary.entityTypes.find((et) => et.type === input.entityType);
1868
+ if (!entityTypeDef) {
1869
+ const closest = findClosestType(input.entityType, vocabulary.entityTypes);
1870
+ return fail({
1871
+ field: "entityType",
1872
+ message: `Entity type "${input.entityType}" does not exist in the vocabulary`,
1873
+ suggestion: closest ? `Did you mean "${closest}"?` : `Available types: ${vocabulary.entityTypes.map((et) => et.type).join(", ") || "none"}`
1874
+ });
1875
+ }
1876
+ if (!input.label || input.label.trim().length === 0) {
1877
+ return fail({
1878
+ field: "label",
1879
+ message: "Entity label is required and cannot be empty"
1880
+ });
1881
+ }
1882
+ return validateProperties(input.properties, entityTypeDef.properties);
1883
+ }
1884
+ function validateEntityUpdate(input, currentEntityTypeDef, vocabulary) {
1885
+ if (input.label !== void 0 && input.label.trim().length === 0) {
1886
+ return fail({
1887
+ field: "label",
1888
+ message: "Entity label cannot be empty"
1889
+ });
1890
+ }
1891
+ let targetTypeDef = currentEntityTypeDef;
1892
+ if (input.entityType !== void 0 && input.entityType !== currentEntityTypeDef.type) {
1893
+ const newTypeDef = vocabulary.entityTypes.find((et) => et.type === input.entityType);
1894
+ if (!newTypeDef) {
1895
+ const closest = findClosestType(input.entityType, vocabulary.entityTypes);
1896
+ return fail({
1897
+ field: "entityType",
1898
+ message: `Entity type "${input.entityType}" does not exist in the vocabulary`,
1899
+ suggestion: closest ? `Did you mean "${closest}"?` : `Available types: ${vocabulary.entityTypes.map((et) => et.type).join(", ") || "none"}`
1900
+ });
1901
+ }
1902
+ targetTypeDef = newTypeDef;
1903
+ }
1904
+ if (input.properties) {
1905
+ const results = [];
1906
+ const hasDefinedProps = targetTypeDef.properties.length > 0;
1907
+ for (const [key, value] of Object.entries(input.properties)) {
1908
+ if (value === null) continue;
1909
+ const schema = targetTypeDef.properties.find((p) => p.name === key);
1910
+ if (!schema) {
1911
+ if (hasDefinedProps) {
1912
+ results.push(fail({
1913
+ field: `properties.${key}`,
1914
+ message: `Unknown property "${key}"`,
1915
+ suggestion: `Known properties: ${targetTypeDef.properties.map((p) => p.name).join(", ")}`
1916
+ }));
1917
+ }
1918
+ } else {
1919
+ results.push(validatePropertyValue(key, value, schema));
1920
+ }
1921
+ }
1922
+ return merge(...results);
1923
+ }
1924
+ return ok();
1925
+ }
1926
+ function validateRelationship(input, vocabulary, sourceEntityType, targetEntityType) {
1927
+ const normalizedType = toScreamingSnakeCase(input.relationshipType);
1928
+ const relTypeDef = vocabulary.relationshipTypes.find(
1929
+ (rt) => rt.type === normalizedType
1930
+ );
1931
+ if (!relTypeDef) {
1932
+ const closest = findClosestType(input.relationshipType, vocabulary.relationshipTypes);
1933
+ return fail({
1934
+ field: "relationshipType",
1935
+ message: `Relationship type "${input.relationshipType}" does not exist in the vocabulary`,
1936
+ suggestion: closest ? `Did you mean "${closest}"?` : `Available types: ${vocabulary.relationshipTypes.map((rt) => rt.type).join(", ") || "none"}`
1937
+ });
1938
+ }
1939
+ const results = [];
1940
+ if (relTypeDef.allowedSourceTypes.length > 0 && !relTypeDef.allowedSourceTypes.includes(sourceEntityType)) {
1941
+ results.push(
1942
+ fail({
1943
+ field: "sourceEntityId",
1944
+ message: `Entity type "${sourceEntityType}" is not allowed as source for relationship type "${input.relationshipType}"`,
1945
+ suggestion: `Allowed source types: ${relTypeDef.allowedSourceTypes.join(", ")}`
1946
+ })
1947
+ );
1948
+ }
1949
+ if (relTypeDef.allowedTargetTypes.length > 0 && !relTypeDef.allowedTargetTypes.includes(targetEntityType)) {
1950
+ results.push(
1951
+ fail({
1952
+ field: "targetEntityId",
1953
+ message: `Entity type "${targetEntityType}" is not allowed as target for relationship type "${input.relationshipType}"`,
1954
+ suggestion: `Allowed target types: ${relTypeDef.allowedTargetTypes.join(", ")}`
1955
+ })
1956
+ );
1957
+ }
1958
+ if (relTypeDef.properties) {
1959
+ results.push(
1960
+ validateProperties(input.properties, relTypeDef.properties)
1961
+ );
1962
+ }
1963
+ return merge(...results);
1964
+ }
1965
+ function getEntityTypeDef(entityType, vocabulary) {
1966
+ return vocabulary.entityTypes.find((et) => et.type === entityType) ?? null;
1967
+ }
1968
+
1969
+ // src/validation/RepositoryValidator.ts
1970
+ var DEFAULT_TAKE = 200;
1971
+ var RepositoryValidator = class {
1972
+ constructor(repositoryId, storage, vocabularyEngine) {
1973
+ this.repositoryId = repositoryId;
1974
+ this.storage = storage;
1975
+ this.vocabularyEngine = vocabularyEngine;
1976
+ }
1977
+ repositoryId;
1978
+ storage;
1979
+ vocabularyEngine;
1980
+ async validateEntities(options) {
1981
+ const offset = options?.offset ?? 0;
1982
+ const take = options?.take ?? DEFAULT_TAKE;
1983
+ const delayMs = options?.delayBetweenChunksMs ?? 0;
1984
+ const vocabulary = await this.vocabularyEngine.getVocabulary();
1985
+ const issues = [];
1986
+ let scanned = 0;
1987
+ let skipped = 0;
1988
+ let stoppedEarly = false;
1989
+ for await (const chunk of this.storage.exportAll(this.repositoryId)) {
1990
+ if (chunk.type !== "entities") {
1991
+ break;
1992
+ }
1993
+ const data = chunk.data;
1994
+ for (const entity of data) {
1995
+ scanned++;
1996
+ const result = validateEntity(
1997
+ {
1998
+ entityType: entity.entityType,
1999
+ label: entity.label,
2000
+ properties: entity.properties
2001
+ },
2002
+ vocabulary
2003
+ );
2004
+ if (!result.valid) {
2005
+ if (skipped < offset) {
2006
+ skipped++;
2007
+ continue;
2008
+ }
2009
+ issues.push({
2010
+ entityId: entity.id,
2011
+ slug: entity.slug,
2012
+ entityType: entity.entityType,
2013
+ label: entity.label,
2014
+ errors: result.errors
2015
+ });
2016
+ if (issues.length >= take) {
2017
+ stoppedEarly = true;
2018
+ break;
2019
+ }
2020
+ }
2021
+ }
2022
+ if (stoppedEarly) break;
2023
+ if (delayMs > 0 && !chunk.isLast) {
2024
+ await new Promise((resolve) => {
2025
+ setTimeout(resolve, delayMs);
2026
+ });
2027
+ }
2028
+ }
2029
+ return {
2030
+ issues,
2031
+ scanned,
2032
+ nextOffset: offset + issues.length,
2033
+ done: !stoppedEarly
2034
+ };
2035
+ }
2036
+ async validateRelationships(options) {
2037
+ const offset = options?.offset ?? 0;
2038
+ const take = options?.take ?? DEFAULT_TAKE;
2039
+ const delayMs = options?.delayBetweenChunksMs ?? 0;
2040
+ const vocabulary = await this.vocabularyEngine.getVocabulary();
2041
+ const entityMap = /* @__PURE__ */ new Map();
2042
+ const issues = [];
2043
+ let scanned = 0;
2044
+ let skipped = 0;
2045
+ let stoppedEarly = false;
2046
+ for await (const chunk of this.storage.exportAll(this.repositoryId)) {
2047
+ if (chunk.type === "entities") {
2048
+ for (const entity of chunk.data) {
2049
+ entityMap.set(entity.id, {
2050
+ entityType: entity.entityType,
2051
+ label: entity.label,
2052
+ slug: entity.slug
2053
+ });
2054
+ }
2055
+ if (delayMs > 0 && !chunk.isLast) {
2056
+ await new Promise((resolve) => {
2057
+ setTimeout(resolve, delayMs);
2058
+ });
2059
+ }
2060
+ continue;
2061
+ }
2062
+ const data = chunk.data;
2063
+ for (const rel of data) {
2064
+ scanned++;
2065
+ const src = entityMap.get(rel.sourceEntityId);
2066
+ const tgt = entityMap.get(rel.targetEntityId);
2067
+ const errors = [];
2068
+ if (!src) {
2069
+ errors.push({
2070
+ field: "sourceEntityId",
2071
+ message: `Source entity "${rel.sourceEntityId}" does not exist in this repository`
2072
+ });
2073
+ }
2074
+ if (!tgt) {
2075
+ errors.push({
2076
+ field: "targetEntityId",
2077
+ message: `Target entity "${rel.targetEntityId}" does not exist in this repository`
2078
+ });
2079
+ }
2080
+ if (rel.sourceEntityId === rel.targetEntityId) {
2081
+ errors.push({
2082
+ field: "targetEntityId",
2083
+ message: `Self-referential relationship: source and target are the same entity "${rel.sourceEntityId}"`,
2084
+ suggestion: "Remove the relationship, or repoint it to a different entity."
2085
+ });
2086
+ }
2087
+ if (src && tgt) {
2088
+ const result = validateRelationship(
2089
+ {
2090
+ relationshipType: rel.relationshipType,
2091
+ sourceEntityId: rel.sourceEntityId,
2092
+ targetEntityId: rel.targetEntityId,
2093
+ properties: rel.properties
2094
+ },
2095
+ vocabulary,
2096
+ src.entityType,
2097
+ tgt.entityType
2098
+ );
2099
+ errors.push(...result.errors);
2100
+ }
2101
+ if (errors.length > 0) {
2102
+ if (skipped < offset) {
2103
+ skipped++;
2104
+ continue;
2105
+ }
2106
+ issues.push({
2107
+ relationshipId: rel.id,
2108
+ relationshipType: rel.relationshipType,
2109
+ sourceEntityId: rel.sourceEntityId,
2110
+ targetEntityId: rel.targetEntityId,
2111
+ sourceLabel: src?.label,
2112
+ targetLabel: tgt?.label,
2113
+ sourceEntityType: src?.entityType,
2114
+ targetEntityType: tgt?.entityType,
2115
+ errors
2116
+ });
2117
+ if (issues.length >= take) {
2118
+ stoppedEarly = true;
2119
+ break;
2120
+ }
2121
+ }
2122
+ }
2123
+ if (stoppedEarly) break;
2124
+ if (delayMs > 0 && !chunk.isLast) {
2125
+ await new Promise((resolve) => {
2126
+ setTimeout(resolve, delayMs);
2127
+ });
2128
+ }
2129
+ }
2130
+ return {
2131
+ issues,
2132
+ scanned,
2133
+ nextOffset: offset + issues.length,
2134
+ done: !stoppedEarly,
2135
+ entitiesInMap: entityMap.size
2136
+ };
2137
+ }
2138
+ };
2139
+
1697
2140
  // src/core/MemoryRepository.ts
1698
2141
  var MemoryRepository = class {
1699
2142
  repositoryId;
@@ -1706,6 +2149,8 @@ var MemoryRepository = class {
1706
2149
  relationshipManager;
1707
2150
  graphTraversal;
1708
2151
  searchOrchestrator;
2152
+ embeddingFactory;
2153
+ embedding;
1709
2154
  constructor(config) {
1710
2155
  this.repositoryId = config.repositoryId;
1711
2156
  this.storage = config.storage;
@@ -1713,6 +2158,8 @@ var MemoryRepository = class {
1713
2158
  this.vocabularyEngine = config.vocabularyEngine;
1714
2159
  this.provenanceTracker = config.provenanceTracker;
1715
2160
  this.eventBus = config.eventBus;
2161
+ this.embedding = config.embedding;
2162
+ this.embeddingFactory = config.embeddingFactory;
1716
2163
  this.entityManager = new EntityManager(
1717
2164
  config.repositoryId,
1718
2165
  config.vocabularyEngine,
@@ -1798,11 +2245,14 @@ var MemoryRepository = class {
1798
2245
  async findEntities(query) {
1799
2246
  return this.searchOrchestrator.findEntities(query);
1800
2247
  }
1801
- async deleteEntity(entityId) {
1802
- await this.entityManager.delete(entityId);
1803
- if (this.search) {
1804
- await this.search.removeEntity(this.repositoryId, entityId);
2248
+ async deleteEntities(ids) {
2249
+ const result = await this.entityManager.deleteMany(ids);
2250
+ if (this.search && result.deleted.length > 0) {
2251
+ for (const id of result.deleted) {
2252
+ await this.search.removeEntity(this.repositoryId, id);
2253
+ }
1805
2254
  }
2255
+ return result;
1806
2256
  }
1807
2257
  // ─── Re-embedding ──────────────────────────────────────────────────
1808
2258
  /** Re-embed specific entities using the current EmbeddingProvider */
@@ -1810,11 +2260,45 @@ var MemoryRepository = class {
1810
2260
  return this.entityManager.reembedEntities(entityIds);
1811
2261
  }
1812
2262
  /**
1813
- * Re-embed all entities in the repository using the current EmbeddingProvider.
2263
+ * Re-embed all entities in the repository. If `model` or `dimensions` is provided,
2264
+ * the repository's embedding configuration is updated to the new values before
2265
+ * re-embedding, so all subsequent entity writes use the new model/dimensionality.
2266
+ *
1814
2267
  * Processes in batches with optional rate limiting. Emits reembed:* events for progress tracking.
1815
2268
  * Updates repository metadata with the new model ID and dimensions on completion.
1816
2269
  */
1817
2270
  async reembedAll(options) {
2271
+ if (options?.model !== void 0 || options?.dimensions !== void 0) {
2272
+ if (!this.embeddingFactory) {
2273
+ throw new Error(
2274
+ "Cannot change embedding model or dimensions: DeepMemory was constructed without an embeddingFactory. Provide embeddingFactory in DeepMemoryConfig to enable per-repository embedding reconfiguration."
2275
+ );
2276
+ }
2277
+ const currentModel = this.embedding?.modelId();
2278
+ let currentDimensions;
2279
+ try {
2280
+ currentDimensions = this.embedding?.dimensions();
2281
+ } catch {
2282
+ currentDimensions = void 0;
2283
+ }
2284
+ const nextModel = options.model ?? currentModel;
2285
+ const nextDimensions = options.dimensions ?? currentDimensions;
2286
+ if (!nextModel || nextDimensions === void 0) {
2287
+ throw new Error(
2288
+ `Cannot rebuild embedding provider: model and dimensions must both be known. Got model="${nextModel ?? "undefined"}", dimensions=${nextDimensions ?? "undefined"}.`
2289
+ );
2290
+ }
2291
+ const newProvider = this.embeddingFactory({ model: nextModel, dimensions: nextDimensions });
2292
+ this.embedding = newProvider;
2293
+ this.entityManager.setEmbeddingProvider(newProvider);
2294
+ this.searchOrchestrator.setEmbeddingProvider(newProvider);
2295
+ await this.storage.updateRepository(this.repositoryId, {
2296
+ metadata: {
2297
+ embeddingModelId: nextModel,
2298
+ embeddingDimensions: nextDimensions
2299
+ }
2300
+ });
2301
+ }
1818
2302
  const stats = await this.getStats();
1819
2303
  await this.eventBus.emit("reembed:started", {
1820
2304
  repositoryId: this.repositoryId,
@@ -1860,8 +2344,8 @@ var MemoryRepository = class {
1860
2344
  async createRelationships(inputs) {
1861
2345
  return this.relationshipManager.create(inputs);
1862
2346
  }
1863
- async removeRelationship(relationshipId) {
1864
- return this.relationshipManager.remove(relationshipId);
2347
+ async removeRelationships(ids) {
2348
+ return this.relationshipManager.removeMany(ids);
1865
2349
  }
1866
2350
  async getRelationships(entityId, options) {
1867
2351
  return this.relationshipManager.getForEntity(entityId, options);
@@ -2047,6 +2531,34 @@ var MemoryRepository = class {
2047
2531
  async getStats() {
2048
2532
  return this.storage.getRepositoryStats(this.repositoryId);
2049
2533
  }
2534
+ // ─── Validation ────────────────────────────────────────────────────
2535
+ /**
2536
+ * Audit a window of entities against the current vocabulary. Returns a single
2537
+ * page; callers loop until `done` is true. Paging is over scanned entities,
2538
+ * not issues. Does not mutate anything.
2539
+ */
2540
+ async validateEntities(options) {
2541
+ const validator = new RepositoryValidator(
2542
+ this.repositoryId,
2543
+ this.storage,
2544
+ this.vocabularyEngine
2545
+ );
2546
+ return validator.validateEntities(options);
2547
+ }
2548
+ /**
2549
+ * Audit a window of relationships against the current vocabulary. Returns a
2550
+ * single page; callers loop until `done` is true. The full entity set is
2551
+ * loaded once per call to resolve orphan and type-mismatch checks, then
2552
+ * offset/take is applied to the relationship stream. Does not mutate anything.
2553
+ */
2554
+ async validateRelationships(options) {
2555
+ const validator = new RepositoryValidator(
2556
+ this.repositoryId,
2557
+ this.storage,
2558
+ this.vocabularyEngine
2559
+ );
2560
+ return validator.validateRelationships(options);
2561
+ }
2050
2562
  // ─── Bulk Operations ──────────────────────────────────────────────
2051
2563
  /** Delete all entities and relationships in this repository, preserving the repository and vocabulary */
2052
2564
  async deleteAllContents() {
@@ -2620,195 +3132,6 @@ function generateChangeId() {
2620
3132
  return `change_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
2621
3133
  }
2622
3134
 
2623
- // src/vocabulary/VocabularyValidator.ts
2624
- function ok() {
2625
- return { valid: true, errors: [] };
2626
- }
2627
- function fail(...errors) {
2628
- return { valid: false, errors };
2629
- }
2630
- function merge(...results) {
2631
- const errors = results.flatMap((r) => r.errors);
2632
- return { valid: errors.length === 0, errors };
2633
- }
2634
- function findClosestType(typeName, types) {
2635
- if (types.length === 0) return void 0;
2636
- const lower = typeName.toLowerCase();
2637
- const match = types.find(
2638
- (t) => t.type.toLowerCase().includes(lower) || lower.includes(t.type.toLowerCase())
2639
- );
2640
- return match?.type;
2641
- }
2642
- function validatePropertyValue(name, value, schema) {
2643
- if (value === void 0 || value === null) {
2644
- if (schema.required) {
2645
- return fail({
2646
- field: `properties.${name}`,
2647
- message: `Required property "${name}" is missing`
2648
- });
2649
- }
2650
- return ok();
2651
- }
2652
- switch (schema.type) {
2653
- case "string":
2654
- if (typeof value !== "string") {
2655
- return fail({
2656
- field: `properties.${name}`,
2657
- message: `Property "${name}" must be a string, got ${typeof value}`
2658
- });
2659
- }
2660
- break;
2661
- case "number":
2662
- if (typeof value !== "number" || Number.isNaN(value)) {
2663
- return fail({
2664
- field: `properties.${name}`,
2665
- message: `Property "${name}" must be a number, got ${typeof value}`
2666
- });
2667
- }
2668
- break;
2669
- case "boolean":
2670
- if (typeof value !== "boolean") {
2671
- return fail({
2672
- field: `properties.${name}`,
2673
- message: `Property "${name}" must be a boolean, got ${typeof value}`
2674
- });
2675
- }
2676
- break;
2677
- case "date":
2678
- if (typeof value !== "string" || Number.isNaN(Date.parse(value))) {
2679
- return fail({
2680
- field: `properties.${name}`,
2681
- message: `Property "${name}" must be a valid ISO date string`
2682
- });
2683
- }
2684
- break;
2685
- case "enum":
2686
- if (typeof value !== "string") {
2687
- return fail({
2688
- field: `properties.${name}`,
2689
- message: `Property "${name}" must be a string (enum value), got ${typeof value}`
2690
- });
2691
- }
2692
- if (schema.enumValues && !schema.enumValues.includes(value)) {
2693
- return fail({
2694
- field: `properties.${name}`,
2695
- message: `Property "${name}" must be one of: ${schema.enumValues.join(", ")}`,
2696
- suggestion: `Valid values: ${schema.enumValues.join(", ")}`
2697
- });
2698
- }
2699
- break;
2700
- }
2701
- return ok();
2702
- }
2703
- function validateProperties(properties, schemas, context) {
2704
- const results = [];
2705
- const providedProperties = properties ?? {};
2706
- for (const schema of schemas) {
2707
- const value = providedProperties[schema.name];
2708
- results.push(validatePropertyValue(schema.name, value, schema));
2709
- }
2710
- const knownNames = new Set(schemas.map((s) => s.name));
2711
- for (const key of Object.keys(providedProperties)) {
2712
- if (!knownNames.has(key)) {
2713
- results.push(
2714
- fail({
2715
- field: `properties.${key}`,
2716
- message: `Unknown property "${key}" on ${context}`,
2717
- suggestion: `Known properties: ${schemas.map((s) => s.name).join(", ") || "none"}`
2718
- })
2719
- );
2720
- }
2721
- }
2722
- return merge(...results);
2723
- }
2724
- function validateEntity(input, vocabulary) {
2725
- const entityTypeDef = vocabulary.entityTypes.find((et) => et.type === input.entityType);
2726
- if (!entityTypeDef) {
2727
- const closest = findClosestType(input.entityType, vocabulary.entityTypes);
2728
- return fail({
2729
- field: "entityType",
2730
- message: `Entity type "${input.entityType}" does not exist in the vocabulary`,
2731
- suggestion: closest ? `Did you mean "${closest}"?` : `Available types: ${vocabulary.entityTypes.map((et) => et.type).join(", ") || "none"}`
2732
- });
2733
- }
2734
- if (!input.label || input.label.trim().length === 0) {
2735
- return fail({
2736
- field: "label",
2737
- message: "Entity label is required and cannot be empty"
2738
- });
2739
- }
2740
- return validateProperties(input.properties, entityTypeDef.properties, input.entityType);
2741
- }
2742
- function validateEntityUpdate(input, entityTypeDef) {
2743
- if (input.label !== void 0 && input.label.trim().length === 0) {
2744
- return fail({
2745
- field: "label",
2746
- message: "Entity label cannot be empty"
2747
- });
2748
- }
2749
- if (input.properties) {
2750
- const results = [];
2751
- for (const [key, value] of Object.entries(input.properties)) {
2752
- const schema = entityTypeDef.properties.find((p) => p.name === key);
2753
- if (!schema) {
2754
- results.push(
2755
- fail({
2756
- field: `properties.${key}`,
2757
- message: `Unknown property "${key}" on ${entityTypeDef.type}`,
2758
- suggestion: `Known properties: ${entityTypeDef.properties.map((p) => p.name).join(", ") || "none"}`
2759
- })
2760
- );
2761
- } else {
2762
- results.push(validatePropertyValue(key, value, schema));
2763
- }
2764
- }
2765
- return merge(...results);
2766
- }
2767
- return ok();
2768
- }
2769
- function validateRelationship(input, vocabulary, sourceEntityType, targetEntityType) {
2770
- const normalizedType = toScreamingSnakeCase(input.relationshipType);
2771
- const relTypeDef = vocabulary.relationshipTypes.find(
2772
- (rt) => rt.type === normalizedType
2773
- );
2774
- if (!relTypeDef) {
2775
- const closest = findClosestType(input.relationshipType, vocabulary.relationshipTypes);
2776
- return fail({
2777
- field: "relationshipType",
2778
- message: `Relationship type "${input.relationshipType}" does not exist in the vocabulary`,
2779
- suggestion: closest ? `Did you mean "${closest}"?` : `Available types: ${vocabulary.relationshipTypes.map((rt) => rt.type).join(", ") || "none"}`
2780
- });
2781
- }
2782
- const results = [];
2783
- if (relTypeDef.allowedSourceTypes.length > 0 && !relTypeDef.allowedSourceTypes.includes(sourceEntityType)) {
2784
- results.push(
2785
- fail({
2786
- field: "sourceEntityId",
2787
- message: `Entity type "${sourceEntityType}" is not allowed as source for relationship type "${input.relationshipType}"`,
2788
- suggestion: `Allowed source types: ${relTypeDef.allowedSourceTypes.join(", ")}`
2789
- })
2790
- );
2791
- }
2792
- if (relTypeDef.allowedTargetTypes.length > 0 && !relTypeDef.allowedTargetTypes.includes(targetEntityType)) {
2793
- results.push(
2794
- fail({
2795
- field: "targetEntityId",
2796
- message: `Entity type "${targetEntityType}" is not allowed as target for relationship type "${input.relationshipType}"`,
2797
- suggestion: `Allowed target types: ${relTypeDef.allowedTargetTypes.join(", ")}`
2798
- })
2799
- );
2800
- }
2801
- if (relTypeDef.properties) {
2802
- results.push(
2803
- validateProperties(input.properties, relTypeDef.properties, input.relationshipType)
2804
- );
2805
- }
2806
- return merge(...results);
2807
- }
2808
- function getEntityTypeDef(entityType, vocabulary) {
2809
- return vocabulary.entityTypes.find((et) => et.type === entityType) ?? null;
2810
- }
2811
-
2812
3135
  // src/core/VocabularyEngine.ts
2813
3136
  var VocabularyEngine = class {
2814
3137
  repositoryId;
@@ -2870,7 +3193,7 @@ var VocabularyEngine = class {
2870
3193
  ]
2871
3194
  };
2872
3195
  }
2873
- return validateEntityUpdate(input, typeDef);
3196
+ return validateEntityUpdate(input, typeDef, vocabulary);
2874
3197
  }
2875
3198
  /** Validate a relationship creation input against the vocabulary */
2876
3199
  async validateRelationship(input, sourceEntityType, targetEntityType) {
@@ -3094,7 +3417,7 @@ var EventBus = class {
3094
3417
  };
3095
3418
 
3096
3419
  // src/portability/RepositoryExporter.ts
3097
- var LIBRARY_VERSION = true ? "0.6.1" : "0.1.0";
3420
+ var LIBRARY_VERSION = true ? "0.7.0" : "0.1.0";
3098
3421
  var RepositoryExporter = class {
3099
3422
  storage;
3100
3423
  provenance;
@@ -3744,7 +4067,9 @@ function isValidUuid(value) {
3744
4067
  var DeepMemory = class {
3745
4068
  storage;
3746
4069
  search;
3747
- embedding;
4070
+ embeddingFactory;
4071
+ defaultEmbeddingModel;
4072
+ defaultEmbeddingDimensions;
3748
4073
  /** Reserved for future distributed locking support */
3749
4074
  lock;
3750
4075
  graphTraversalProvider;
@@ -3754,12 +4079,22 @@ var DeepMemory = class {
3754
4079
  constructor(config) {
3755
4080
  this.storage = config.storage;
3756
4081
  this.search = config.search;
3757
- this.embedding = config.embedding;
4082
+ this.embeddingFactory = config.embeddingFactory;
4083
+ this.defaultEmbeddingModel = config.defaultEmbeddingModel;
4084
+ this.defaultEmbeddingDimensions = config.defaultEmbeddingDimensions;
3758
4085
  this.lock = config.lock;
3759
4086
  this.graphTraversalProvider = config.graphTraversal;
3760
4087
  this.provenance = new ProvenanceTracker(config.provenance);
3761
4088
  this.globalEventBus = new EventBus(config.provenance);
3762
4089
  }
4090
+ /**
4091
+ * Build an embedding provider for a repository given its stored model + dimensions.
4092
+ * Returns undefined when no factory is configured or when model/dimensions are missing.
4093
+ */
4094
+ buildEmbeddingForRepository(model, dimensions) {
4095
+ if (!this.embeddingFactory || !model || dimensions === void 0) return void 0;
4096
+ return this.embeddingFactory({ model, dimensions });
4097
+ }
3763
4098
  /** Initialize the storage provider and optional providers (call once before use) */
3764
4099
  async ensureInitialized() {
3765
4100
  if (!this.initialized) {
@@ -3811,16 +4146,11 @@ var DeepMemory = class {
3811
4146
  const now = (/* @__PURE__ */ new Date()).toISOString();
3812
4147
  const governanceConfig = config.governance ?? { mode: "open" };
3813
4148
  const metadata = { ...config.metadata };
3814
- if (this.embedding) {
3815
- if (!metadata.embeddingModelId) {
3816
- metadata.embeddingModelId = this.embedding.modelId();
3817
- }
3818
- if (metadata.embeddingDimensions === void 0) {
3819
- try {
3820
- metadata.embeddingDimensions = this.embedding.dimensions();
3821
- } catch {
3822
- }
3823
- }
4149
+ if (!metadata.embeddingModelId && this.defaultEmbeddingModel) {
4150
+ metadata.embeddingModelId = this.defaultEmbeddingModel;
4151
+ }
4152
+ if (metadata.embeddingDimensions === void 0 && this.defaultEmbeddingDimensions !== void 0) {
4153
+ metadata.embeddingDimensions = this.defaultEmbeddingDimensions;
3824
4154
  }
3825
4155
  await this.storage.createRepository({
3826
4156
  repositoryId,
@@ -3836,18 +4166,23 @@ var DeepMemory = class {
3836
4166
  });
3837
4167
  const vocabulary = config.vocabulary ? buildVocabulary(config.vocabulary, context.actorId) : createEmptyVocabulary(context.actorId);
3838
4168
  await this.storage.saveVocabulary(repositoryId, vocabulary);
4169
+ const embedding = this.buildEmbeddingForRepository(
4170
+ metadata.embeddingModelId,
4171
+ metadata.embeddingDimensions
4172
+ );
3839
4173
  const eventBus = new EventBus(context, repositoryId);
3840
4174
  const vocabularyEngine = new VocabularyEngine({
3841
4175
  repositoryId,
3842
4176
  storageProvider: this.storage,
3843
4177
  governanceConfig,
3844
- embeddingProvider: this.embedding
4178
+ embeddingProvider: embedding
3845
4179
  });
3846
4180
  const repo = new MemoryRepository({
3847
4181
  repositoryId,
3848
4182
  storage: this.storage,
3849
4183
  search: this.search,
3850
- embedding: this.embedding,
4184
+ embedding,
4185
+ embeddingFactory: this.embeddingFactory,
3851
4186
  graphTraversal: this.graphTraversalProvider,
3852
4187
  vocabularyEngine,
3853
4188
  provenanceTracker: this.provenance,
@@ -3869,17 +4204,22 @@ var DeepMemory = class {
3869
4204
  }
3870
4205
  const context = this.provenance.getContext();
3871
4206
  const eventBus = new EventBus(context, repositoryId);
4207
+ const embedding = this.buildEmbeddingForRepository(
4208
+ storedRepo.metadata?.embeddingModelId ?? this.defaultEmbeddingModel,
4209
+ storedRepo.metadata?.embeddingDimensions ?? this.defaultEmbeddingDimensions
4210
+ );
3872
4211
  const vocabularyEngine = new VocabularyEngine({
3873
4212
  repositoryId,
3874
4213
  storageProvider: this.storage,
3875
4214
  governanceConfig: storedRepo.governanceConfig,
3876
- embeddingProvider: this.embedding
4215
+ embeddingProvider: embedding
3877
4216
  });
3878
4217
  const repo = new MemoryRepository({
3879
4218
  repositoryId,
3880
4219
  storage: this.storage,
3881
4220
  search: this.search,
3882
- embedding: this.embedding,
4221
+ embedding,
4222
+ embeddingFactory: this.embeddingFactory,
3883
4223
  graphTraversal: this.graphTraversalProvider,
3884
4224
  vocabularyEngine,
3885
4225
  provenanceTracker: this.provenance,
@@ -4601,11 +4941,12 @@ var InMemoryStorageProvider = class {
4601
4941
  }
4602
4942
  const updated = {
4603
4943
  ...existing,
4944
+ entityType: updates.entityType ?? existing.entityType,
4604
4945
  label: updates.label ?? existing.label,
4605
- summary: updates.summary !== void 0 ? updates.summary : existing.summary,
4946
+ summary: updates.summary === void 0 ? existing.summary : updates.summary ?? void 0,
4606
4947
  properties: updates.properties ?? existing.properties,
4607
- data: updates.data !== void 0 ? updates.data : existing.data,
4608
- dataFormat: updates.dataFormat !== void 0 ? updates.dataFormat : existing.dataFormat,
4948
+ data: updates.data === void 0 ? existing.data : updates.data ?? void 0,
4949
+ dataFormat: updates.dataFormat === void 0 ? existing.dataFormat : updates.dataFormat ?? void 0,
4609
4950
  provenance: updates.provenance,
4610
4951
  embedding: updates.embedding ?? existing.embedding
4611
4952
  };
@@ -4732,6 +5073,29 @@ var InMemoryStorageProvider = class {
4732
5073
  offset
4733
5074
  };
4734
5075
  }
5076
+ async deleteEntities(repositoryId, ids) {
5077
+ const store = this.getStore(repositoryId);
5078
+ const deleted = [];
5079
+ const notFound = [];
5080
+ const deletedSet = /* @__PURE__ */ new Set();
5081
+ for (const id of ids) {
5082
+ const existing = store.entities.get(id);
5083
+ if (!existing) {
5084
+ notFound.push(id);
5085
+ continue;
5086
+ }
5087
+ store.slugIndex.delete(existing.slug);
5088
+ store.entities.delete(id);
5089
+ deleted.push(id);
5090
+ deletedSet.add(id);
5091
+ }
5092
+ for (const [relId, rel] of store.relationships) {
5093
+ if (deletedSet.has(rel.sourceEntityId) || deletedSet.has(rel.targetEntityId)) {
5094
+ store.relationships.delete(relId);
5095
+ }
5096
+ }
5097
+ return { deleted, notFound };
5098
+ }
4735
5099
  async deleteRelationship(repositoryId, relationshipId) {
4736
5100
  const store = this.getStore(repositoryId);
4737
5101
  if (!store.relationships.has(relationshipId)) {
@@ -4739,6 +5103,20 @@ var InMemoryStorageProvider = class {
4739
5103
  }
4740
5104
  store.relationships.delete(relationshipId);
4741
5105
  }
5106
+ async deleteRelationships(repositoryId, ids) {
5107
+ const store = this.getStore(repositoryId);
5108
+ const deleted = [];
5109
+ const notFound = [];
5110
+ for (const id of ids) {
5111
+ if (store.relationships.has(id)) {
5112
+ store.relationships.delete(id);
5113
+ deleted.push(id);
5114
+ } else {
5115
+ notFound.push(id);
5116
+ }
5117
+ }
5118
+ return { deleted, notFound };
5119
+ }
4742
5120
  async deleteRelationshipsByType(repositoryId, relationshipType) {
4743
5121
  const store = this.getStore(repositoryId);
4744
5122
  let deletedRelationships = 0;
@@ -5150,6 +5528,8 @@ var NoOpEmbeddingProvider = class {
5150
5528
  RelationshipConstraintError,
5151
5529
  RelationshipNotFoundError,
5152
5530
  RepositoryNotFoundError,
5531
+ RepositoryValidator,
5532
+ SelfReferentialRelationshipError,
5153
5533
  TraversalTimeoutError,
5154
5534
  TraversalValidationError,
5155
5535
  TraversalVocabularyError,