@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/{StorageProvider-ynfpc5wz.d.ts → StorageProvider-CX4mKNph.d.cts} +11 -1
- package/dist/{StorageProvider-CBhwqavV.d.cts → StorageProvider-CrIcN06Q.d.ts} +11 -1
- package/dist/index.cjs +618 -238
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +81 -26
- package/dist/index.d.ts +81 -26
- package/dist/index.js +616 -238
- package/dist/index.js.map +1 -1
- package/dist/{portability-B-8J4nQQ.d.cts → portability-fa5sVgsP.d.cts} +120 -9
- package/dist/{portability-B-8J4nQQ.d.ts → portability-fa5sVgsP.d.ts} +120 -9
- package/dist/providers/index.cjs.map +1 -1
- package/dist/providers/index.d.cts +17 -4
- package/dist/providers/index.d.ts +17 -4
- package/dist/testing/conformance.cjs +36 -0
- package/dist/testing/conformance.cjs.map +1 -1
- package/dist/testing/conformance.d.cts +2 -2
- package/dist/testing/conformance.d.ts +2 -2
- package/dist/testing/conformance.js +36 -0
- package/dist/testing/conformance.js.map +1 -1
- package/dist/{traversal-Llalk03r.d.cts → traversal-CXqN780S.d.cts} +1 -1
- package/dist/{traversal-BdmDLMxV.d.ts → traversal-DpSdLzxn.d.ts} +1 -1
- package/dist/types/index.cjs.map +1 -1
- package/dist/types/index.d.cts +7 -7
- package/dist/types/index.d.ts +7 -7
- package/package.json +1 -1
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
|
-
|
|
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
|
|
420
|
-
|
|
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", {
|
|
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", {
|
|
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
|
|
781
|
-
async
|
|
782
|
-
const
|
|
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
|
-
|
|
790
|
-
|
|
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
|
-
|
|
794
|
-
|
|
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
|
|
1802
|
-
await this.entityManager.
|
|
1803
|
-
if (this.search) {
|
|
1804
|
-
|
|
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
|
|
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
|
|
1864
|
-
return this.relationshipManager.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
4946
|
+
summary: updates.summary === void 0 ? existing.summary : updates.summary ?? void 0,
|
|
4606
4947
|
properties: updates.properties ?? existing.properties,
|
|
4607
|
-
data: updates.data
|
|
4608
|
-
dataFormat: updates.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,
|