@utaba/deep-memory 0.1.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/LICENSE +190 -0
- package/README.md +224 -0
- package/dist/StorageProvider-CJjz8uBY.d.ts +56 -0
- package/dist/StorageProvider-CkFjdboX.d.cts +56 -0
- package/dist/index.cjs +4054 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +408 -0
- package/dist/index.d.ts +408 -0
- package/dist/index.js +4004 -0
- package/dist/index.js.map +1 -0
- package/dist/portability-DdlNYXGX.d.cts +897 -0
- package/dist/portability-DdlNYXGX.d.ts +897 -0
- package/dist/providers/index.cjs +19 -0
- package/dist/providers/index.cjs.map +1 -0
- package/dist/providers/index.d.cts +75 -0
- package/dist/providers/index.d.ts +75 -0
- package/dist/providers/index.js +1 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/testing/conformance.cjs +357 -0
- package/dist/testing/conformance.cjs.map +1 -0
- package/dist/testing/conformance.d.cts +12 -0
- package/dist/testing/conformance.d.ts +12 -0
- package/dist/testing/conformance.js +332 -0
- package/dist/testing/conformance.js.map +1 -0
- package/dist/types/index.cjs +19 -0
- package/dist/types/index.cjs.map +1 -0
- package/dist/types/index.d.cts +121 -0
- package/dist/types/index.d.ts +121 -0
- package/dist/types/index.js +1 -0
- package/dist/types/index.js.map +1 -0
- package/package.json +96 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,4054 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var src_exports = {};
|
|
22
|
+
__export(src_exports, {
|
|
23
|
+
DeepMemory: () => DeepMemory,
|
|
24
|
+
DeepMemoryError: () => DeepMemoryError,
|
|
25
|
+
DuplicateEntityError: () => DuplicateEntityError,
|
|
26
|
+
DuplicateRelationshipError: () => DuplicateRelationshipError,
|
|
27
|
+
DuplicateRepositoryError: () => DuplicateRepositoryError,
|
|
28
|
+
EmbeddingProviderRequiredError: () => EmbeddingProviderRequiredError,
|
|
29
|
+
EntityNotFoundError: () => EntityNotFoundError,
|
|
30
|
+
ExportError: () => ExportError,
|
|
31
|
+
GovernanceDeniedError: () => GovernanceDeniedError,
|
|
32
|
+
ImportError: () => ImportError,
|
|
33
|
+
InMemorySearchProvider: () => InMemorySearchProvider,
|
|
34
|
+
InMemoryStorageProvider: () => InMemoryStorageProvider,
|
|
35
|
+
InvalidInputError: () => InvalidInputError,
|
|
36
|
+
MemoryRepository: () => MemoryRepository,
|
|
37
|
+
NoOpEmbeddingProvider: () => NoOpEmbeddingProvider,
|
|
38
|
+
OperationCancelledError: () => OperationCancelledError,
|
|
39
|
+
ProviderError: () => ProviderError,
|
|
40
|
+
RelationshipConstraintError: () => RelationshipConstraintError,
|
|
41
|
+
RelationshipNotFoundError: () => RelationshipNotFoundError,
|
|
42
|
+
RepositoryNotFoundError: () => RepositoryNotFoundError,
|
|
43
|
+
VocabularyValidationError: () => VocabularyValidationError,
|
|
44
|
+
generateId: () => generateId,
|
|
45
|
+
isValidUuid: () => isValidUuid,
|
|
46
|
+
matchesPropertyFilters: () => matchesPropertyFilters
|
|
47
|
+
});
|
|
48
|
+
module.exports = __toCommonJS(src_exports);
|
|
49
|
+
|
|
50
|
+
// src/core/errors.ts
|
|
51
|
+
var DeepMemoryError = class extends Error {
|
|
52
|
+
code;
|
|
53
|
+
suggestion;
|
|
54
|
+
constructor(code, message, suggestion) {
|
|
55
|
+
super(message);
|
|
56
|
+
this.name = "DeepMemoryError";
|
|
57
|
+
this.code = code;
|
|
58
|
+
this.suggestion = suggestion;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
var InvalidInputError = class extends DeepMemoryError {
|
|
62
|
+
field;
|
|
63
|
+
constructor(field, message, suggestion) {
|
|
64
|
+
super(
|
|
65
|
+
"INVALID_INPUT",
|
|
66
|
+
message,
|
|
67
|
+
suggestion ?? `Check the value of "${field}" and try again.`
|
|
68
|
+
);
|
|
69
|
+
this.name = "InvalidInputError";
|
|
70
|
+
this.field = field;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
var EntityNotFoundError = class extends DeepMemoryError {
|
|
74
|
+
id;
|
|
75
|
+
slug;
|
|
76
|
+
constructor(idOrSlug, slug) {
|
|
77
|
+
super(
|
|
78
|
+
"ENTITY_NOT_FOUND",
|
|
79
|
+
`Entity "${idOrSlug}" not found`,
|
|
80
|
+
`Check the entity ID or slug is correct. Use findEntities() to search by label if unknown.`
|
|
81
|
+
);
|
|
82
|
+
this.name = "EntityNotFoundError";
|
|
83
|
+
this.id = idOrSlug;
|
|
84
|
+
this.slug = slug;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
var DuplicateEntityError = class extends DeepMemoryError {
|
|
88
|
+
id;
|
|
89
|
+
constructor(id) {
|
|
90
|
+
super(
|
|
91
|
+
"ENTITY_ALREADY_EXISTS",
|
|
92
|
+
`Entity "${id}" already exists`,
|
|
93
|
+
`Use updateEntity() to modify an existing entity, or omit id to auto-generate a unique one.`
|
|
94
|
+
);
|
|
95
|
+
this.name = "DuplicateEntityError";
|
|
96
|
+
this.id = id;
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
var RelationshipNotFoundError = class extends DeepMemoryError {
|
|
100
|
+
relationshipId;
|
|
101
|
+
constructor(relationshipId) {
|
|
102
|
+
super(
|
|
103
|
+
"RELATIONSHIP_NOT_FOUND",
|
|
104
|
+
`Relationship "${relationshipId}" not found`,
|
|
105
|
+
`Check the relationship ID is correct. Use getRelationships() to list relationships for an entity.`
|
|
106
|
+
);
|
|
107
|
+
this.name = "RelationshipNotFoundError";
|
|
108
|
+
this.relationshipId = relationshipId;
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
var DuplicateRelationshipError = class extends DeepMemoryError {
|
|
112
|
+
relationshipId;
|
|
113
|
+
constructor(relationshipId) {
|
|
114
|
+
super(
|
|
115
|
+
"RELATIONSHIP_ALREADY_EXISTS",
|
|
116
|
+
`Relationship "${relationshipId}" already exists`,
|
|
117
|
+
`Omit relationshipId to auto-generate a unique one, or use a different explicit ID.`
|
|
118
|
+
);
|
|
119
|
+
this.name = "DuplicateRelationshipError";
|
|
120
|
+
this.relationshipId = relationshipId;
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
var RepositoryNotFoundError = class extends DeepMemoryError {
|
|
124
|
+
repositoryId;
|
|
125
|
+
constructor(repositoryId) {
|
|
126
|
+
super(
|
|
127
|
+
"REPOSITORY_NOT_FOUND",
|
|
128
|
+
`Repository "${repositoryId}" not found`,
|
|
129
|
+
`Use listRepositories() to see available repositories, or createRepository() to create a new one.`
|
|
130
|
+
);
|
|
131
|
+
this.name = "RepositoryNotFoundError";
|
|
132
|
+
this.repositoryId = repositoryId;
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
var DuplicateRepositoryError = class extends DeepMemoryError {
|
|
136
|
+
repositoryId;
|
|
137
|
+
constructor(repositoryId) {
|
|
138
|
+
super(
|
|
139
|
+
"REPOSITORY_ALREADY_EXISTS",
|
|
140
|
+
`Repository "${repositoryId}" already exists`,
|
|
141
|
+
`Use openRepository() to access an existing repository, or choose a different ID.`
|
|
142
|
+
);
|
|
143
|
+
this.name = "DuplicateRepositoryError";
|
|
144
|
+
this.repositoryId = repositoryId;
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
var VocabularyValidationError = class extends DeepMemoryError {
|
|
148
|
+
errors;
|
|
149
|
+
constructor(errors) {
|
|
150
|
+
const errorMsg = errors.map((e) => e.message).join("; ");
|
|
151
|
+
const suggestions = errors.filter((e) => e.suggestion).map((e) => e.suggestion);
|
|
152
|
+
super(
|
|
153
|
+
"VOCABULARY_VALIDATION_FAILED",
|
|
154
|
+
`Vocabulary validation failed: ${errorMsg}`,
|
|
155
|
+
suggestions.length > 0 ? suggestions.join(" ") : `Check the repository vocabulary with getVocabulary() to see valid types and properties.`
|
|
156
|
+
);
|
|
157
|
+
this.name = "VocabularyValidationError";
|
|
158
|
+
this.errors = errors;
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
var RelationshipConstraintError = class extends DeepMemoryError {
|
|
162
|
+
relationshipType;
|
|
163
|
+
sourceType;
|
|
164
|
+
targetType;
|
|
165
|
+
constructor(relationshipType, message, sourceType, targetType) {
|
|
166
|
+
super(
|
|
167
|
+
"RELATIONSHIP_CONSTRAINT_FAILED",
|
|
168
|
+
message,
|
|
169
|
+
`Check the vocabulary for allowed source/target types on "${relationshipType}".`
|
|
170
|
+
);
|
|
171
|
+
this.name = "RelationshipConstraintError";
|
|
172
|
+
this.relationshipType = relationshipType;
|
|
173
|
+
this.sourceType = sourceType;
|
|
174
|
+
this.targetType = targetType;
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
var GovernanceDeniedError = class extends DeepMemoryError {
|
|
178
|
+
governanceMode;
|
|
179
|
+
constructor(governanceMode, reason) {
|
|
180
|
+
super(
|
|
181
|
+
"GOVERNANCE_DENIED",
|
|
182
|
+
`Governance denied: ${reason}`,
|
|
183
|
+
governanceMode === "locked" ? `The vocabulary is locked. Change governance mode to "managed" or "open" to allow modifications.` : `The operation was denied by governance rules. Review the governance configuration.`
|
|
184
|
+
);
|
|
185
|
+
this.name = "GovernanceDeniedError";
|
|
186
|
+
this.governanceMode = governanceMode;
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
var OperationCancelledError = class extends DeepMemoryError {
|
|
190
|
+
operation;
|
|
191
|
+
reason;
|
|
192
|
+
constructor(operation, reason) {
|
|
193
|
+
super(
|
|
194
|
+
"OPERATION_CANCELLED",
|
|
195
|
+
`${operation} cancelled: ${reason}`,
|
|
196
|
+
`A pre-mutation hook cancelled this operation. Review registered hooks if this is unexpected.`
|
|
197
|
+
);
|
|
198
|
+
this.name = "OperationCancelledError";
|
|
199
|
+
this.operation = operation;
|
|
200
|
+
this.reason = reason;
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
var EmbeddingProviderRequiredError = class extends DeepMemoryError {
|
|
204
|
+
constructor() {
|
|
205
|
+
super(
|
|
206
|
+
"EMBEDDING_PROVIDER_REQUIRED",
|
|
207
|
+
"EmbeddingProvider required: no embedding provider is configured",
|
|
208
|
+
`Provide an EmbeddingProvider in the DeepMemory config to use semantic search (searchByConcept).`
|
|
209
|
+
);
|
|
210
|
+
this.name = "EmbeddingProviderRequiredError";
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
var ImportError = class extends DeepMemoryError {
|
|
214
|
+
constructor(message, suggestion) {
|
|
215
|
+
super(
|
|
216
|
+
"IMPORT_ERROR",
|
|
217
|
+
message,
|
|
218
|
+
suggestion ?? `Verify the archive format and check that the target repository is accessible.`
|
|
219
|
+
);
|
|
220
|
+
this.name = "ImportError";
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
var ExportError = class extends DeepMemoryError {
|
|
224
|
+
constructor(message, suggestion) {
|
|
225
|
+
super(
|
|
226
|
+
"EXPORT_ERROR",
|
|
227
|
+
message,
|
|
228
|
+
suggestion ?? `Verify the repository exists and the storage provider is accessible.`
|
|
229
|
+
);
|
|
230
|
+
this.name = "ExportError";
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
var ProviderError = class extends DeepMemoryError {
|
|
234
|
+
constructor(message, suggestion) {
|
|
235
|
+
super(
|
|
236
|
+
"PROVIDER_ERROR",
|
|
237
|
+
message,
|
|
238
|
+
suggestion ?? `Check provider configuration and connectivity.`
|
|
239
|
+
);
|
|
240
|
+
this.name = "ProviderError";
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
// src/entities/IdGenerator.ts
|
|
245
|
+
function slugify(text) {
|
|
246
|
+
return text.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
247
|
+
}
|
|
248
|
+
function generateSlug(entityType, label) {
|
|
249
|
+
const slug = slugify(label);
|
|
250
|
+
return `${entityType}:${slug || "unnamed"}`;
|
|
251
|
+
}
|
|
252
|
+
async function generateUniqueSlug(entityType, label, existsCheck) {
|
|
253
|
+
const baseSlug = generateSlug(entityType, label);
|
|
254
|
+
if (!await existsCheck(baseSlug)) {
|
|
255
|
+
return baseSlug;
|
|
256
|
+
}
|
|
257
|
+
for (let i = 2; i <= 100; i++) {
|
|
258
|
+
const candidateSlug = `${baseSlug}-${i}`;
|
|
259
|
+
if (!await existsCheck(candidateSlug)) {
|
|
260
|
+
return candidateSlug;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
const randomSuffix = Math.random().toString(36).slice(2, 8);
|
|
264
|
+
return `${baseSlug}-${randomSuffix}`;
|
|
265
|
+
}
|
|
266
|
+
function generateEntityId() {
|
|
267
|
+
return generateId();
|
|
268
|
+
}
|
|
269
|
+
function generateRelationshipId() {
|
|
270
|
+
return generateId();
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// src/entities/EntityManager.ts
|
|
274
|
+
var EntityManager = class {
|
|
275
|
+
constructor(repositoryId, vocabularyEngine, provenanceTracker, eventBus, storage, embedding) {
|
|
276
|
+
this.repositoryId = repositoryId;
|
|
277
|
+
this.vocabularyEngine = vocabularyEngine;
|
|
278
|
+
this.provenanceTracker = provenanceTracker;
|
|
279
|
+
this.eventBus = eventBus;
|
|
280
|
+
this.storage = storage;
|
|
281
|
+
this.embedding = embedding;
|
|
282
|
+
}
|
|
283
|
+
/** Create a new entity with vocabulary validation, ID generation, provenance, and events */
|
|
284
|
+
async create(input) {
|
|
285
|
+
const validation = await this.vocabularyEngine.validateEntity(input);
|
|
286
|
+
if (!validation.valid) {
|
|
287
|
+
const errorMsg = validation.errors.map((e) => e.message).join("; ");
|
|
288
|
+
const suggestions = validation.errors.filter((e) => e.suggestion).map((e) => e.suggestion);
|
|
289
|
+
await this.eventBus.emit("validation:failed", {
|
|
290
|
+
operation: "createEntity",
|
|
291
|
+
error: errorMsg,
|
|
292
|
+
suggestions
|
|
293
|
+
});
|
|
294
|
+
throw new VocabularyValidationError(validation.errors);
|
|
295
|
+
}
|
|
296
|
+
const hookResult = await this.eventBus.emitHook("entity:creating", { input });
|
|
297
|
+
if (hookResult.cancelled) {
|
|
298
|
+
throw new OperationCancelledError("Entity creation", hookResult.reason ?? "cancelled by hook");
|
|
299
|
+
}
|
|
300
|
+
const id = input.id ?? generateEntityId();
|
|
301
|
+
const slug = await generateUniqueSlug(
|
|
302
|
+
input.entityType,
|
|
303
|
+
input.label,
|
|
304
|
+
async (candidateSlug) => {
|
|
305
|
+
const existing = await this.storage.getEntityBySlug(this.repositoryId, candidateSlug);
|
|
306
|
+
return existing !== null;
|
|
307
|
+
}
|
|
308
|
+
);
|
|
309
|
+
const provenance = this.provenanceTracker.stampCreate();
|
|
310
|
+
const entityEmbedding = await this.generateEmbedding(input.label, input.summary);
|
|
311
|
+
const storedEntity = {
|
|
312
|
+
id,
|
|
313
|
+
slug,
|
|
314
|
+
entityType: input.entityType,
|
|
315
|
+
label: input.label,
|
|
316
|
+
summary: input.summary,
|
|
317
|
+
properties: input.properties ?? {},
|
|
318
|
+
data: input.data,
|
|
319
|
+
dataFormat: input.dataFormat,
|
|
320
|
+
provenance,
|
|
321
|
+
embedding: entityEmbedding
|
|
322
|
+
};
|
|
323
|
+
const created = await this.storage.createEntity(this.repositoryId, storedEntity);
|
|
324
|
+
const entity = storedToEntity(created);
|
|
325
|
+
await this.eventBus.emit("entity:created", { entity });
|
|
326
|
+
return entity;
|
|
327
|
+
}
|
|
328
|
+
/** Update an existing entity */
|
|
329
|
+
async update(entityId, updates) {
|
|
330
|
+
const existing = await this.storage.getEntity(this.repositoryId, entityId);
|
|
331
|
+
if (!existing) {
|
|
332
|
+
throw new EntityNotFoundError(entityId);
|
|
333
|
+
}
|
|
334
|
+
const validation = await this.vocabularyEngine.validateEntityUpdate(
|
|
335
|
+
updates,
|
|
336
|
+
existing.entityType
|
|
337
|
+
);
|
|
338
|
+
if (!validation.valid) {
|
|
339
|
+
await this.eventBus.emit("validation:failed", {
|
|
340
|
+
operation: "updateEntity",
|
|
341
|
+
error: validation.errors.map((e) => e.message).join("; "),
|
|
342
|
+
suggestions: validation.errors.filter((e) => e.suggestion).map((e) => e.suggestion)
|
|
343
|
+
});
|
|
344
|
+
throw new VocabularyValidationError(validation.errors);
|
|
345
|
+
}
|
|
346
|
+
const hookResult = await this.eventBus.emitHook("entity:updating", {
|
|
347
|
+
id: entityId,
|
|
348
|
+
updates
|
|
349
|
+
});
|
|
350
|
+
if (hookResult.cancelled) {
|
|
351
|
+
throw new OperationCancelledError("Entity update", hookResult.reason ?? "cancelled by hook");
|
|
352
|
+
}
|
|
353
|
+
const provenance = this.provenanceTracker.stampUpdate(existing.provenance);
|
|
354
|
+
const mergedProperties = updates.properties ? { ...existing.properties, ...updates.properties } : existing.properties;
|
|
355
|
+
const needsReembed = updates.reembed === true || updates.label !== void 0 || updates.summary !== void 0;
|
|
356
|
+
const entityEmbedding = needsReembed ? await this.generateEmbedding(
|
|
357
|
+
updates.label ?? existing.label,
|
|
358
|
+
updates.summary !== void 0 ? updates.summary : existing.summary
|
|
359
|
+
) : void 0;
|
|
360
|
+
const updated = await this.storage.updateEntity(this.repositoryId, entityId, {
|
|
361
|
+
label: updates.label,
|
|
362
|
+
summary: updates.summary,
|
|
363
|
+
properties: updates.properties ? mergedProperties : void 0,
|
|
364
|
+
data: updates.data,
|
|
365
|
+
dataFormat: updates.dataFormat,
|
|
366
|
+
provenance,
|
|
367
|
+
embedding: entityEmbedding
|
|
368
|
+
});
|
|
369
|
+
const entity = storedToEntity(updated);
|
|
370
|
+
await this.eventBus.emit("entity:updated", { entity });
|
|
371
|
+
return entity;
|
|
372
|
+
}
|
|
373
|
+
/** Get a single entity with configurable detail level */
|
|
374
|
+
async get(entityId, detailLevel = "full") {
|
|
375
|
+
const stored = await this.storage.getEntity(this.repositoryId, entityId);
|
|
376
|
+
if (!stored) return null;
|
|
377
|
+
switch (detailLevel) {
|
|
378
|
+
case "brief":
|
|
379
|
+
return storedToBrief(stored);
|
|
380
|
+
case "summary":
|
|
381
|
+
return storedToSummary(stored);
|
|
382
|
+
case "full":
|
|
383
|
+
default:
|
|
384
|
+
return storedToEntity(stored);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
/** Get an entity by its slug */
|
|
388
|
+
async getBySlug(slug, detailLevel = "full") {
|
|
389
|
+
const stored = await this.storage.getEntityBySlug(this.repositoryId, slug);
|
|
390
|
+
if (!stored) return null;
|
|
391
|
+
switch (detailLevel) {
|
|
392
|
+
case "brief":
|
|
393
|
+
return storedToBrief(stored);
|
|
394
|
+
case "summary":
|
|
395
|
+
return storedToSummary(stored);
|
|
396
|
+
case "full":
|
|
397
|
+
default:
|
|
398
|
+
return storedToEntity(stored);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
/** Get multiple entities in a single call (max 50, brief or summary only) */
|
|
402
|
+
async getMany(entityIds, detailLevel = "summary") {
|
|
403
|
+
const ids = entityIds.slice(0, 50);
|
|
404
|
+
const storedMap = await this.storage.getEntities(this.repositoryId, ids);
|
|
405
|
+
const result = /* @__PURE__ */ new Map();
|
|
406
|
+
for (const [id, stored] of storedMap) {
|
|
407
|
+
result.set(
|
|
408
|
+
id,
|
|
409
|
+
detailLevel === "brief" ? storedToBrief(stored) : storedToSummary(stored)
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
return result;
|
|
413
|
+
}
|
|
414
|
+
/** Find entities by search criteria */
|
|
415
|
+
async find(query) {
|
|
416
|
+
const storageQuery = {
|
|
417
|
+
searchTerm: query.searchTerm,
|
|
418
|
+
entityTypes: query.entityTypes,
|
|
419
|
+
properties: query.properties,
|
|
420
|
+
limit: Math.min(query.limit ?? 10, 50),
|
|
421
|
+
offset: query.offset ?? 0
|
|
422
|
+
};
|
|
423
|
+
const result = await this.storage.findEntities(this.repositoryId, storageQuery);
|
|
424
|
+
return {
|
|
425
|
+
items: result.items.map(storedToSummary),
|
|
426
|
+
total: result.total,
|
|
427
|
+
hasMore: result.hasMore,
|
|
428
|
+
limit: result.limit,
|
|
429
|
+
offset: result.offset
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
/** Delete an entity */
|
|
433
|
+
async delete(entityId) {
|
|
434
|
+
const existing = await this.storage.getEntity(this.repositoryId, entityId);
|
|
435
|
+
if (!existing) {
|
|
436
|
+
throw new EntityNotFoundError(entityId);
|
|
437
|
+
}
|
|
438
|
+
const hookResult = await this.eventBus.emitHook("entity:deleting", { id: entityId });
|
|
439
|
+
if (hookResult.cancelled) {
|
|
440
|
+
throw new OperationCancelledError("Entity deletion", hookResult.reason ?? "cancelled by hook");
|
|
441
|
+
}
|
|
442
|
+
await this.storage.deleteEntity(this.repositoryId, entityId);
|
|
443
|
+
await this.eventBus.emit("entity:deleted", { id: entityId });
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Re-embed a specific set of entities using the current EmbeddingProvider.
|
|
447
|
+
* Retries embedBatch up to maxRetries times with exponential backoff on failure.
|
|
448
|
+
*/
|
|
449
|
+
async reembedEntities(entityIds, options) {
|
|
450
|
+
if (!this.embedding) {
|
|
451
|
+
throw new EmbeddingProviderRequiredError();
|
|
452
|
+
}
|
|
453
|
+
const maxRetries = options?.maxRetries ?? 3;
|
|
454
|
+
const storedMap = await this.storage.getEntities(this.repositoryId, entityIds);
|
|
455
|
+
const entries = Array.from(storedMap.entries());
|
|
456
|
+
const texts = entries.map(([, e]) => [e.label, e.summary ?? ""].join(" "));
|
|
457
|
+
let embeddings;
|
|
458
|
+
let attempt = 0;
|
|
459
|
+
while (true) {
|
|
460
|
+
try {
|
|
461
|
+
embeddings = await this.embedding.embedBatch(texts);
|
|
462
|
+
break;
|
|
463
|
+
} catch (err) {
|
|
464
|
+
attempt++;
|
|
465
|
+
if (attempt > maxRetries) {
|
|
466
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
467
|
+
return {
|
|
468
|
+
processed: 0,
|
|
469
|
+
failed: entries.length,
|
|
470
|
+
errors: entries.map(([id]) => ({ entityId: id, error: `embedBatch failed after ${maxRetries} retries: ${errorMsg}` })),
|
|
471
|
+
modelId: this.embedding.modelId(),
|
|
472
|
+
dimensions: this.embedding.dimensions()
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
const backoffMs = 1e3 * 2 ** (attempt - 1);
|
|
476
|
+
await new Promise((resolve) => {
|
|
477
|
+
setTimeout(resolve, backoffMs);
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
const errors = [];
|
|
482
|
+
let processed = 0;
|
|
483
|
+
for (let i = 0; i < entries.length; i++) {
|
|
484
|
+
const [id, entity] = entries[i];
|
|
485
|
+
try {
|
|
486
|
+
await this.storage.updateEntity(this.repositoryId, id, {
|
|
487
|
+
provenance: entity.provenance,
|
|
488
|
+
// preserve existing provenance — not a content edit
|
|
489
|
+
embedding: embeddings[i]
|
|
490
|
+
});
|
|
491
|
+
processed++;
|
|
492
|
+
} catch (err) {
|
|
493
|
+
errors.push({ entityId: id, error: err instanceof Error ? err.message : String(err) });
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
return {
|
|
497
|
+
processed,
|
|
498
|
+
failed: errors.length,
|
|
499
|
+
errors,
|
|
500
|
+
modelId: this.embedding.modelId(),
|
|
501
|
+
dimensions: this.embedding.dimensions()
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Re-embed all entities in the repository, processing in batches.
|
|
506
|
+
* @param options.batchSize — entities per batch (default 50)
|
|
507
|
+
* @param options.maxRetries — retries per batch on embedding failure (default 3)
|
|
508
|
+
* @param options.errorThresholdToAbort — abort after this many cumulative failures (default: no limit)
|
|
509
|
+
* @param options.delayBetweenBatchesMs — milliseconds to wait between batches for rate limiting (default 0)
|
|
510
|
+
* @param options.onProgress — callback invoked after each batch
|
|
511
|
+
*/
|
|
512
|
+
async reembedAll(options) {
|
|
513
|
+
if (!this.embedding) {
|
|
514
|
+
throw new EmbeddingProviderRequiredError();
|
|
515
|
+
}
|
|
516
|
+
const batchSize = options?.batchSize ?? 50;
|
|
517
|
+
const errorThreshold = options?.errorThresholdToAbort;
|
|
518
|
+
const firstPage = await this.storage.findEntities(this.repositoryId, { limit: 1, offset: 0 });
|
|
519
|
+
const total = firstPage.total;
|
|
520
|
+
let totalProcessed = 0;
|
|
521
|
+
let totalFailed = 0;
|
|
522
|
+
const allErrors = [];
|
|
523
|
+
let offset = 0;
|
|
524
|
+
while (offset < total) {
|
|
525
|
+
const page = await this.storage.findEntities(this.repositoryId, {
|
|
526
|
+
limit: batchSize,
|
|
527
|
+
offset
|
|
528
|
+
});
|
|
529
|
+
if (page.items.length === 0) break;
|
|
530
|
+
const ids = page.items.map((e) => e.id);
|
|
531
|
+
const result = await this.reembedEntities(ids, {
|
|
532
|
+
maxRetries: options?.maxRetries
|
|
533
|
+
});
|
|
534
|
+
totalProcessed += result.processed;
|
|
535
|
+
totalFailed += result.failed;
|
|
536
|
+
allErrors.push(...result.errors);
|
|
537
|
+
options?.onProgress?.(totalProcessed, total, totalFailed);
|
|
538
|
+
if (errorThreshold !== void 0 && totalFailed >= errorThreshold) {
|
|
539
|
+
allErrors.push({ entityId: "", error: `Aborted: error threshold of ${errorThreshold} reached (${totalFailed} failures)` });
|
|
540
|
+
break;
|
|
541
|
+
}
|
|
542
|
+
offset += page.items.length;
|
|
543
|
+
const delayMs = options?.delayBetweenBatchesMs ?? 0;
|
|
544
|
+
if (delayMs > 0 && offset < total) {
|
|
545
|
+
await new Promise((resolve) => {
|
|
546
|
+
setTimeout(resolve, delayMs);
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
return {
|
|
551
|
+
processed: totalProcessed,
|
|
552
|
+
failed: totalFailed,
|
|
553
|
+
errors: allErrors,
|
|
554
|
+
modelId: this.embedding.modelId(),
|
|
555
|
+
dimensions: this.embedding.dimensions()
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
/** Generate an embedding vector from label + summary if a provider is available */
|
|
559
|
+
async generateEmbedding(label, summary) {
|
|
560
|
+
if (!this.embedding) return void 0;
|
|
561
|
+
const text = [label, summary ?? ""].join(" ");
|
|
562
|
+
return this.embedding.embed(text);
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
function storedToEntity(stored) {
|
|
566
|
+
return {
|
|
567
|
+
id: stored.id,
|
|
568
|
+
slug: stored.slug,
|
|
569
|
+
entityType: stored.entityType,
|
|
570
|
+
label: stored.label,
|
|
571
|
+
summary: stored.summary,
|
|
572
|
+
properties: stored.properties,
|
|
573
|
+
data: stored.data,
|
|
574
|
+
dataFormat: stored.dataFormat,
|
|
575
|
+
provenance: stored.provenance
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
function storedToSummary(stored) {
|
|
579
|
+
return {
|
|
580
|
+
id: stored.id,
|
|
581
|
+
slug: stored.slug,
|
|
582
|
+
entityType: stored.entityType,
|
|
583
|
+
label: stored.label,
|
|
584
|
+
summary: stored.summary,
|
|
585
|
+
properties: stored.properties
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
function storedToBrief(stored) {
|
|
589
|
+
return {
|
|
590
|
+
id: stored.id,
|
|
591
|
+
slug: stored.slug,
|
|
592
|
+
entityType: stored.entityType,
|
|
593
|
+
label: stored.label,
|
|
594
|
+
summary: stored.summary
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// src/vocabulary/similarity.ts
|
|
599
|
+
function jaroSimilarity(a, b) {
|
|
600
|
+
if (a === b) return 1;
|
|
601
|
+
if (a.length === 0 || b.length === 0) return 0;
|
|
602
|
+
const matchWindow = Math.max(Math.floor(Math.max(a.length, b.length) / 2) - 1, 0);
|
|
603
|
+
const aMatches = new Array(a.length).fill(false);
|
|
604
|
+
const bMatches = new Array(b.length).fill(false);
|
|
605
|
+
let matches = 0;
|
|
606
|
+
let transpositions = 0;
|
|
607
|
+
for (let i = 0; i < a.length; i++) {
|
|
608
|
+
const start = Math.max(0, i - matchWindow);
|
|
609
|
+
const end = Math.min(i + matchWindow + 1, b.length);
|
|
610
|
+
for (let j = start; j < end; j++) {
|
|
611
|
+
if (bMatches[j] || a[i] !== b[j]) continue;
|
|
612
|
+
aMatches[i] = true;
|
|
613
|
+
bMatches[j] = true;
|
|
614
|
+
matches++;
|
|
615
|
+
break;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
if (matches === 0) return 0;
|
|
619
|
+
let k = 0;
|
|
620
|
+
for (let i = 0; i < a.length; i++) {
|
|
621
|
+
if (!aMatches[i]) continue;
|
|
622
|
+
while (!bMatches[k]) k++;
|
|
623
|
+
if (a[i] !== b[k]) transpositions++;
|
|
624
|
+
k++;
|
|
625
|
+
}
|
|
626
|
+
return (matches / a.length + matches / b.length + (matches - transpositions / 2) / matches) / 3;
|
|
627
|
+
}
|
|
628
|
+
function jaroWinklerSimilarity(a, b, scalingFactor = 0.1) {
|
|
629
|
+
const jaro = jaroSimilarity(a, b);
|
|
630
|
+
if (jaro === 0) return 0;
|
|
631
|
+
const prefixLength = Math.min(4, Math.min(a.length, b.length));
|
|
632
|
+
let commonPrefix = 0;
|
|
633
|
+
for (let i = 0; i < prefixLength; i++) {
|
|
634
|
+
if (a[i] === b[i]) {
|
|
635
|
+
commonPrefix++;
|
|
636
|
+
} else {
|
|
637
|
+
break;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
return jaro + commonPrefix * Math.min(scalingFactor, 0.25) * (1 - jaro);
|
|
641
|
+
}
|
|
642
|
+
function normaliseTypeName(name) {
|
|
643
|
+
return name.toLowerCase().trim().replace(/[-\s.]+/g, "_").replace(/[^a-z0-9_]/g, "");
|
|
644
|
+
}
|
|
645
|
+
function toScreamingSnakeCase(name) {
|
|
646
|
+
return name.trim().replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2").replace(/[-\s.]+/g, "_").replace(/[^A-Za-z0-9_]/g, "").replace(/_+/g, "_").replace(/^_+|_+$/g, "").toUpperCase();
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// src/relationships/RelationshipManager.ts
|
|
650
|
+
var RelationshipManager = class {
|
|
651
|
+
constructor(repositoryId, vocabularyEngine, provenanceTracker, eventBus, storage) {
|
|
652
|
+
this.repositoryId = repositoryId;
|
|
653
|
+
this.vocabularyEngine = vocabularyEngine;
|
|
654
|
+
this.provenanceTracker = provenanceTracker;
|
|
655
|
+
this.eventBus = eventBus;
|
|
656
|
+
this.storage = storage;
|
|
657
|
+
}
|
|
658
|
+
/** Create a relationship with vocabulary validation, provenance, and events */
|
|
659
|
+
async create(input) {
|
|
660
|
+
const sourceEntity = await this.storage.getEntity(this.repositoryId, input.sourceEntityId);
|
|
661
|
+
if (!sourceEntity) {
|
|
662
|
+
throw new EntityNotFoundError(input.sourceEntityId);
|
|
663
|
+
}
|
|
664
|
+
const targetEntity = await this.storage.getEntity(this.repositoryId, input.targetEntityId);
|
|
665
|
+
if (!targetEntity) {
|
|
666
|
+
throw new EntityNotFoundError(input.targetEntityId);
|
|
667
|
+
}
|
|
668
|
+
const validation = await this.vocabularyEngine.validateRelationship(
|
|
669
|
+
input,
|
|
670
|
+
sourceEntity.entityType,
|
|
671
|
+
targetEntity.entityType
|
|
672
|
+
);
|
|
673
|
+
if (!validation.valid) {
|
|
674
|
+
await this.eventBus.emit("validation:failed", {
|
|
675
|
+
operation: "createRelationship",
|
|
676
|
+
error: validation.errors.map((e) => e.message).join("; "),
|
|
677
|
+
suggestions: validation.errors.filter((e) => e.suggestion).map((e) => e.suggestion)
|
|
678
|
+
});
|
|
679
|
+
throw new VocabularyValidationError(validation.errors);
|
|
680
|
+
}
|
|
681
|
+
const hookResult = await this.eventBus.emitHook("relationship:creating", { input });
|
|
682
|
+
if (hookResult.cancelled) {
|
|
683
|
+
throw new OperationCancelledError(
|
|
684
|
+
"Relationship creation",
|
|
685
|
+
hookResult.reason ?? "cancelled by hook"
|
|
686
|
+
);
|
|
687
|
+
}
|
|
688
|
+
const normalizedType = toScreamingSnakeCase(input.relationshipType);
|
|
689
|
+
const vocabulary = await this.vocabularyEngine.getVocabulary();
|
|
690
|
+
const relType = vocabulary.relationshipTypes.find((rt) => rt.type === normalizedType);
|
|
691
|
+
const bidirectional = relType?.bidirectional ?? false;
|
|
692
|
+
const id = input.id ?? generateRelationshipId();
|
|
693
|
+
const provenance = this.provenanceTracker.stampCreate();
|
|
694
|
+
const storedRelationship = {
|
|
695
|
+
id,
|
|
696
|
+
relationshipType: normalizedType,
|
|
697
|
+
sourceEntityId: input.sourceEntityId,
|
|
698
|
+
targetEntityId: input.targetEntityId,
|
|
699
|
+
properties: input.properties ?? {},
|
|
700
|
+
bidirectional,
|
|
701
|
+
provenance
|
|
702
|
+
};
|
|
703
|
+
const created = await this.storage.createRelationship(this.repositoryId, storedRelationship);
|
|
704
|
+
const relationship = storedToRelationship(created);
|
|
705
|
+
await this.eventBus.emit("relationship:created", { relationship });
|
|
706
|
+
return relationship;
|
|
707
|
+
}
|
|
708
|
+
/** Remove a relationship by ID */
|
|
709
|
+
async remove(relationshipId) {
|
|
710
|
+
const existing = await this.storage.getRelationship(this.repositoryId, relationshipId);
|
|
711
|
+
if (!existing) {
|
|
712
|
+
throw new RelationshipNotFoundError(relationshipId);
|
|
713
|
+
}
|
|
714
|
+
const hookResult = await this.eventBus.emitHook("relationship:removing", { id: relationshipId });
|
|
715
|
+
if (hookResult.cancelled) {
|
|
716
|
+
throw new OperationCancelledError(
|
|
717
|
+
"Relationship removal",
|
|
718
|
+
hookResult.reason ?? "cancelled by hook"
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
await this.storage.deleteRelationship(this.repositoryId, relationshipId);
|
|
722
|
+
await this.eventBus.emit("relationship:removed", { id: relationshipId });
|
|
723
|
+
}
|
|
724
|
+
/** Get relationships for an entity with filtering */
|
|
725
|
+
async getForEntity(entityId, options) {
|
|
726
|
+
const result = await this.storage.getEntityRelationships(
|
|
727
|
+
this.repositoryId,
|
|
728
|
+
entityId,
|
|
729
|
+
options
|
|
730
|
+
);
|
|
731
|
+
return {
|
|
732
|
+
items: result.items.map(storedToRelationship),
|
|
733
|
+
total: result.total,
|
|
734
|
+
hasMore: result.hasMore,
|
|
735
|
+
limit: result.limit,
|
|
736
|
+
offset: result.offset
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
};
|
|
740
|
+
function storedToRelationship(stored) {
|
|
741
|
+
return {
|
|
742
|
+
id: stored.id,
|
|
743
|
+
relationshipType: stored.relationshipType,
|
|
744
|
+
sourceEntityId: stored.sourceEntityId,
|
|
745
|
+
targetEntityId: stored.targetEntityId,
|
|
746
|
+
properties: stored.properties,
|
|
747
|
+
bidirectional: stored.bidirectional,
|
|
748
|
+
provenance: stored.provenance
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// src/entities/entityProjection.ts
|
|
753
|
+
function projectEntity(stored, level) {
|
|
754
|
+
switch (level) {
|
|
755
|
+
case "brief":
|
|
756
|
+
return {
|
|
757
|
+
id: stored.id,
|
|
758
|
+
slug: stored.slug,
|
|
759
|
+
entityType: stored.entityType,
|
|
760
|
+
label: stored.label,
|
|
761
|
+
summary: stored.summary
|
|
762
|
+
};
|
|
763
|
+
case "summary":
|
|
764
|
+
return {
|
|
765
|
+
id: stored.id,
|
|
766
|
+
slug: stored.slug,
|
|
767
|
+
entityType: stored.entityType,
|
|
768
|
+
label: stored.label,
|
|
769
|
+
summary: stored.summary,
|
|
770
|
+
properties: stored.properties
|
|
771
|
+
};
|
|
772
|
+
case "full":
|
|
773
|
+
return {
|
|
774
|
+
id: stored.id,
|
|
775
|
+
slug: stored.slug,
|
|
776
|
+
entityType: stored.entityType,
|
|
777
|
+
label: stored.label,
|
|
778
|
+
summary: stored.summary,
|
|
779
|
+
properties: stored.properties,
|
|
780
|
+
data: stored.data,
|
|
781
|
+
dataFormat: stored.dataFormat,
|
|
782
|
+
provenance: stored.provenance
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// src/relationships/GraphTraversal.ts
|
|
788
|
+
var GraphTraversal = class {
|
|
789
|
+
constructor(repositoryId, storage) {
|
|
790
|
+
this.repositoryId = repositoryId;
|
|
791
|
+
this.storage = storage;
|
|
792
|
+
}
|
|
793
|
+
/** BFS neighbourhood exploration from a centre entity */
|
|
794
|
+
async exploreNeighbourhood(entityId, options) {
|
|
795
|
+
const storageOptions = {
|
|
796
|
+
depth: options?.depth ?? 1,
|
|
797
|
+
relationshipTypes: options?.relationshipTypes,
|
|
798
|
+
entityTypes: options?.entityTypes,
|
|
799
|
+
direction: options?.direction ?? "both",
|
|
800
|
+
limitPerType: options?.limitPerType ?? 10,
|
|
801
|
+
offsetPerType: options?.offsetPerType ?? 0,
|
|
802
|
+
relationshipPropertyFilters: options?.relationshipPropertyFilters
|
|
803
|
+
};
|
|
804
|
+
const storageResult = await this.storage.exploreNeighbourhood(
|
|
805
|
+
this.repositoryId,
|
|
806
|
+
entityId,
|
|
807
|
+
storageOptions
|
|
808
|
+
);
|
|
809
|
+
const centreEntity = await this.storage.getEntity(this.repositoryId, entityId);
|
|
810
|
+
if (!centreEntity) {
|
|
811
|
+
throw new EntityNotFoundError(entityId);
|
|
812
|
+
}
|
|
813
|
+
const detailLevel = options?.detailLevel ?? "summary";
|
|
814
|
+
let totalEntities = 0;
|
|
815
|
+
let returnedEntities = 0;
|
|
816
|
+
const truncatedTypes = [];
|
|
817
|
+
const layers = storageResult.layers.map((storageLayer) => {
|
|
818
|
+
const layer = {};
|
|
819
|
+
for (const [relType, group] of Object.entries(storageLayer)) {
|
|
820
|
+
totalEntities += group.total;
|
|
821
|
+
returnedEntities += group.entities.length;
|
|
822
|
+
if (group.entities.length < group.total && !truncatedTypes.includes(relType)) {
|
|
823
|
+
truncatedTypes.push(relType);
|
|
824
|
+
}
|
|
825
|
+
layer[relType] = {
|
|
826
|
+
total: group.total,
|
|
827
|
+
returned: group.entities.length,
|
|
828
|
+
entities: group.entities.map((e) => projectEntity(e, detailLevel))
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
return layer;
|
|
832
|
+
});
|
|
833
|
+
return {
|
|
834
|
+
centre: {
|
|
835
|
+
id: centreEntity.id,
|
|
836
|
+
slug: centreEntity.slug,
|
|
837
|
+
entityType: centreEntity.entityType,
|
|
838
|
+
label: centreEntity.label
|
|
839
|
+
},
|
|
840
|
+
layers,
|
|
841
|
+
statistics: {
|
|
842
|
+
totalEntities,
|
|
843
|
+
returnedEntities,
|
|
844
|
+
truncatedTypes
|
|
845
|
+
}
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
/** BFS shortest path finding between two entities */
|
|
849
|
+
async findPaths(sourceId, targetId, options) {
|
|
850
|
+
const storageOptions = {
|
|
851
|
+
maxDepth: options?.maxDepth ?? 3,
|
|
852
|
+
relationshipTypes: options?.relationshipTypes,
|
|
853
|
+
entityTypes: options?.entityTypes,
|
|
854
|
+
limit: options?.limit ?? 5,
|
|
855
|
+
offset: options?.offset ?? 0,
|
|
856
|
+
relationshipPropertyFilters: options?.relationshipPropertyFilters
|
|
857
|
+
};
|
|
858
|
+
const storageResult = await this.storage.findPaths(
|
|
859
|
+
this.repositoryId,
|
|
860
|
+
sourceId,
|
|
861
|
+
targetId,
|
|
862
|
+
storageOptions
|
|
863
|
+
);
|
|
864
|
+
const entityIdSet = /* @__PURE__ */ new Set();
|
|
865
|
+
const relIdSet = /* @__PURE__ */ new Set();
|
|
866
|
+
for (const path of storageResult.paths) {
|
|
867
|
+
for (const eid of path.entityIds) entityIdSet.add(eid);
|
|
868
|
+
for (const rid of path.relationshipIds) relIdSet.add(rid);
|
|
869
|
+
}
|
|
870
|
+
const entityMap = await this.storage.getEntities(
|
|
871
|
+
this.repositoryId,
|
|
872
|
+
Array.from(entityIdSet)
|
|
873
|
+
);
|
|
874
|
+
const relMap = /* @__PURE__ */ new Map();
|
|
875
|
+
for (const rid of relIdSet) {
|
|
876
|
+
const rel = await this.storage.getRelationship(this.repositoryId, rid);
|
|
877
|
+
if (rel) {
|
|
878
|
+
relMap.set(rid, {
|
|
879
|
+
id: rel.id,
|
|
880
|
+
type: rel.relationshipType,
|
|
881
|
+
sourceEntityId: rel.sourceEntityId,
|
|
882
|
+
properties: rel.properties
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
const entityTypeFilter = options?.entityTypes;
|
|
887
|
+
const filteredPaths = entityTypeFilter ? storageResult.paths.filter(
|
|
888
|
+
(sp) => sp.entityIds.every((eid) => {
|
|
889
|
+
const e = entityMap.get(eid);
|
|
890
|
+
return e ? entityTypeFilter.includes(e.entityType) : true;
|
|
891
|
+
})
|
|
892
|
+
) : storageResult.paths;
|
|
893
|
+
const pathDetailLevel = options?.detailLevel ?? "brief";
|
|
894
|
+
return {
|
|
895
|
+
totalPaths: entityTypeFilter ? filteredPaths.length : storageResult.totalPaths,
|
|
896
|
+
returned: filteredPaths.length,
|
|
897
|
+
paths: filteredPaths.map((sp) => ({
|
|
898
|
+
length: sp.entityIds.length - 1,
|
|
899
|
+
entities: sp.entityIds.map((eid) => {
|
|
900
|
+
const e = entityMap.get(eid);
|
|
901
|
+
if (e) return projectEntity(e, pathDetailLevel);
|
|
902
|
+
return {
|
|
903
|
+
id: eid,
|
|
904
|
+
slug: eid,
|
|
905
|
+
entityType: "unknown",
|
|
906
|
+
label: eid,
|
|
907
|
+
summary: void 0
|
|
908
|
+
};
|
|
909
|
+
}),
|
|
910
|
+
relationships: sp.relationshipIds.map((rid, idx) => {
|
|
911
|
+
const r = relMap.get(rid);
|
|
912
|
+
const currentEntityId = sp.entityIds[idx];
|
|
913
|
+
return {
|
|
914
|
+
id: rid,
|
|
915
|
+
type: r?.type ?? "unknown",
|
|
916
|
+
direction: r?.sourceEntityId === currentEntityId ? "outbound" : "inbound",
|
|
917
|
+
properties: r?.properties ?? {}
|
|
918
|
+
};
|
|
919
|
+
})
|
|
920
|
+
}))
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
};
|
|
924
|
+
|
|
925
|
+
// src/search/SearchOrchestrator.ts
|
|
926
|
+
var SearchOrchestrator = class {
|
|
927
|
+
repositoryId;
|
|
928
|
+
storage;
|
|
929
|
+
search;
|
|
930
|
+
embedding;
|
|
931
|
+
eventBus;
|
|
932
|
+
defaultSimilarityThreshold;
|
|
933
|
+
conceptSearchScanLimit;
|
|
934
|
+
constructor(config) {
|
|
935
|
+
this.repositoryId = config.repositoryId;
|
|
936
|
+
this.storage = config.storage;
|
|
937
|
+
this.search = config.search;
|
|
938
|
+
this.embedding = config.embedding;
|
|
939
|
+
this.eventBus = config.eventBus;
|
|
940
|
+
this.defaultSimilarityThreshold = config.defaultSimilarityThreshold ?? 0.5;
|
|
941
|
+
this.conceptSearchScanLimit = config.conceptSearchScanLimit ?? 1e3;
|
|
942
|
+
}
|
|
943
|
+
/**
|
|
944
|
+
* Find entities by label, type, and property filters.
|
|
945
|
+
* When a SearchProvider is available, its results are merged with
|
|
946
|
+
* storage-level results for better relevance ranking.
|
|
947
|
+
*/
|
|
948
|
+
async findEntities(query) {
|
|
949
|
+
const limit = Math.min(query.limit ?? 10, 50);
|
|
950
|
+
const offset = query.offset ?? 0;
|
|
951
|
+
const detailLevel = query.detailLevel ?? "summary";
|
|
952
|
+
const storageQuery = {
|
|
953
|
+
searchTerm: query.searchTerm,
|
|
954
|
+
entityTypes: query.entityTypes,
|
|
955
|
+
properties: query.properties,
|
|
956
|
+
provenance: query.provenance,
|
|
957
|
+
limit,
|
|
958
|
+
offset
|
|
959
|
+
};
|
|
960
|
+
if (this.search && query.searchTerm) {
|
|
961
|
+
const [storageResult, searchResult] = await Promise.all([
|
|
962
|
+
this.storage.findEntities(this.repositoryId, storageQuery),
|
|
963
|
+
this.search.search(this.repositoryId, query.searchTerm, {
|
|
964
|
+
entityTypes: query.entityTypes,
|
|
965
|
+
limit: limit * 2,
|
|
966
|
+
// fetch more from search to merge
|
|
967
|
+
offset: 0
|
|
968
|
+
})
|
|
969
|
+
]);
|
|
970
|
+
const merged = this.mergeResults(storageResult.items, searchResult.items, limit, offset, detailLevel);
|
|
971
|
+
await this.emitSearchEvent(query.searchTerm, merged.items.length);
|
|
972
|
+
return merged;
|
|
973
|
+
}
|
|
974
|
+
const result = await this.storage.findEntities(this.repositoryId, storageQuery);
|
|
975
|
+
if (query.searchTerm) {
|
|
976
|
+
await this.emitSearchEvent(query.searchTerm, result.items.length);
|
|
977
|
+
}
|
|
978
|
+
return {
|
|
979
|
+
items: result.items.map((e) => projectEntity(e, detailLevel)),
|
|
980
|
+
total: result.total,
|
|
981
|
+
hasMore: result.hasMore,
|
|
982
|
+
limit: result.limit,
|
|
983
|
+
offset: result.offset
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
/**
|
|
987
|
+
* Semantic concept search using the EmbeddingProvider.
|
|
988
|
+
* Embeds the query, then compares against entity embeddings.
|
|
989
|
+
* Falls back to label/summary text matching when no embeddings exist on entities.
|
|
990
|
+
*/
|
|
991
|
+
async searchByConcept(query, options) {
|
|
992
|
+
if (!this.embedding) {
|
|
993
|
+
throw new EmbeddingProviderRequiredError();
|
|
994
|
+
}
|
|
995
|
+
const limit = Math.min(options?.limit ?? 10, 50);
|
|
996
|
+
const offset = options?.offset ?? 0;
|
|
997
|
+
const threshold = options?.similarityThreshold ?? this.defaultSimilarityThreshold;
|
|
998
|
+
const queryEmbedding = await this.embedding.embed(query);
|
|
999
|
+
const allEntities = await this.storage.findEntities(this.repositoryId, {
|
|
1000
|
+
entityTypes: options?.entityTypes,
|
|
1001
|
+
limit: this.conceptSearchScanLimit,
|
|
1002
|
+
offset: 0
|
|
1003
|
+
});
|
|
1004
|
+
const scored = [];
|
|
1005
|
+
for (const entity of allEntities.items) {
|
|
1006
|
+
let score;
|
|
1007
|
+
if (entity.embedding && entity.embedding.length > 0) {
|
|
1008
|
+
score = this.cosineSimilarity(queryEmbedding, entity.embedding);
|
|
1009
|
+
} else {
|
|
1010
|
+
const entityText = [entity.label, entity.summary ?? ""].join(" ");
|
|
1011
|
+
const entityEmbedding = await this.embedding.embed(entityText);
|
|
1012
|
+
score = this.cosineSimilarity(queryEmbedding, entityEmbedding);
|
|
1013
|
+
}
|
|
1014
|
+
if (score >= threshold) {
|
|
1015
|
+
scored.push({
|
|
1016
|
+
id: entity.id,
|
|
1017
|
+
slug: entity.slug,
|
|
1018
|
+
entityType: entity.entityType,
|
|
1019
|
+
label: entity.label,
|
|
1020
|
+
summary: entity.summary,
|
|
1021
|
+
score
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
scored.sort((a, b) => b.score - a.score);
|
|
1026
|
+
const total = scored.length;
|
|
1027
|
+
const items = scored.slice(offset, offset + limit);
|
|
1028
|
+
await this.emitSearchEvent(query, items.length);
|
|
1029
|
+
return {
|
|
1030
|
+
items,
|
|
1031
|
+
total,
|
|
1032
|
+
hasMore: offset + limit < total,
|
|
1033
|
+
limit,
|
|
1034
|
+
offset
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
/** Merge storage results with search provider results */
|
|
1038
|
+
mergeResults(storageItems, searchHits, limit, offset, detailLevel = "summary") {
|
|
1039
|
+
const scoreMap = /* @__PURE__ */ new Map();
|
|
1040
|
+
for (const hit of searchHits) {
|
|
1041
|
+
scoreMap.set(hit.id, hit.score);
|
|
1042
|
+
}
|
|
1043
|
+
const allEntityIds = /* @__PURE__ */ new Set();
|
|
1044
|
+
const rankedItems = [];
|
|
1045
|
+
for (const hit of searchHits) {
|
|
1046
|
+
const storageEntity = storageItems.find((e) => e.id === hit.id);
|
|
1047
|
+
if (storageEntity) {
|
|
1048
|
+
rankedItems.push({ entity: storageEntity, score: hit.score });
|
|
1049
|
+
allEntityIds.add(hit.id);
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
for (const entity of storageItems) {
|
|
1053
|
+
if (!allEntityIds.has(entity.id)) {
|
|
1054
|
+
rankedItems.push({ entity, score: 0 });
|
|
1055
|
+
allEntityIds.add(entity.id);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
rankedItems.sort((a, b) => b.score - a.score);
|
|
1059
|
+
const total = rankedItems.length;
|
|
1060
|
+
const page = rankedItems.slice(offset, offset + limit);
|
|
1061
|
+
return {
|
|
1062
|
+
items: page.map((r) => projectEntity(r.entity, detailLevel)),
|
|
1063
|
+
total,
|
|
1064
|
+
hasMore: offset + limit < total,
|
|
1065
|
+
limit,
|
|
1066
|
+
offset
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
/** Cosine similarity between two vectors */
|
|
1070
|
+
cosineSimilarity(a, b) {
|
|
1071
|
+
if (this.embedding?.similarity) {
|
|
1072
|
+
return this.embedding.similarity(a, b);
|
|
1073
|
+
}
|
|
1074
|
+
if (a.length !== b.length || a.length === 0) return 0;
|
|
1075
|
+
let dotProduct = 0;
|
|
1076
|
+
let normA = 0;
|
|
1077
|
+
let normB = 0;
|
|
1078
|
+
for (let i = 0; i < a.length; i++) {
|
|
1079
|
+
const ai = a[i];
|
|
1080
|
+
const bi = b[i];
|
|
1081
|
+
dotProduct += ai * bi;
|
|
1082
|
+
normA += ai * ai;
|
|
1083
|
+
normB += bi * bi;
|
|
1084
|
+
}
|
|
1085
|
+
const denominator = Math.sqrt(normA) * Math.sqrt(normB);
|
|
1086
|
+
return denominator === 0 ? 0 : dotProduct / denominator;
|
|
1087
|
+
}
|
|
1088
|
+
async emitSearchEvent(query, resultCount) {
|
|
1089
|
+
if (this.eventBus) {
|
|
1090
|
+
await this.eventBus.emit("search:executed", { query, resultCount });
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
};
|
|
1094
|
+
|
|
1095
|
+
// src/core/MemoryRepository.ts
|
|
1096
|
+
var MemoryRepository = class {
|
|
1097
|
+
repositoryId;
|
|
1098
|
+
storage;
|
|
1099
|
+
search;
|
|
1100
|
+
vocabularyEngine;
|
|
1101
|
+
provenanceTracker;
|
|
1102
|
+
eventBus;
|
|
1103
|
+
entityManager;
|
|
1104
|
+
relationshipManager;
|
|
1105
|
+
graphTraversal;
|
|
1106
|
+
searchOrchestrator;
|
|
1107
|
+
constructor(config) {
|
|
1108
|
+
this.repositoryId = config.repositoryId;
|
|
1109
|
+
this.storage = config.storage;
|
|
1110
|
+
this.search = config.search;
|
|
1111
|
+
this.vocabularyEngine = config.vocabularyEngine;
|
|
1112
|
+
this.provenanceTracker = config.provenanceTracker;
|
|
1113
|
+
this.eventBus = config.eventBus;
|
|
1114
|
+
this.entityManager = new EntityManager(
|
|
1115
|
+
config.repositoryId,
|
|
1116
|
+
config.vocabularyEngine,
|
|
1117
|
+
config.provenanceTracker,
|
|
1118
|
+
config.eventBus,
|
|
1119
|
+
config.storage,
|
|
1120
|
+
config.embedding
|
|
1121
|
+
);
|
|
1122
|
+
this.relationshipManager = new RelationshipManager(
|
|
1123
|
+
config.repositoryId,
|
|
1124
|
+
config.vocabularyEngine,
|
|
1125
|
+
config.provenanceTracker,
|
|
1126
|
+
config.eventBus,
|
|
1127
|
+
config.storage
|
|
1128
|
+
);
|
|
1129
|
+
this.graphTraversal = new GraphTraversal(
|
|
1130
|
+
config.repositoryId,
|
|
1131
|
+
config.storage
|
|
1132
|
+
);
|
|
1133
|
+
this.searchOrchestrator = new SearchOrchestrator({
|
|
1134
|
+
repositoryId: config.repositoryId,
|
|
1135
|
+
storage: config.storage,
|
|
1136
|
+
search: config.search,
|
|
1137
|
+
embedding: config.embedding,
|
|
1138
|
+
eventBus: config.eventBus,
|
|
1139
|
+
defaultSimilarityThreshold: config.vocabularyEngine.getGovernanceConfig().defaultSimilarityThreshold
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
// ─── Vocabulary ────────────────────────────────────────────────────
|
|
1143
|
+
async getVocabulary() {
|
|
1144
|
+
return this.vocabularyEngine.getResolvedVocabulary();
|
|
1145
|
+
}
|
|
1146
|
+
async proposeVocabularyChange(proposal) {
|
|
1147
|
+
const context = this.provenanceTracker.getContext();
|
|
1148
|
+
return this.vocabularyEngine.proposeChange(proposal, context.actorId);
|
|
1149
|
+
}
|
|
1150
|
+
/** @deprecated Use proposeVocabularyChange instead */
|
|
1151
|
+
async proposeVocabularyExtension(proposal) {
|
|
1152
|
+
return this.proposeVocabularyChange(proposal);
|
|
1153
|
+
}
|
|
1154
|
+
// ─── Entities ──────────────────────────────────────────────────────
|
|
1155
|
+
async createEntity(input) {
|
|
1156
|
+
const entity = await this.entityManager.create(input);
|
|
1157
|
+
if (this.search) {
|
|
1158
|
+
await this.search.indexEntity(this.repositoryId, {
|
|
1159
|
+
entityId: entity.id,
|
|
1160
|
+
entityType: entity.entityType,
|
|
1161
|
+
label: entity.label,
|
|
1162
|
+
summary: entity.summary,
|
|
1163
|
+
properties: entity.properties,
|
|
1164
|
+
data: entity.data
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1167
|
+
return entity;
|
|
1168
|
+
}
|
|
1169
|
+
async updateEntity(entityId, updates) {
|
|
1170
|
+
const entity = await this.entityManager.update(entityId, updates);
|
|
1171
|
+
if (this.search) {
|
|
1172
|
+
await this.search.indexEntity(this.repositoryId, {
|
|
1173
|
+
entityId: entity.id,
|
|
1174
|
+
entityType: entity.entityType,
|
|
1175
|
+
label: entity.label,
|
|
1176
|
+
summary: entity.summary,
|
|
1177
|
+
properties: entity.properties,
|
|
1178
|
+
data: entity.data
|
|
1179
|
+
});
|
|
1180
|
+
}
|
|
1181
|
+
return entity;
|
|
1182
|
+
}
|
|
1183
|
+
async getEntity(entityId, detailLevel) {
|
|
1184
|
+
return this.entityManager.get(entityId, detailLevel);
|
|
1185
|
+
}
|
|
1186
|
+
async getBySlug(slug, detailLevel) {
|
|
1187
|
+
return this.entityManager.getBySlug(slug, detailLevel);
|
|
1188
|
+
}
|
|
1189
|
+
async getEntities(entityIds, detailLevel) {
|
|
1190
|
+
return this.entityManager.getMany(entityIds, detailLevel);
|
|
1191
|
+
}
|
|
1192
|
+
async findEntities(query) {
|
|
1193
|
+
return this.searchOrchestrator.findEntities(query);
|
|
1194
|
+
}
|
|
1195
|
+
async deleteEntity(entityId) {
|
|
1196
|
+
await this.entityManager.delete(entityId);
|
|
1197
|
+
if (this.search) {
|
|
1198
|
+
await this.search.removeEntity(this.repositoryId, entityId);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
// ─── Re-embedding ──────────────────────────────────────────────────
|
|
1202
|
+
/** Re-embed specific entities using the current EmbeddingProvider */
|
|
1203
|
+
async reembedEntities(entityIds) {
|
|
1204
|
+
return this.entityManager.reembedEntities(entityIds);
|
|
1205
|
+
}
|
|
1206
|
+
/**
|
|
1207
|
+
* Re-embed all entities in the repository using the current EmbeddingProvider.
|
|
1208
|
+
* Processes in batches with optional rate limiting. Emits reembed:* events for progress tracking.
|
|
1209
|
+
* Updates repository metadata with the new model ID and dimensions on completion.
|
|
1210
|
+
*/
|
|
1211
|
+
async reembedAll(options) {
|
|
1212
|
+
const stats = await this.getStats();
|
|
1213
|
+
await this.eventBus.emit("reembed:started", {
|
|
1214
|
+
repositoryId: this.repositoryId,
|
|
1215
|
+
totalEntities: stats.entityCount
|
|
1216
|
+
});
|
|
1217
|
+
try {
|
|
1218
|
+
const result = await this.entityManager.reembedAll({
|
|
1219
|
+
batchSize: options?.batchSize,
|
|
1220
|
+
maxRetries: options?.maxRetries,
|
|
1221
|
+
errorThresholdToAbort: options?.errorThresholdToAbort,
|
|
1222
|
+
delayBetweenBatchesMs: options?.delayBetweenBatchesMs,
|
|
1223
|
+
onProgress: async (processed, total, failed) => {
|
|
1224
|
+
await this.eventBus.emit("reembed:progress", {
|
|
1225
|
+
repositoryId: this.repositoryId,
|
|
1226
|
+
processed,
|
|
1227
|
+
totalEntities: total,
|
|
1228
|
+
failed
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1231
|
+
});
|
|
1232
|
+
await this.storage.updateRepository(this.repositoryId, {
|
|
1233
|
+
metadata: {
|
|
1234
|
+
embeddingModelId: result.modelId,
|
|
1235
|
+
embeddingDimensions: result.dimensions
|
|
1236
|
+
}
|
|
1237
|
+
});
|
|
1238
|
+
await this.eventBus.emit("reembed:completed", {
|
|
1239
|
+
repositoryId: this.repositoryId,
|
|
1240
|
+
processed: result.processed,
|
|
1241
|
+
failed: result.failed,
|
|
1242
|
+
modelId: result.modelId
|
|
1243
|
+
});
|
|
1244
|
+
return result;
|
|
1245
|
+
} catch (err) {
|
|
1246
|
+
await this.eventBus.emit("reembed:failed", {
|
|
1247
|
+
repositoryId: this.repositoryId,
|
|
1248
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1249
|
+
});
|
|
1250
|
+
throw err;
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
// ─── Relationships ─────────────────────────────────────────────────
|
|
1254
|
+
async createRelationship(input) {
|
|
1255
|
+
return this.relationshipManager.create(input);
|
|
1256
|
+
}
|
|
1257
|
+
async removeRelationship(relationshipId) {
|
|
1258
|
+
return this.relationshipManager.remove(relationshipId);
|
|
1259
|
+
}
|
|
1260
|
+
async getRelationships(entityId, options) {
|
|
1261
|
+
return this.relationshipManager.getForEntity(entityId, options);
|
|
1262
|
+
}
|
|
1263
|
+
async getRelationshipSummary(entityId) {
|
|
1264
|
+
const result = await this.storage.getEntityRelationships(
|
|
1265
|
+
this.repositoryId,
|
|
1266
|
+
entityId,
|
|
1267
|
+
{ direction: "both", limit: 1e4 }
|
|
1268
|
+
);
|
|
1269
|
+
const outbound = {};
|
|
1270
|
+
const inbound = {};
|
|
1271
|
+
for (const rel of result.items) {
|
|
1272
|
+
if (rel.sourceEntityId === entityId) {
|
|
1273
|
+
outbound[rel.relationshipType] = (outbound[rel.relationshipType] ?? 0) + 1;
|
|
1274
|
+
}
|
|
1275
|
+
if (rel.targetEntityId === entityId) {
|
|
1276
|
+
inbound[rel.relationshipType] = (inbound[rel.relationshipType] ?? 0) + 1;
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
return { outbound, inbound };
|
|
1280
|
+
}
|
|
1281
|
+
async getRelationshipsForEntities(entityIds) {
|
|
1282
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1283
|
+
const results = [];
|
|
1284
|
+
for (const entityId of entityIds) {
|
|
1285
|
+
const rels = await this.relationshipManager.getForEntity(entityId, {
|
|
1286
|
+
direction: "both",
|
|
1287
|
+
limit: 1e4
|
|
1288
|
+
});
|
|
1289
|
+
for (const rel of rels.items) {
|
|
1290
|
+
if (!seen.has(rel.id)) {
|
|
1291
|
+
seen.add(rel.id);
|
|
1292
|
+
results.push(rel);
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
return results;
|
|
1297
|
+
}
|
|
1298
|
+
// ─── Graph ─────────────────────────────────────────────────────────
|
|
1299
|
+
/** Get the full graph — entities with enriched relationships, vocabulary, and stats. Paginated. */
|
|
1300
|
+
async getGraph(options) {
|
|
1301
|
+
const pageSize = options?.limit ?? 200;
|
|
1302
|
+
const offset = options?.offset ?? 0;
|
|
1303
|
+
const [stats, vocabulary] = await Promise.all([
|
|
1304
|
+
this.getStats(),
|
|
1305
|
+
this.getVocabulary()
|
|
1306
|
+
]);
|
|
1307
|
+
const entities = await this.findEntities({ limit: pageSize, offset, detailLevel: options?.detailLevel ?? "summary" });
|
|
1308
|
+
const rawRelationships = await this.getRelationshipsForEntities(
|
|
1309
|
+
entities.items.map((e) => e.id)
|
|
1310
|
+
);
|
|
1311
|
+
const entityLookup = /* @__PURE__ */ new Map();
|
|
1312
|
+
for (const e of entities.items) {
|
|
1313
|
+
entityLookup.set(e.id, { slug: e.slug, label: e.label });
|
|
1314
|
+
}
|
|
1315
|
+
const MAX_RELATIONSHIPS = 1e3;
|
|
1316
|
+
const totalRelationships = rawRelationships.length;
|
|
1317
|
+
const cappedRelationships = rawRelationships.slice(0, MAX_RELATIONSHIPS);
|
|
1318
|
+
const relationships = cappedRelationships.map((rel) => {
|
|
1319
|
+
const source = entityLookup.get(rel.sourceEntityId);
|
|
1320
|
+
const target = entityLookup.get(rel.targetEntityId);
|
|
1321
|
+
return {
|
|
1322
|
+
...rel,
|
|
1323
|
+
sourceSlug: source?.slug ?? rel.sourceEntityId,
|
|
1324
|
+
sourceLabel: source?.label ?? rel.sourceEntityId,
|
|
1325
|
+
targetSlug: target?.slug ?? rel.targetEntityId,
|
|
1326
|
+
targetLabel: target?.label ?? rel.targetEntityId
|
|
1327
|
+
};
|
|
1328
|
+
});
|
|
1329
|
+
const nextOffset = offset + entities.items.length;
|
|
1330
|
+
const hasMore = nextOffset < stats.entityCount;
|
|
1331
|
+
return {
|
|
1332
|
+
vocabulary,
|
|
1333
|
+
stats,
|
|
1334
|
+
entities: entities.items,
|
|
1335
|
+
relationships,
|
|
1336
|
+
totalRelationships,
|
|
1337
|
+
relationshipsTruncated: totalRelationships > MAX_RELATIONSHIPS,
|
|
1338
|
+
hasMore,
|
|
1339
|
+
...hasMore ? { cursor: `offset:${nextOffset}` } : {}
|
|
1340
|
+
};
|
|
1341
|
+
}
|
|
1342
|
+
// ─── Graph Traversal ───────────────────────────────────────────────
|
|
1343
|
+
async exploreNeighbourhood(entityId, options) {
|
|
1344
|
+
return this.graphTraversal.exploreNeighbourhood(entityId, options);
|
|
1345
|
+
}
|
|
1346
|
+
async findPaths(sourceId, targetId, options) {
|
|
1347
|
+
return this.graphTraversal.findPaths(sourceId, targetId, options);
|
|
1348
|
+
}
|
|
1349
|
+
// ─── Search ────────────────────────────────────────────────────────
|
|
1350
|
+
async searchByConcept(query, options) {
|
|
1351
|
+
return this.searchOrchestrator.searchByConcept(query, options);
|
|
1352
|
+
}
|
|
1353
|
+
// ─── Timeline ──────────────────────────────────────────────────────
|
|
1354
|
+
async getTimeline(entityId, options) {
|
|
1355
|
+
const storageOptions = {
|
|
1356
|
+
timeRange: options?.timeRange,
|
|
1357
|
+
eventTypes: options?.eventTypes,
|
|
1358
|
+
provenance: options?.provenance,
|
|
1359
|
+
limit: options?.limit ?? 20,
|
|
1360
|
+
offset: options?.offset ?? 0
|
|
1361
|
+
};
|
|
1362
|
+
const storageResult = await this.storage.getTimeline(
|
|
1363
|
+
this.repositoryId,
|
|
1364
|
+
entityId,
|
|
1365
|
+
storageOptions
|
|
1366
|
+
);
|
|
1367
|
+
const entity = await this.storage.getEntity(this.repositoryId, entityId);
|
|
1368
|
+
const centreRef = {
|
|
1369
|
+
id: entityId,
|
|
1370
|
+
slug: entity?.slug ?? entityId,
|
|
1371
|
+
label: entity?.label ?? entityId
|
|
1372
|
+
};
|
|
1373
|
+
const relIds = storageResult.events.filter((e) => e.relationshipId).map((e) => e.relationshipId);
|
|
1374
|
+
const relMap = /* @__PURE__ */ new Map();
|
|
1375
|
+
const entityIdsToResolve = /* @__PURE__ */ new Set();
|
|
1376
|
+
for (const rid of relIds) {
|
|
1377
|
+
const rel = await this.storage.getRelationship(this.repositoryId, rid);
|
|
1378
|
+
if (rel) {
|
|
1379
|
+
relMap.set(rid, {
|
|
1380
|
+
id: rel.id,
|
|
1381
|
+
type: rel.relationshipType,
|
|
1382
|
+
sourceId: rel.sourceEntityId,
|
|
1383
|
+
targetId: rel.targetEntityId
|
|
1384
|
+
});
|
|
1385
|
+
entityIdsToResolve.add(rel.sourceEntityId);
|
|
1386
|
+
entityIdsToResolve.add(rel.targetEntityId);
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
const entityMap = await this.storage.getEntities(
|
|
1390
|
+
this.repositoryId,
|
|
1391
|
+
Array.from(entityIdsToResolve)
|
|
1392
|
+
);
|
|
1393
|
+
const toRef = (eid) => {
|
|
1394
|
+
const e = entityMap.get(eid);
|
|
1395
|
+
return { id: eid, slug: e?.slug ?? eid, label: e?.label ?? eid };
|
|
1396
|
+
};
|
|
1397
|
+
const events = storageResult.events.map((e) => {
|
|
1398
|
+
if (e.relationshipId && relMap.has(e.relationshipId)) {
|
|
1399
|
+
const rel = relMap.get(e.relationshipId);
|
|
1400
|
+
const sourceRef = toRef(rel.sourceId);
|
|
1401
|
+
const targetRef = toRef(rel.targetId);
|
|
1402
|
+
const otherRef = rel.sourceId === entityId ? targetRef : sourceRef;
|
|
1403
|
+
const direction = rel.sourceId === entityId ? "to" : "from";
|
|
1404
|
+
const relDetail = {
|
|
1405
|
+
id: rel.id,
|
|
1406
|
+
relationshipType: rel.type,
|
|
1407
|
+
sourceEntity: sourceRef,
|
|
1408
|
+
targetEntity: targetRef
|
|
1409
|
+
};
|
|
1410
|
+
return {
|
|
1411
|
+
timestamp: e.timestamp,
|
|
1412
|
+
eventType: e.eventType,
|
|
1413
|
+
description: `${rel.type} relationship created ${direction} ${otherRef.label}`,
|
|
1414
|
+
relatedEntities: [centreRef, otherRef],
|
|
1415
|
+
relationship: relDetail
|
|
1416
|
+
};
|
|
1417
|
+
}
|
|
1418
|
+
const action = e.eventType === "entity:created" ? "created" : "updated";
|
|
1419
|
+
return {
|
|
1420
|
+
timestamp: e.timestamp,
|
|
1421
|
+
eventType: e.eventType,
|
|
1422
|
+
description: `${centreRef.label} ${action}`,
|
|
1423
|
+
relatedEntities: [centreRef]
|
|
1424
|
+
};
|
|
1425
|
+
});
|
|
1426
|
+
return {
|
|
1427
|
+
id: entityId,
|
|
1428
|
+
slug: centreRef.slug,
|
|
1429
|
+
totalEvents: storageResult.total,
|
|
1430
|
+
returned: events.length,
|
|
1431
|
+
events
|
|
1432
|
+
};
|
|
1433
|
+
}
|
|
1434
|
+
// ─── Stats ─────────────────────────────────────────────────────────
|
|
1435
|
+
async getStats() {
|
|
1436
|
+
return this.storage.getRepositoryStats(this.repositoryId);
|
|
1437
|
+
}
|
|
1438
|
+
// ─── Events ────────────────────────────────────────────────────────
|
|
1439
|
+
on(event, handler) {
|
|
1440
|
+
return this.eventBus.on(event, handler);
|
|
1441
|
+
}
|
|
1442
|
+
onHook(event, handler) {
|
|
1443
|
+
return this.eventBus.onHook(event, handler);
|
|
1444
|
+
}
|
|
1445
|
+
};
|
|
1446
|
+
|
|
1447
|
+
// src/vocabulary/SemanticDeduplicator.ts
|
|
1448
|
+
var DEFAULT_THRESHOLD = 0.85;
|
|
1449
|
+
var SemanticDeduplicator = class {
|
|
1450
|
+
threshold;
|
|
1451
|
+
embeddingProvider;
|
|
1452
|
+
constructor(config = {}) {
|
|
1453
|
+
this.threshold = config.similarityThreshold ?? DEFAULT_THRESHOLD;
|
|
1454
|
+
this.embeddingProvider = config.embeddingProvider;
|
|
1455
|
+
}
|
|
1456
|
+
/**
|
|
1457
|
+
* Check whether a proposed type name is a duplicate of an existing type.
|
|
1458
|
+
* Uses embedding similarity if an EmbeddingProvider is available, otherwise falls back
|
|
1459
|
+
* to Jaro-Winkler string similarity on normalised type names.
|
|
1460
|
+
*/
|
|
1461
|
+
async checkDuplicate(proposedType, proposedDescription, existingTypes) {
|
|
1462
|
+
if (existingTypes.length === 0) {
|
|
1463
|
+
return { isDuplicate: false, matches: [] };
|
|
1464
|
+
}
|
|
1465
|
+
const normProposed = normaliseTypeName(proposedType);
|
|
1466
|
+
const exactMatch = existingTypes.find((t) => normaliseTypeName(t.type) === normProposed);
|
|
1467
|
+
if (exactMatch) {
|
|
1468
|
+
return {
|
|
1469
|
+
isDuplicate: true,
|
|
1470
|
+
matches: [{ type: exactMatch.type, description: exactMatch.description, similarity: 1 }]
|
|
1471
|
+
};
|
|
1472
|
+
}
|
|
1473
|
+
if (this.embeddingProvider) {
|
|
1474
|
+
return this.checkWithEmbeddings(proposedType, proposedDescription, existingTypes);
|
|
1475
|
+
}
|
|
1476
|
+
return this.checkWithStringSimilarity(proposedType, existingTypes);
|
|
1477
|
+
}
|
|
1478
|
+
async checkWithEmbeddings(proposedType, proposedDescription, existingTypes) {
|
|
1479
|
+
const proposedText = `${proposedType}: ${proposedDescription}`;
|
|
1480
|
+
const existingTexts = existingTypes.map((t) => `${t.type}: ${t.description}`);
|
|
1481
|
+
const allTexts = [proposedText, ...existingTexts];
|
|
1482
|
+
const embeddings = await this.embeddingProvider.embedBatch(allTexts);
|
|
1483
|
+
const proposedEmbedding = embeddings[0];
|
|
1484
|
+
const matches = [];
|
|
1485
|
+
for (let i = 0; i < existingTypes.length; i++) {
|
|
1486
|
+
const similarity = this.cosineSimilarity(proposedEmbedding, embeddings[i + 1]);
|
|
1487
|
+
if (similarity >= this.threshold) {
|
|
1488
|
+
matches.push({
|
|
1489
|
+
type: existingTypes[i].type,
|
|
1490
|
+
description: existingTypes[i].description,
|
|
1491
|
+
similarity
|
|
1492
|
+
});
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
matches.sort((a, b) => b.similarity - a.similarity);
|
|
1496
|
+
return {
|
|
1497
|
+
isDuplicate: matches.length > 0,
|
|
1498
|
+
matches
|
|
1499
|
+
};
|
|
1500
|
+
}
|
|
1501
|
+
checkWithStringSimilarity(proposedType, existingTypes) {
|
|
1502
|
+
const normProposed = normaliseTypeName(proposedType);
|
|
1503
|
+
const matches = [];
|
|
1504
|
+
for (const existing of existingTypes) {
|
|
1505
|
+
const normExisting = normaliseTypeName(existing.type);
|
|
1506
|
+
const similarity = jaroWinklerSimilarity(normProposed, normExisting);
|
|
1507
|
+
if (similarity >= this.threshold) {
|
|
1508
|
+
matches.push({
|
|
1509
|
+
type: existing.type,
|
|
1510
|
+
description: existing.description,
|
|
1511
|
+
similarity
|
|
1512
|
+
});
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
matches.sort((a, b) => b.similarity - a.similarity);
|
|
1516
|
+
return {
|
|
1517
|
+
isDuplicate: matches.length > 0,
|
|
1518
|
+
matches
|
|
1519
|
+
};
|
|
1520
|
+
}
|
|
1521
|
+
cosineSimilarity(a, b) {
|
|
1522
|
+
if (this.embeddingProvider?.similarity) {
|
|
1523
|
+
return this.embeddingProvider.similarity(a, b);
|
|
1524
|
+
}
|
|
1525
|
+
let dotProduct = 0;
|
|
1526
|
+
let normA = 0;
|
|
1527
|
+
let normB = 0;
|
|
1528
|
+
for (let i = 0; i < a.length; i++) {
|
|
1529
|
+
dotProduct += a[i] * b[i];
|
|
1530
|
+
normA += a[i] * a[i];
|
|
1531
|
+
normB += b[i] * b[i];
|
|
1532
|
+
}
|
|
1533
|
+
const denominator = Math.sqrt(normA) * Math.sqrt(normB);
|
|
1534
|
+
return denominator === 0 ? 0 : dotProduct / denominator;
|
|
1535
|
+
}
|
|
1536
|
+
};
|
|
1537
|
+
|
|
1538
|
+
// src/vocabulary/VocabularySchema.ts
|
|
1539
|
+
function createEmptyVocabulary(modifiedBy) {
|
|
1540
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1541
|
+
return {
|
|
1542
|
+
version: "0.0.0",
|
|
1543
|
+
lastModified: now,
|
|
1544
|
+
modifiedBy,
|
|
1545
|
+
entityTypes: [],
|
|
1546
|
+
relationshipTypes: []
|
|
1547
|
+
};
|
|
1548
|
+
}
|
|
1549
|
+
function createEntityTypeDefinition(input, createdBy) {
|
|
1550
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1551
|
+
return {
|
|
1552
|
+
type: input.type,
|
|
1553
|
+
description: input.description,
|
|
1554
|
+
version: "1.0.0",
|
|
1555
|
+
properties: input.properties ?? [],
|
|
1556
|
+
createdAt: now,
|
|
1557
|
+
createdBy,
|
|
1558
|
+
modifiedAt: now,
|
|
1559
|
+
modifiedBy: createdBy
|
|
1560
|
+
};
|
|
1561
|
+
}
|
|
1562
|
+
function createRelationshipTypeDefinition(input, createdBy) {
|
|
1563
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1564
|
+
return {
|
|
1565
|
+
type: toScreamingSnakeCase(input.type),
|
|
1566
|
+
description: input.description,
|
|
1567
|
+
version: "1.0.0",
|
|
1568
|
+
allowedSourceTypes: input.allowedSourceTypes,
|
|
1569
|
+
allowedTargetTypes: input.allowedTargetTypes,
|
|
1570
|
+
bidirectional: input.bidirectional ?? false,
|
|
1571
|
+
properties: input.properties,
|
|
1572
|
+
createdAt: now,
|
|
1573
|
+
createdBy,
|
|
1574
|
+
modifiedAt: now,
|
|
1575
|
+
modifiedBy: createdBy
|
|
1576
|
+
};
|
|
1577
|
+
}
|
|
1578
|
+
function buildVocabulary(input, createdBy) {
|
|
1579
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1580
|
+
return {
|
|
1581
|
+
version: "1.0.0",
|
|
1582
|
+
lastModified: now,
|
|
1583
|
+
modifiedBy: createdBy,
|
|
1584
|
+
entityTypes: (input.entityTypes ?? []).map((et) => createEntityTypeDefinition(et, createdBy)),
|
|
1585
|
+
relationshipTypes: (input.relationshipTypes ?? []).map(
|
|
1586
|
+
(rt) => createRelationshipTypeDefinition(rt, createdBy)
|
|
1587
|
+
)
|
|
1588
|
+
};
|
|
1589
|
+
}
|
|
1590
|
+
function incrementVersion(version, level = "patch") {
|
|
1591
|
+
const parts = version.split(".").map(Number);
|
|
1592
|
+
const major = parts[0] ?? 0;
|
|
1593
|
+
const minor = parts[1] ?? 0;
|
|
1594
|
+
const patch = parts[2] ?? 0;
|
|
1595
|
+
switch (level) {
|
|
1596
|
+
case "major":
|
|
1597
|
+
return `${major + 1}.0.0`;
|
|
1598
|
+
case "minor":
|
|
1599
|
+
return `${major}.${minor + 1}.0`;
|
|
1600
|
+
case "patch":
|
|
1601
|
+
return `${major}.${minor}.${patch + 1}`;
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
function mergeProperties(existing, addProperties, removeProperties, updateProperties) {
|
|
1605
|
+
let result = [...existing];
|
|
1606
|
+
if (removeProperties && removeProperties.length > 0) {
|
|
1607
|
+
const removeSet = new Set(removeProperties);
|
|
1608
|
+
result = result.filter((p) => !removeSet.has(p.name));
|
|
1609
|
+
}
|
|
1610
|
+
if (updateProperties && updateProperties.length > 0) {
|
|
1611
|
+
const updateMap = new Map(updateProperties.map((p) => [p.name, p]));
|
|
1612
|
+
result = result.map((p) => updateMap.get(p.name) ?? p);
|
|
1613
|
+
}
|
|
1614
|
+
if (addProperties && addProperties.length > 0) {
|
|
1615
|
+
const existingNames = new Set(result.map((p) => p.name));
|
|
1616
|
+
for (const prop of addProperties) {
|
|
1617
|
+
if (!existingNames.has(prop.name)) {
|
|
1618
|
+
result.push(prop);
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
return result;
|
|
1623
|
+
}
|
|
1624
|
+
function mergeEntityTypeEdit(existing, edit, modifiedBy) {
|
|
1625
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1626
|
+
return {
|
|
1627
|
+
...existing,
|
|
1628
|
+
description: edit.description ?? existing.description,
|
|
1629
|
+
version: incrementVersion(existing.version, "minor"),
|
|
1630
|
+
properties: mergeProperties(
|
|
1631
|
+
existing.properties,
|
|
1632
|
+
edit.addProperties,
|
|
1633
|
+
edit.removeProperties,
|
|
1634
|
+
edit.updateProperties
|
|
1635
|
+
),
|
|
1636
|
+
modifiedAt: now,
|
|
1637
|
+
modifiedBy
|
|
1638
|
+
};
|
|
1639
|
+
}
|
|
1640
|
+
function mergeRelationshipTypeEdit(existing, edit, modifiedBy) {
|
|
1641
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1642
|
+
return {
|
|
1643
|
+
...existing,
|
|
1644
|
+
description: edit.description ?? existing.description,
|
|
1645
|
+
allowedSourceTypes: edit.allowedSourceTypes ?? existing.allowedSourceTypes,
|
|
1646
|
+
allowedTargetTypes: edit.allowedTargetTypes ?? existing.allowedTargetTypes,
|
|
1647
|
+
bidirectional: edit.bidirectional ?? existing.bidirectional,
|
|
1648
|
+
version: incrementVersion(existing.version, "minor"),
|
|
1649
|
+
properties: mergeProperties(
|
|
1650
|
+
existing.properties ?? [],
|
|
1651
|
+
edit.addProperties,
|
|
1652
|
+
edit.removeProperties,
|
|
1653
|
+
edit.updateProperties
|
|
1654
|
+
),
|
|
1655
|
+
modifiedAt: now,
|
|
1656
|
+
modifiedBy
|
|
1657
|
+
};
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
// src/vocabulary/VocabularyGovernor.ts
|
|
1661
|
+
function canPropose(governanceConfig, _proposal) {
|
|
1662
|
+
switch (governanceConfig.mode) {
|
|
1663
|
+
case "locked":
|
|
1664
|
+
return {
|
|
1665
|
+
allowed: false,
|
|
1666
|
+
reason: "Vocabulary is locked. Only organisation admins can modify the vocabulary via the admin API."
|
|
1667
|
+
};
|
|
1668
|
+
case "managed":
|
|
1669
|
+
case "open":
|
|
1670
|
+
return { allowed: true };
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
function processProposal(vocabulary, proposal, governanceConfig, options) {
|
|
1674
|
+
const decision = canPropose(governanceConfig, proposal);
|
|
1675
|
+
if (!decision.allowed) {
|
|
1676
|
+
return {
|
|
1677
|
+
result: {
|
|
1678
|
+
status: "rejected",
|
|
1679
|
+
type: getProposedTypeName(proposal),
|
|
1680
|
+
reason: decision.reason
|
|
1681
|
+
}
|
|
1682
|
+
};
|
|
1683
|
+
}
|
|
1684
|
+
const isAddProposal = proposal.proposalType === "entity_type" || proposal.proposalType === "relationship_type";
|
|
1685
|
+
if (isAddProposal && options.duplicates && options.duplicates.length > 0) {
|
|
1686
|
+
return {
|
|
1687
|
+
result: {
|
|
1688
|
+
status: "rejected",
|
|
1689
|
+
type: getProposedTypeName(proposal),
|
|
1690
|
+
reason: `Similar type(s) already exist in the vocabulary`,
|
|
1691
|
+
duplicates: options.duplicates
|
|
1692
|
+
}
|
|
1693
|
+
};
|
|
1694
|
+
}
|
|
1695
|
+
if (governanceConfig.mode === "managed" && governanceConfig.requireApproval) {
|
|
1696
|
+
return {
|
|
1697
|
+
result: {
|
|
1698
|
+
status: "pending_approval",
|
|
1699
|
+
type: getProposedTypeName(proposal),
|
|
1700
|
+
proposalId: generateProposalId()
|
|
1701
|
+
}
|
|
1702
|
+
};
|
|
1703
|
+
}
|
|
1704
|
+
switch (proposal.proposalType) {
|
|
1705
|
+
case "entity_type":
|
|
1706
|
+
case "relationship_type":
|
|
1707
|
+
return applyAddProposal(vocabulary, proposal, options.proposedBy);
|
|
1708
|
+
case "edit_entity_type":
|
|
1709
|
+
case "edit_relationship_type":
|
|
1710
|
+
return applyEditProposal(vocabulary, proposal, options.proposedBy);
|
|
1711
|
+
case "delete_entity_type":
|
|
1712
|
+
case "delete_relationship_type":
|
|
1713
|
+
return applyDeleteProposal(vocabulary, proposal, options.proposedBy);
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
function applyAddProposal(vocabulary, proposal, proposedBy) {
|
|
1717
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1718
|
+
const newVersion = incrementVersion(vocabulary.version, "minor");
|
|
1719
|
+
const typeName = getProposedTypeName(proposal);
|
|
1720
|
+
let updatedVocabulary;
|
|
1721
|
+
let changeType;
|
|
1722
|
+
if (proposal.proposalType === "entity_type" && proposal.entityType) {
|
|
1723
|
+
const newType = createEntityTypeDefinition(proposal.entityType, proposedBy);
|
|
1724
|
+
updatedVocabulary = {
|
|
1725
|
+
...vocabulary,
|
|
1726
|
+
version: newVersion,
|
|
1727
|
+
lastModified: now,
|
|
1728
|
+
modifiedBy: proposedBy,
|
|
1729
|
+
entityTypes: [...vocabulary.entityTypes, newType]
|
|
1730
|
+
};
|
|
1731
|
+
changeType = "entity_type_added";
|
|
1732
|
+
} else if (proposal.proposalType === "relationship_type" && proposal.relationshipType) {
|
|
1733
|
+
const newType = createRelationshipTypeDefinition(
|
|
1734
|
+
{
|
|
1735
|
+
type: proposal.relationshipType.type,
|
|
1736
|
+
description: proposal.relationshipType.description,
|
|
1737
|
+
allowedSourceTypes: proposal.relationshipType.allowedSourceTypes,
|
|
1738
|
+
allowedTargetTypes: proposal.relationshipType.allowedTargetTypes,
|
|
1739
|
+
bidirectional: proposal.relationshipType.bidirectional,
|
|
1740
|
+
properties: proposal.relationshipType.properties
|
|
1741
|
+
},
|
|
1742
|
+
proposedBy
|
|
1743
|
+
);
|
|
1744
|
+
updatedVocabulary = {
|
|
1745
|
+
...vocabulary,
|
|
1746
|
+
version: newVersion,
|
|
1747
|
+
lastModified: now,
|
|
1748
|
+
modifiedBy: proposedBy,
|
|
1749
|
+
relationshipTypes: [...vocabulary.relationshipTypes, newType]
|
|
1750
|
+
};
|
|
1751
|
+
changeType = "relationship_type_added";
|
|
1752
|
+
} else {
|
|
1753
|
+
return {
|
|
1754
|
+
result: {
|
|
1755
|
+
status: "rejected",
|
|
1756
|
+
type: typeName,
|
|
1757
|
+
reason: "Invalid proposal: missing type definition for the specified proposal type"
|
|
1758
|
+
},
|
|
1759
|
+
updatedVocabulary: vocabulary,
|
|
1760
|
+
changeRecord: {
|
|
1761
|
+
changeId: generateChangeId(),
|
|
1762
|
+
changeType: "entity_type_added",
|
|
1763
|
+
typeName,
|
|
1764
|
+
newVersion: vocabulary.version,
|
|
1765
|
+
proposedBy,
|
|
1766
|
+
proposedAt: now,
|
|
1767
|
+
reason: proposal.justification
|
|
1768
|
+
}
|
|
1769
|
+
};
|
|
1770
|
+
}
|
|
1771
|
+
const changeRecord = {
|
|
1772
|
+
changeId: generateChangeId(),
|
|
1773
|
+
changeType,
|
|
1774
|
+
typeName,
|
|
1775
|
+
previousVersion: vocabulary.version,
|
|
1776
|
+
newVersion,
|
|
1777
|
+
proposedBy,
|
|
1778
|
+
proposedAt: now,
|
|
1779
|
+
approvedAt: now,
|
|
1780
|
+
reason: proposal.justification
|
|
1781
|
+
};
|
|
1782
|
+
return {
|
|
1783
|
+
result: { status: "approved", type: typeName, vocabularyVersion: newVersion },
|
|
1784
|
+
updatedVocabulary,
|
|
1785
|
+
changeRecord
|
|
1786
|
+
};
|
|
1787
|
+
}
|
|
1788
|
+
function applyEditProposal(vocabulary, proposal, proposedBy) {
|
|
1789
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1790
|
+
const typeName = getProposedTypeName(proposal);
|
|
1791
|
+
const newVersion = incrementVersion(vocabulary.version, "minor");
|
|
1792
|
+
let updatedVocabulary;
|
|
1793
|
+
let changeType;
|
|
1794
|
+
if (proposal.proposalType === "edit_entity_type" && proposal.editEntityType) {
|
|
1795
|
+
const existing = vocabulary.entityTypes.find((et) => et.type === proposal.editEntityType.type);
|
|
1796
|
+
if (!existing) {
|
|
1797
|
+
return {
|
|
1798
|
+
result: {
|
|
1799
|
+
status: "rejected",
|
|
1800
|
+
type: typeName,
|
|
1801
|
+
reason: `Entity type "${typeName}" not found in vocabulary`
|
|
1802
|
+
}
|
|
1803
|
+
};
|
|
1804
|
+
}
|
|
1805
|
+
const merged = mergeEntityTypeEdit(existing, proposal.editEntityType, proposedBy);
|
|
1806
|
+
updatedVocabulary = {
|
|
1807
|
+
...vocabulary,
|
|
1808
|
+
version: newVersion,
|
|
1809
|
+
lastModified: now,
|
|
1810
|
+
modifiedBy: proposedBy,
|
|
1811
|
+
entityTypes: vocabulary.entityTypes.map((et) => et.type === typeName ? merged : et)
|
|
1812
|
+
};
|
|
1813
|
+
changeType = "entity_type_modified";
|
|
1814
|
+
} else if (proposal.proposalType === "edit_relationship_type" && proposal.editRelationshipType) {
|
|
1815
|
+
const existing = vocabulary.relationshipTypes.find(
|
|
1816
|
+
(rt) => rt.type === proposal.editRelationshipType.type
|
|
1817
|
+
);
|
|
1818
|
+
if (!existing) {
|
|
1819
|
+
return {
|
|
1820
|
+
result: {
|
|
1821
|
+
status: "rejected",
|
|
1822
|
+
type: typeName,
|
|
1823
|
+
reason: `Relationship type "${typeName}" not found in vocabulary`
|
|
1824
|
+
}
|
|
1825
|
+
};
|
|
1826
|
+
}
|
|
1827
|
+
const merged = mergeRelationshipTypeEdit(existing, proposal.editRelationshipType, proposedBy);
|
|
1828
|
+
updatedVocabulary = {
|
|
1829
|
+
...vocabulary,
|
|
1830
|
+
version: newVersion,
|
|
1831
|
+
lastModified: now,
|
|
1832
|
+
modifiedBy: proposedBy,
|
|
1833
|
+
relationshipTypes: vocabulary.relationshipTypes.map(
|
|
1834
|
+
(rt) => rt.type === typeName ? merged : rt
|
|
1835
|
+
)
|
|
1836
|
+
};
|
|
1837
|
+
changeType = "relationship_type_modified";
|
|
1838
|
+
} else {
|
|
1839
|
+
return {
|
|
1840
|
+
result: {
|
|
1841
|
+
status: "rejected",
|
|
1842
|
+
type: typeName,
|
|
1843
|
+
reason: "Invalid proposal: missing edit definition for the specified proposal type"
|
|
1844
|
+
}
|
|
1845
|
+
};
|
|
1846
|
+
}
|
|
1847
|
+
const changeRecord = {
|
|
1848
|
+
changeId: generateChangeId(),
|
|
1849
|
+
changeType,
|
|
1850
|
+
typeName,
|
|
1851
|
+
previousVersion: vocabulary.version,
|
|
1852
|
+
newVersion,
|
|
1853
|
+
proposedBy,
|
|
1854
|
+
proposedAt: now,
|
|
1855
|
+
approvedAt: now,
|
|
1856
|
+
reason: proposal.justification
|
|
1857
|
+
};
|
|
1858
|
+
return {
|
|
1859
|
+
result: { status: "approved", type: typeName, vocabularyVersion: newVersion },
|
|
1860
|
+
updatedVocabulary,
|
|
1861
|
+
changeRecord
|
|
1862
|
+
};
|
|
1863
|
+
}
|
|
1864
|
+
function applyDeleteProposal(vocabulary, proposal, proposedBy) {
|
|
1865
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1866
|
+
const typeName = getProposedTypeName(proposal);
|
|
1867
|
+
const newVersion = incrementVersion(vocabulary.version, "major");
|
|
1868
|
+
let updatedVocabulary;
|
|
1869
|
+
let changeType;
|
|
1870
|
+
if (proposal.proposalType === "delete_entity_type" && proposal.deleteEntityType) {
|
|
1871
|
+
const exists = vocabulary.entityTypes.some((et) => et.type === proposal.deleteEntityType.type);
|
|
1872
|
+
if (!exists) {
|
|
1873
|
+
return {
|
|
1874
|
+
result: {
|
|
1875
|
+
status: "rejected",
|
|
1876
|
+
type: typeName,
|
|
1877
|
+
reason: `Entity type "${typeName}" not found in vocabulary`
|
|
1878
|
+
}
|
|
1879
|
+
};
|
|
1880
|
+
}
|
|
1881
|
+
updatedVocabulary = {
|
|
1882
|
+
...vocabulary,
|
|
1883
|
+
version: newVersion,
|
|
1884
|
+
lastModified: now,
|
|
1885
|
+
modifiedBy: proposedBy,
|
|
1886
|
+
entityTypes: vocabulary.entityTypes.filter((et) => et.type !== typeName)
|
|
1887
|
+
};
|
|
1888
|
+
changeType = "entity_type_removed";
|
|
1889
|
+
} else if (proposal.proposalType === "delete_relationship_type" && proposal.deleteRelationshipType) {
|
|
1890
|
+
const exists = vocabulary.relationshipTypes.some(
|
|
1891
|
+
(rt) => rt.type === proposal.deleteRelationshipType.type
|
|
1892
|
+
);
|
|
1893
|
+
if (!exists) {
|
|
1894
|
+
return {
|
|
1895
|
+
result: {
|
|
1896
|
+
status: "rejected",
|
|
1897
|
+
type: typeName,
|
|
1898
|
+
reason: `Relationship type "${typeName}" not found in vocabulary`
|
|
1899
|
+
}
|
|
1900
|
+
};
|
|
1901
|
+
}
|
|
1902
|
+
updatedVocabulary = {
|
|
1903
|
+
...vocabulary,
|
|
1904
|
+
version: newVersion,
|
|
1905
|
+
lastModified: now,
|
|
1906
|
+
modifiedBy: proposedBy,
|
|
1907
|
+
relationshipTypes: vocabulary.relationshipTypes.filter((rt) => rt.type !== typeName)
|
|
1908
|
+
};
|
|
1909
|
+
changeType = "relationship_type_removed";
|
|
1910
|
+
} else {
|
|
1911
|
+
return {
|
|
1912
|
+
result: {
|
|
1913
|
+
status: "rejected",
|
|
1914
|
+
type: typeName,
|
|
1915
|
+
reason: "Invalid proposal: missing delete definition for the specified proposal type"
|
|
1916
|
+
}
|
|
1917
|
+
};
|
|
1918
|
+
}
|
|
1919
|
+
const changeRecord = {
|
|
1920
|
+
changeId: generateChangeId(),
|
|
1921
|
+
changeType,
|
|
1922
|
+
typeName,
|
|
1923
|
+
previousVersion: vocabulary.version,
|
|
1924
|
+
newVersion,
|
|
1925
|
+
proposedBy,
|
|
1926
|
+
proposedAt: now,
|
|
1927
|
+
approvedAt: now,
|
|
1928
|
+
reason: proposal.justification
|
|
1929
|
+
};
|
|
1930
|
+
return {
|
|
1931
|
+
result: { status: "approved", type: typeName, vocabularyVersion: newVersion },
|
|
1932
|
+
updatedVocabulary,
|
|
1933
|
+
changeRecord
|
|
1934
|
+
};
|
|
1935
|
+
}
|
|
1936
|
+
function getProposedTypeName(proposal) {
|
|
1937
|
+
switch (proposal.proposalType) {
|
|
1938
|
+
case "entity_type":
|
|
1939
|
+
return proposal.entityType?.type ?? "unknown";
|
|
1940
|
+
case "relationship_type":
|
|
1941
|
+
return proposal.relationshipType?.type ?? "unknown";
|
|
1942
|
+
case "edit_entity_type":
|
|
1943
|
+
return proposal.editEntityType?.type ?? "unknown";
|
|
1944
|
+
case "edit_relationship_type":
|
|
1945
|
+
return proposal.editRelationshipType?.type ?? "unknown";
|
|
1946
|
+
case "delete_entity_type":
|
|
1947
|
+
return proposal.deleteEntityType?.type ?? "unknown";
|
|
1948
|
+
case "delete_relationship_type":
|
|
1949
|
+
return proposal.deleteRelationshipType?.type ?? "unknown";
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
function generateProposalId() {
|
|
1953
|
+
return `proposal_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
1954
|
+
}
|
|
1955
|
+
function generateChangeId() {
|
|
1956
|
+
return `change_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
// src/vocabulary/VocabularyValidator.ts
|
|
1960
|
+
function ok() {
|
|
1961
|
+
return { valid: true, errors: [] };
|
|
1962
|
+
}
|
|
1963
|
+
function fail(...errors) {
|
|
1964
|
+
return { valid: false, errors };
|
|
1965
|
+
}
|
|
1966
|
+
function merge(...results) {
|
|
1967
|
+
const errors = results.flatMap((r) => r.errors);
|
|
1968
|
+
return { valid: errors.length === 0, errors };
|
|
1969
|
+
}
|
|
1970
|
+
function findClosestType(typeName, types) {
|
|
1971
|
+
if (types.length === 0) return void 0;
|
|
1972
|
+
const lower = typeName.toLowerCase();
|
|
1973
|
+
const match = types.find(
|
|
1974
|
+
(t) => t.type.toLowerCase().includes(lower) || lower.includes(t.type.toLowerCase())
|
|
1975
|
+
);
|
|
1976
|
+
return match?.type;
|
|
1977
|
+
}
|
|
1978
|
+
function validatePropertyValue(name, value, schema) {
|
|
1979
|
+
if (value === void 0 || value === null) {
|
|
1980
|
+
if (schema.required) {
|
|
1981
|
+
return fail({
|
|
1982
|
+
field: `properties.${name}`,
|
|
1983
|
+
message: `Required property "${name}" is missing`
|
|
1984
|
+
});
|
|
1985
|
+
}
|
|
1986
|
+
return ok();
|
|
1987
|
+
}
|
|
1988
|
+
switch (schema.type) {
|
|
1989
|
+
case "string":
|
|
1990
|
+
if (typeof value !== "string") {
|
|
1991
|
+
return fail({
|
|
1992
|
+
field: `properties.${name}`,
|
|
1993
|
+
message: `Property "${name}" must be a string, got ${typeof value}`
|
|
1994
|
+
});
|
|
1995
|
+
}
|
|
1996
|
+
break;
|
|
1997
|
+
case "number":
|
|
1998
|
+
if (typeof value !== "number" || Number.isNaN(value)) {
|
|
1999
|
+
return fail({
|
|
2000
|
+
field: `properties.${name}`,
|
|
2001
|
+
message: `Property "${name}" must be a number, got ${typeof value}`
|
|
2002
|
+
});
|
|
2003
|
+
}
|
|
2004
|
+
break;
|
|
2005
|
+
case "boolean":
|
|
2006
|
+
if (typeof value !== "boolean") {
|
|
2007
|
+
return fail({
|
|
2008
|
+
field: `properties.${name}`,
|
|
2009
|
+
message: `Property "${name}" must be a boolean, got ${typeof value}`
|
|
2010
|
+
});
|
|
2011
|
+
}
|
|
2012
|
+
break;
|
|
2013
|
+
case "date":
|
|
2014
|
+
if (typeof value !== "string" || Number.isNaN(Date.parse(value))) {
|
|
2015
|
+
return fail({
|
|
2016
|
+
field: `properties.${name}`,
|
|
2017
|
+
message: `Property "${name}" must be a valid ISO date string`
|
|
2018
|
+
});
|
|
2019
|
+
}
|
|
2020
|
+
break;
|
|
2021
|
+
case "enum":
|
|
2022
|
+
if (typeof value !== "string") {
|
|
2023
|
+
return fail({
|
|
2024
|
+
field: `properties.${name}`,
|
|
2025
|
+
message: `Property "${name}" must be a string (enum value), got ${typeof value}`
|
|
2026
|
+
});
|
|
2027
|
+
}
|
|
2028
|
+
if (schema.enumValues && !schema.enumValues.includes(value)) {
|
|
2029
|
+
return fail({
|
|
2030
|
+
field: `properties.${name}`,
|
|
2031
|
+
message: `Property "${name}" must be one of: ${schema.enumValues.join(", ")}`,
|
|
2032
|
+
suggestion: `Valid values: ${schema.enumValues.join(", ")}`
|
|
2033
|
+
});
|
|
2034
|
+
}
|
|
2035
|
+
break;
|
|
2036
|
+
}
|
|
2037
|
+
return ok();
|
|
2038
|
+
}
|
|
2039
|
+
function validateProperties(properties, schemas, context) {
|
|
2040
|
+
const results = [];
|
|
2041
|
+
const providedProperties = properties ?? {};
|
|
2042
|
+
for (const schema of schemas) {
|
|
2043
|
+
const value = providedProperties[schema.name];
|
|
2044
|
+
results.push(validatePropertyValue(schema.name, value, schema));
|
|
2045
|
+
}
|
|
2046
|
+
const knownNames = new Set(schemas.map((s) => s.name));
|
|
2047
|
+
for (const key of Object.keys(providedProperties)) {
|
|
2048
|
+
if (!knownNames.has(key)) {
|
|
2049
|
+
results.push(
|
|
2050
|
+
fail({
|
|
2051
|
+
field: `properties.${key}`,
|
|
2052
|
+
message: `Unknown property "${key}" on ${context}`,
|
|
2053
|
+
suggestion: `Known properties: ${schemas.map((s) => s.name).join(", ") || "none"}`
|
|
2054
|
+
})
|
|
2055
|
+
);
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
return merge(...results);
|
|
2059
|
+
}
|
|
2060
|
+
function validateEntity(input, vocabulary) {
|
|
2061
|
+
const entityTypeDef = vocabulary.entityTypes.find((et) => et.type === input.entityType);
|
|
2062
|
+
if (!entityTypeDef) {
|
|
2063
|
+
const closest = findClosestType(input.entityType, vocabulary.entityTypes);
|
|
2064
|
+
return fail({
|
|
2065
|
+
field: "entityType",
|
|
2066
|
+
message: `Entity type "${input.entityType}" does not exist in the vocabulary`,
|
|
2067
|
+
suggestion: closest ? `Did you mean "${closest}"?` : `Available types: ${vocabulary.entityTypes.map((et) => et.type).join(", ") || "none"}`
|
|
2068
|
+
});
|
|
2069
|
+
}
|
|
2070
|
+
if (!input.label || input.label.trim().length === 0) {
|
|
2071
|
+
return fail({
|
|
2072
|
+
field: "label",
|
|
2073
|
+
message: "Entity label is required and cannot be empty"
|
|
2074
|
+
});
|
|
2075
|
+
}
|
|
2076
|
+
return validateProperties(input.properties, entityTypeDef.properties, input.entityType);
|
|
2077
|
+
}
|
|
2078
|
+
function validateEntityUpdate(input, entityTypeDef) {
|
|
2079
|
+
if (input.label !== void 0 && input.label.trim().length === 0) {
|
|
2080
|
+
return fail({
|
|
2081
|
+
field: "label",
|
|
2082
|
+
message: "Entity label cannot be empty"
|
|
2083
|
+
});
|
|
2084
|
+
}
|
|
2085
|
+
if (input.properties) {
|
|
2086
|
+
const results = [];
|
|
2087
|
+
for (const [key, value] of Object.entries(input.properties)) {
|
|
2088
|
+
const schema = entityTypeDef.properties.find((p) => p.name === key);
|
|
2089
|
+
if (!schema) {
|
|
2090
|
+
results.push(
|
|
2091
|
+
fail({
|
|
2092
|
+
field: `properties.${key}`,
|
|
2093
|
+
message: `Unknown property "${key}" on ${entityTypeDef.type}`,
|
|
2094
|
+
suggestion: `Known properties: ${entityTypeDef.properties.map((p) => p.name).join(", ") || "none"}`
|
|
2095
|
+
})
|
|
2096
|
+
);
|
|
2097
|
+
} else {
|
|
2098
|
+
results.push(validatePropertyValue(key, value, schema));
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
return merge(...results);
|
|
2102
|
+
}
|
|
2103
|
+
return ok();
|
|
2104
|
+
}
|
|
2105
|
+
function validateRelationship(input, vocabulary, sourceEntityType, targetEntityType) {
|
|
2106
|
+
const normalisedType = toScreamingSnakeCase(input.relationshipType);
|
|
2107
|
+
const relTypeDef = vocabulary.relationshipTypes.find(
|
|
2108
|
+
(rt) => rt.type === normalisedType
|
|
2109
|
+
);
|
|
2110
|
+
if (!relTypeDef) {
|
|
2111
|
+
const closest = findClosestType(input.relationshipType, vocabulary.relationshipTypes);
|
|
2112
|
+
return fail({
|
|
2113
|
+
field: "relationshipType",
|
|
2114
|
+
message: `Relationship type "${input.relationshipType}" does not exist in the vocabulary`,
|
|
2115
|
+
suggestion: closest ? `Did you mean "${closest}"?` : `Available types: ${vocabulary.relationshipTypes.map((rt) => rt.type).join(", ") || "none"}`
|
|
2116
|
+
});
|
|
2117
|
+
}
|
|
2118
|
+
const results = [];
|
|
2119
|
+
if (relTypeDef.allowedSourceTypes.length > 0 && !relTypeDef.allowedSourceTypes.includes(sourceEntityType)) {
|
|
2120
|
+
results.push(
|
|
2121
|
+
fail({
|
|
2122
|
+
field: "sourceEntityId",
|
|
2123
|
+
message: `Entity type "${sourceEntityType}" is not allowed as source for relationship type "${input.relationshipType}"`,
|
|
2124
|
+
suggestion: `Allowed source types: ${relTypeDef.allowedSourceTypes.join(", ")}`
|
|
2125
|
+
})
|
|
2126
|
+
);
|
|
2127
|
+
}
|
|
2128
|
+
if (relTypeDef.allowedTargetTypes.length > 0 && !relTypeDef.allowedTargetTypes.includes(targetEntityType)) {
|
|
2129
|
+
results.push(
|
|
2130
|
+
fail({
|
|
2131
|
+
field: "targetEntityId",
|
|
2132
|
+
message: `Entity type "${targetEntityType}" is not allowed as target for relationship type "${input.relationshipType}"`,
|
|
2133
|
+
suggestion: `Allowed target types: ${relTypeDef.allowedTargetTypes.join(", ")}`
|
|
2134
|
+
})
|
|
2135
|
+
);
|
|
2136
|
+
}
|
|
2137
|
+
if (relTypeDef.properties) {
|
|
2138
|
+
results.push(
|
|
2139
|
+
validateProperties(input.properties, relTypeDef.properties, input.relationshipType)
|
|
2140
|
+
);
|
|
2141
|
+
}
|
|
2142
|
+
return merge(...results);
|
|
2143
|
+
}
|
|
2144
|
+
function getEntityTypeDef(entityType, vocabulary) {
|
|
2145
|
+
return vocabulary.entityTypes.find((et) => et.type === entityType) ?? null;
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
// src/core/VocabularyEngine.ts
|
|
2149
|
+
var VocabularyEngine = class {
|
|
2150
|
+
repositoryId;
|
|
2151
|
+
storage;
|
|
2152
|
+
governanceConfig;
|
|
2153
|
+
deduplicator;
|
|
2154
|
+
/** Cached vocabulary — invalidated on mutation */
|
|
2155
|
+
cachedVocabulary = null;
|
|
2156
|
+
constructor(config) {
|
|
2157
|
+
this.repositoryId = config.repositoryId;
|
|
2158
|
+
this.storage = config.storageProvider;
|
|
2159
|
+
this.governanceConfig = config.governanceConfig;
|
|
2160
|
+
this.deduplicator = new SemanticDeduplicator({
|
|
2161
|
+
similarityThreshold: config.deduplicationThreshold,
|
|
2162
|
+
embeddingProvider: config.embeddingProvider
|
|
2163
|
+
});
|
|
2164
|
+
}
|
|
2165
|
+
/** Get the governance configuration for this repository */
|
|
2166
|
+
getGovernanceConfig() {
|
|
2167
|
+
return this.governanceConfig;
|
|
2168
|
+
}
|
|
2169
|
+
/** Get the current vocabulary from storage (cached) */
|
|
2170
|
+
async getVocabulary() {
|
|
2171
|
+
if (!this.cachedVocabulary) {
|
|
2172
|
+
this.cachedVocabulary = await this.storage.getVocabulary(this.repositoryId);
|
|
2173
|
+
}
|
|
2174
|
+
return this.cachedVocabulary;
|
|
2175
|
+
}
|
|
2176
|
+
/** Get the resolved vocabulary with governance info */
|
|
2177
|
+
async getResolvedVocabulary() {
|
|
2178
|
+
const vocabulary = await this.getVocabulary();
|
|
2179
|
+
return {
|
|
2180
|
+
vocabulary,
|
|
2181
|
+
governanceMode: this.governanceConfig.mode,
|
|
2182
|
+
governanceConfig: this.governanceConfig
|
|
2183
|
+
};
|
|
2184
|
+
}
|
|
2185
|
+
/** Invalidate the cached vocabulary (call after external mutations) */
|
|
2186
|
+
invalidateCache() {
|
|
2187
|
+
this.cachedVocabulary = null;
|
|
2188
|
+
}
|
|
2189
|
+
/** Validate an entity creation input against the vocabulary */
|
|
2190
|
+
async validateEntity(input) {
|
|
2191
|
+
const vocabulary = await this.getVocabulary();
|
|
2192
|
+
return validateEntity(input, vocabulary);
|
|
2193
|
+
}
|
|
2194
|
+
/** Validate an entity update input against the vocabulary */
|
|
2195
|
+
async validateEntityUpdate(input, entityType) {
|
|
2196
|
+
const vocabulary = await this.getVocabulary();
|
|
2197
|
+
const typeDef = getEntityTypeDef(entityType, vocabulary);
|
|
2198
|
+
if (!typeDef) {
|
|
2199
|
+
return {
|
|
2200
|
+
valid: false,
|
|
2201
|
+
errors: [
|
|
2202
|
+
{
|
|
2203
|
+
field: "entityType",
|
|
2204
|
+
message: `Entity type "${entityType}" does not exist in the vocabulary`
|
|
2205
|
+
}
|
|
2206
|
+
]
|
|
2207
|
+
};
|
|
2208
|
+
}
|
|
2209
|
+
return validateEntityUpdate(input, typeDef);
|
|
2210
|
+
}
|
|
2211
|
+
/** Validate a relationship creation input against the vocabulary */
|
|
2212
|
+
async validateRelationship(input, sourceEntityType, targetEntityType) {
|
|
2213
|
+
const vocabulary = await this.getVocabulary();
|
|
2214
|
+
return validateRelationship(input, vocabulary, sourceEntityType, targetEntityType);
|
|
2215
|
+
}
|
|
2216
|
+
/** Get an entity type definition from the vocabulary */
|
|
2217
|
+
async getEntityTypeDef(entityType) {
|
|
2218
|
+
const vocabulary = await this.getVocabulary();
|
|
2219
|
+
return getEntityTypeDef(entityType, vocabulary);
|
|
2220
|
+
}
|
|
2221
|
+
/**
|
|
2222
|
+
* Propose a vocabulary change (add, edit, or delete).
|
|
2223
|
+
* Runs deduplication for add proposals, then governance rules.
|
|
2224
|
+
* For delete proposals, cascades data deletion if approved.
|
|
2225
|
+
*/
|
|
2226
|
+
async proposeChange(proposal, proposedBy) {
|
|
2227
|
+
const vocabulary = await this.getVocabulary();
|
|
2228
|
+
const isAddProposal = proposal.proposalType === "entity_type" || proposal.proposalType === "relationship_type";
|
|
2229
|
+
let duplicates;
|
|
2230
|
+
if (isAddProposal) {
|
|
2231
|
+
const existingTypes = this.getExistingTypesForProposal(proposal, vocabulary);
|
|
2232
|
+
const proposedTypeName = this.getProposedTypeName(proposal);
|
|
2233
|
+
const proposedDescription = this.getProposedDescription(proposal);
|
|
2234
|
+
const skipDedup = this.governanceConfig.mode === "open" && this.governanceConfig.deduplicationEnabled === false;
|
|
2235
|
+
if (!skipDedup) {
|
|
2236
|
+
const dedupResult = await this.deduplicator.checkDuplicate(
|
|
2237
|
+
proposedTypeName,
|
|
2238
|
+
proposedDescription,
|
|
2239
|
+
existingTypes
|
|
2240
|
+
);
|
|
2241
|
+
if (dedupResult.isDuplicate) {
|
|
2242
|
+
duplicates = dedupResult.matches;
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
const { result, updatedVocabulary } = processProposal(
|
|
2247
|
+
vocabulary,
|
|
2248
|
+
proposal,
|
|
2249
|
+
this.governanceConfig,
|
|
2250
|
+
{ duplicates, proposedBy }
|
|
2251
|
+
);
|
|
2252
|
+
if (result.status === "approved" && updatedVocabulary) {
|
|
2253
|
+
const isDeleteProposal = proposal.proposalType === "delete_entity_type" || proposal.proposalType === "delete_relationship_type";
|
|
2254
|
+
if (isDeleteProposal) {
|
|
2255
|
+
await this.cascadeDeleteData(proposal);
|
|
2256
|
+
}
|
|
2257
|
+
await this.storage.saveVocabulary(this.repositoryId, updatedVocabulary);
|
|
2258
|
+
this.cachedVocabulary = updatedVocabulary;
|
|
2259
|
+
}
|
|
2260
|
+
return result;
|
|
2261
|
+
}
|
|
2262
|
+
/** @deprecated Use proposeChange instead */
|
|
2263
|
+
async proposeExtension(proposal, proposedBy) {
|
|
2264
|
+
return this.proposeChange(proposal, proposedBy);
|
|
2265
|
+
}
|
|
2266
|
+
/** Cascade-delete all data for a deleted vocabulary type */
|
|
2267
|
+
async cascadeDeleteData(proposal) {
|
|
2268
|
+
if (proposal.proposalType === "delete_entity_type" && proposal.deleteEntityType) {
|
|
2269
|
+
return this.storage.deleteEntitiesByType(
|
|
2270
|
+
this.repositoryId,
|
|
2271
|
+
proposal.deleteEntityType.type
|
|
2272
|
+
);
|
|
2273
|
+
}
|
|
2274
|
+
if (proposal.proposalType === "delete_relationship_type" && proposal.deleteRelationshipType) {
|
|
2275
|
+
const result = await this.storage.deleteRelationshipsByType(
|
|
2276
|
+
this.repositoryId,
|
|
2277
|
+
proposal.deleteRelationshipType.type
|
|
2278
|
+
);
|
|
2279
|
+
return { deletedEntities: 0, deletedRelationships: result.deletedRelationships };
|
|
2280
|
+
}
|
|
2281
|
+
return { deletedEntities: 0, deletedRelationships: 0 };
|
|
2282
|
+
}
|
|
2283
|
+
getExistingTypesForProposal(proposal, vocabulary) {
|
|
2284
|
+
if (proposal.proposalType === "entity_type") {
|
|
2285
|
+
return vocabulary.entityTypes.map((et) => ({
|
|
2286
|
+
type: et.type,
|
|
2287
|
+
description: et.description
|
|
2288
|
+
}));
|
|
2289
|
+
}
|
|
2290
|
+
return vocabulary.relationshipTypes.map((rt) => ({
|
|
2291
|
+
type: rt.type,
|
|
2292
|
+
description: rt.description
|
|
2293
|
+
}));
|
|
2294
|
+
}
|
|
2295
|
+
getProposedTypeName(proposal) {
|
|
2296
|
+
if (proposal.proposalType === "entity_type") {
|
|
2297
|
+
return proposal.entityType?.type ?? "";
|
|
2298
|
+
}
|
|
2299
|
+
return proposal.relationshipType?.type ?? "";
|
|
2300
|
+
}
|
|
2301
|
+
getProposedDescription(proposal) {
|
|
2302
|
+
if (proposal.proposalType === "entity_type") {
|
|
2303
|
+
return proposal.entityType?.description ?? "";
|
|
2304
|
+
}
|
|
2305
|
+
return proposal.relationshipType?.description ?? "";
|
|
2306
|
+
}
|
|
2307
|
+
};
|
|
2308
|
+
|
|
2309
|
+
// src/core/ProvenanceTracker.ts
|
|
2310
|
+
var ProvenanceTracker = class {
|
|
2311
|
+
context;
|
|
2312
|
+
constructor(context) {
|
|
2313
|
+
this.context = { ...context };
|
|
2314
|
+
}
|
|
2315
|
+
/** Update the active provenance context (e.g., different conversation) */
|
|
2316
|
+
updateContext(newContext) {
|
|
2317
|
+
this.context = { ...this.context, ...newContext };
|
|
2318
|
+
}
|
|
2319
|
+
/** Get the current provenance context */
|
|
2320
|
+
getContext() {
|
|
2321
|
+
return { ...this.context };
|
|
2322
|
+
}
|
|
2323
|
+
/** Stamp a newly created entity or relationship with full provenance */
|
|
2324
|
+
stampCreate() {
|
|
2325
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2326
|
+
return {
|
|
2327
|
+
createdBy: this.context.actorId,
|
|
2328
|
+
createdByType: this.context.actorType,
|
|
2329
|
+
createdAt: now,
|
|
2330
|
+
createdInConversation: this.context.conversationId,
|
|
2331
|
+
createdFromMessage: this.context.messageId,
|
|
2332
|
+
modifiedBy: this.context.actorId,
|
|
2333
|
+
modifiedByType: this.context.actorType,
|
|
2334
|
+
modifiedAt: now,
|
|
2335
|
+
modifiedInConversation: this.context.conversationId,
|
|
2336
|
+
modifiedFromMessage: this.context.messageId
|
|
2337
|
+
};
|
|
2338
|
+
}
|
|
2339
|
+
/** Stamp an updated entity or relationship — preserves creation fields, updates modified fields */
|
|
2340
|
+
stampUpdate(existing) {
|
|
2341
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2342
|
+
return {
|
|
2343
|
+
...existing,
|
|
2344
|
+
modifiedBy: this.context.actorId,
|
|
2345
|
+
modifiedByType: this.context.actorType,
|
|
2346
|
+
modifiedAt: now,
|
|
2347
|
+
modifiedInConversation: this.context.conversationId,
|
|
2348
|
+
modifiedFromMessage: this.context.messageId
|
|
2349
|
+
};
|
|
2350
|
+
}
|
|
2351
|
+
};
|
|
2352
|
+
|
|
2353
|
+
// src/core/EventBus.ts
|
|
2354
|
+
var EventBus = class {
|
|
2355
|
+
handlers = /* @__PURE__ */ new Map();
|
|
2356
|
+
hookHandlers = /* @__PURE__ */ new Map();
|
|
2357
|
+
provenance;
|
|
2358
|
+
repositoryId;
|
|
2359
|
+
constructor(provenance, repositoryId) {
|
|
2360
|
+
this.provenance = provenance;
|
|
2361
|
+
this.repositoryId = repositoryId;
|
|
2362
|
+
}
|
|
2363
|
+
/** Update provenance context (e.g., when conversation changes) */
|
|
2364
|
+
updateProvenance(provenance) {
|
|
2365
|
+
this.provenance = provenance;
|
|
2366
|
+
}
|
|
2367
|
+
/** Subscribe to an event. Returns an unsubscribe function. */
|
|
2368
|
+
on(event, handler) {
|
|
2369
|
+
if (!this.handlers.has(event)) {
|
|
2370
|
+
this.handlers.set(event, /* @__PURE__ */ new Set());
|
|
2371
|
+
}
|
|
2372
|
+
this.handlers.get(event).add(handler);
|
|
2373
|
+
return () => {
|
|
2374
|
+
this.handlers.get(event)?.delete(handler);
|
|
2375
|
+
};
|
|
2376
|
+
}
|
|
2377
|
+
/** Register a pre-mutation hook that can cancel operations */
|
|
2378
|
+
onHook(event, handler) {
|
|
2379
|
+
if (!this.hookHandlers.has(event)) {
|
|
2380
|
+
this.hookHandlers.set(event, /* @__PURE__ */ new Set());
|
|
2381
|
+
}
|
|
2382
|
+
this.hookHandlers.get(event).add(handler);
|
|
2383
|
+
return () => {
|
|
2384
|
+
this.hookHandlers.get(event)?.delete(handler);
|
|
2385
|
+
};
|
|
2386
|
+
}
|
|
2387
|
+
/** Emit an event to all registered handlers */
|
|
2388
|
+
async emit(event, payload) {
|
|
2389
|
+
const handlers = this.handlers.get(event);
|
|
2390
|
+
if (!handlers || handlers.size === 0) return;
|
|
2391
|
+
const eventObj = {
|
|
2392
|
+
type: event,
|
|
2393
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2394
|
+
repositoryId: this.repositoryId,
|
|
2395
|
+
provenance: { ...this.provenance },
|
|
2396
|
+
payload
|
|
2397
|
+
};
|
|
2398
|
+
for (const handler of handlers) {
|
|
2399
|
+
await handler(eventObj);
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
/**
|
|
2403
|
+
* Emit a pre-mutation hook event.
|
|
2404
|
+
* Returns `{ cancelled: false }` if all hooks pass,
|
|
2405
|
+
* or `{ cancelled: true, reason }` if any hook cancels.
|
|
2406
|
+
*/
|
|
2407
|
+
async emitHook(event, payload) {
|
|
2408
|
+
const hooks = this.hookHandlers.get(event);
|
|
2409
|
+
if (!hooks || hooks.size === 0) return { cancelled: false };
|
|
2410
|
+
const eventObj = {
|
|
2411
|
+
type: event,
|
|
2412
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2413
|
+
repositoryId: this.repositoryId,
|
|
2414
|
+
provenance: { ...this.provenance },
|
|
2415
|
+
payload
|
|
2416
|
+
};
|
|
2417
|
+
for (const hook of hooks) {
|
|
2418
|
+
const result = await hook(eventObj);
|
|
2419
|
+
if (result.cancel) {
|
|
2420
|
+
return { cancelled: true, reason: result.reason };
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
return { cancelled: false };
|
|
2424
|
+
}
|
|
2425
|
+
/** Remove all listeners and hooks */
|
|
2426
|
+
removeAllListeners() {
|
|
2427
|
+
this.handlers.clear();
|
|
2428
|
+
this.hookHandlers.clear();
|
|
2429
|
+
}
|
|
2430
|
+
};
|
|
2431
|
+
|
|
2432
|
+
// src/portability/RepositoryExporter.ts
|
|
2433
|
+
var LIBRARY_VERSION = "0.1.0";
|
|
2434
|
+
var RepositoryExporter = class {
|
|
2435
|
+
storage;
|
|
2436
|
+
provenance;
|
|
2437
|
+
constructor(config) {
|
|
2438
|
+
this.storage = config.storage;
|
|
2439
|
+
this.provenance = config.provenance;
|
|
2440
|
+
}
|
|
2441
|
+
/**
|
|
2442
|
+
* Stream a repository export as an async generator.
|
|
2443
|
+
* Yields: manifest → vocabulary → entity chunks → relationship chunks.
|
|
2444
|
+
* The caller can pipe this to a file, zip stream, or HTTP response.
|
|
2445
|
+
*/
|
|
2446
|
+
async *exportStream(repositoryId) {
|
|
2447
|
+
const repo = await this.storage.getRepository(repositoryId);
|
|
2448
|
+
if (!repo) {
|
|
2449
|
+
throw new RepositoryNotFoundError(repositoryId);
|
|
2450
|
+
}
|
|
2451
|
+
const vocabulary = await this.storage.getVocabulary(repositoryId);
|
|
2452
|
+
const stats = await this.storage.getRepositoryStats(repositoryId);
|
|
2453
|
+
const manifest = {
|
|
2454
|
+
formatVersion: "1.0.0",
|
|
2455
|
+
libraryVersion: LIBRARY_VERSION,
|
|
2456
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2457
|
+
exportedBy: this.provenance,
|
|
2458
|
+
repository: {
|
|
2459
|
+
repositoryId,
|
|
2460
|
+
type: repo.type,
|
|
2461
|
+
label: repo.label,
|
|
2462
|
+
description: repo.description,
|
|
2463
|
+
vocabularyVersion: vocabulary.version,
|
|
2464
|
+
governanceMode: repo.governanceConfig.mode
|
|
2465
|
+
},
|
|
2466
|
+
statistics: {
|
|
2467
|
+
entityCount: stats.entityCount,
|
|
2468
|
+
relationshipCount: stats.relationshipCount,
|
|
2469
|
+
entityTypeBreakdown: stats.entityTypeBreakdown,
|
|
2470
|
+
relationshipTypeBreakdown: stats.relationshipTypeBreakdown
|
|
2471
|
+
}
|
|
2472
|
+
};
|
|
2473
|
+
if (repo.metadata?.embeddingModelId) {
|
|
2474
|
+
manifest.embedding = {
|
|
2475
|
+
modelId: repo.metadata.embeddingModelId,
|
|
2476
|
+
dimensions: repo.metadata.embeddingDimensions ?? 0,
|
|
2477
|
+
note: "Embeddings are model-specific. Re-embed after import if using a different model."
|
|
2478
|
+
};
|
|
2479
|
+
}
|
|
2480
|
+
yield { type: "manifest", data: manifest };
|
|
2481
|
+
yield { type: "vocabulary", data: vocabulary };
|
|
2482
|
+
for await (const chunk of this.storage.exportAll(repositoryId)) {
|
|
2483
|
+
if (chunk.type === "entities") {
|
|
2484
|
+
yield {
|
|
2485
|
+
type: "entities",
|
|
2486
|
+
data: chunk.data,
|
|
2487
|
+
sequence: chunk.sequence,
|
|
2488
|
+
isLast: chunk.isLast
|
|
2489
|
+
};
|
|
2490
|
+
} else {
|
|
2491
|
+
yield {
|
|
2492
|
+
type: "relationships",
|
|
2493
|
+
data: chunk.data,
|
|
2494
|
+
sequence: chunk.sequence,
|
|
2495
|
+
isLast: chunk.isLast
|
|
2496
|
+
};
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2500
|
+
/**
|
|
2501
|
+
* Export a repository to a complete in-memory archive.
|
|
2502
|
+
* Convenience wrapper around exportStream() — use for small repositories.
|
|
2503
|
+
* For large repositories, use exportStream() directly.
|
|
2504
|
+
*/
|
|
2505
|
+
async export(repositoryId) {
|
|
2506
|
+
let manifest;
|
|
2507
|
+
let vocabulary;
|
|
2508
|
+
const entities = [];
|
|
2509
|
+
const relationships = [];
|
|
2510
|
+
for await (const item of this.exportStream(repositoryId)) {
|
|
2511
|
+
switch (item.type) {
|
|
2512
|
+
case "manifest":
|
|
2513
|
+
manifest = item.data;
|
|
2514
|
+
break;
|
|
2515
|
+
case "vocabulary":
|
|
2516
|
+
vocabulary = item.data;
|
|
2517
|
+
break;
|
|
2518
|
+
case "entities":
|
|
2519
|
+
entities.push(...item.data);
|
|
2520
|
+
break;
|
|
2521
|
+
case "relationships":
|
|
2522
|
+
relationships.push(...item.data);
|
|
2523
|
+
break;
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
return {
|
|
2527
|
+
manifest,
|
|
2528
|
+
vocabulary,
|
|
2529
|
+
entities,
|
|
2530
|
+
relationships
|
|
2531
|
+
};
|
|
2532
|
+
}
|
|
2533
|
+
};
|
|
2534
|
+
|
|
2535
|
+
// src/vocabulary/VocabularyDiff.ts
|
|
2536
|
+
function diffVocabularies(from, to) {
|
|
2537
|
+
const changes = [];
|
|
2538
|
+
diffTypes(
|
|
2539
|
+
from.entityTypes,
|
|
2540
|
+
to.entityTypes,
|
|
2541
|
+
"entity_type",
|
|
2542
|
+
diffEntityType,
|
|
2543
|
+
changes
|
|
2544
|
+
);
|
|
2545
|
+
diffTypes(
|
|
2546
|
+
from.relationshipTypes,
|
|
2547
|
+
to.relationshipTypes,
|
|
2548
|
+
"relationship_type",
|
|
2549
|
+
diffRelationshipType,
|
|
2550
|
+
changes
|
|
2551
|
+
);
|
|
2552
|
+
return {
|
|
2553
|
+
fromVersion: from.version,
|
|
2554
|
+
toVersion: to.version,
|
|
2555
|
+
changes,
|
|
2556
|
+
hasChanges: changes.length > 0
|
|
2557
|
+
};
|
|
2558
|
+
}
|
|
2559
|
+
function diffTypes(fromTypes, toTypes, category, detailDiff, changes) {
|
|
2560
|
+
const fromMap = new Map(fromTypes.map((t) => [t.type, t]));
|
|
2561
|
+
const toMap = new Map(toTypes.map((t) => [t.type, t]));
|
|
2562
|
+
for (const [typeName] of toMap) {
|
|
2563
|
+
if (!fromMap.has(typeName)) {
|
|
2564
|
+
changes.push({ changeType: "added", category, typeName });
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
for (const [typeName] of fromMap) {
|
|
2568
|
+
if (!toMap.has(typeName)) {
|
|
2569
|
+
changes.push({ changeType: "removed", category, typeName });
|
|
2570
|
+
}
|
|
2571
|
+
}
|
|
2572
|
+
for (const [typeName, fromType] of fromMap) {
|
|
2573
|
+
const toType = toMap.get(typeName);
|
|
2574
|
+
if (!toType) continue;
|
|
2575
|
+
const details = detailDiff(fromType, toType);
|
|
2576
|
+
if (details) {
|
|
2577
|
+
changes.push({ changeType: "modified", category, typeName, details });
|
|
2578
|
+
}
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
2581
|
+
function diffEntityType(from, to) {
|
|
2582
|
+
const diffs = [];
|
|
2583
|
+
if (from.description !== to.description) {
|
|
2584
|
+
diffs.push("description changed");
|
|
2585
|
+
}
|
|
2586
|
+
const propDiff = diffPropertySchemas(from.properties, to.properties);
|
|
2587
|
+
if (propDiff) {
|
|
2588
|
+
diffs.push(propDiff);
|
|
2589
|
+
}
|
|
2590
|
+
return diffs.length > 0 ? diffs.join("; ") : null;
|
|
2591
|
+
}
|
|
2592
|
+
function diffRelationshipType(from, to) {
|
|
2593
|
+
const diffs = [];
|
|
2594
|
+
if (from.description !== to.description) {
|
|
2595
|
+
diffs.push("description changed");
|
|
2596
|
+
}
|
|
2597
|
+
if (from.bidirectional !== to.bidirectional) {
|
|
2598
|
+
diffs.push(`bidirectional: ${from.bidirectional} \u2192 ${to.bidirectional}`);
|
|
2599
|
+
}
|
|
2600
|
+
const srcDiff = diffArrays(from.allowedSourceTypes, to.allowedSourceTypes);
|
|
2601
|
+
if (srcDiff) diffs.push(`allowedSourceTypes: ${srcDiff}`);
|
|
2602
|
+
const tgtDiff = diffArrays(from.allowedTargetTypes, to.allowedTargetTypes);
|
|
2603
|
+
if (tgtDiff) diffs.push(`allowedTargetTypes: ${tgtDiff}`);
|
|
2604
|
+
const propDiff = diffPropertySchemas(from.properties ?? [], to.properties ?? []);
|
|
2605
|
+
if (propDiff) diffs.push(propDiff);
|
|
2606
|
+
return diffs.length > 0 ? diffs.join("; ") : null;
|
|
2607
|
+
}
|
|
2608
|
+
function diffPropertySchemas(from, to) {
|
|
2609
|
+
const fromMap = new Map(from.map((p) => [p.name, p]));
|
|
2610
|
+
const toMap = new Map(to.map((p) => [p.name, p]));
|
|
2611
|
+
const parts = [];
|
|
2612
|
+
for (const [name] of toMap) {
|
|
2613
|
+
if (!fromMap.has(name)) parts.push(`property "${name}" added`);
|
|
2614
|
+
}
|
|
2615
|
+
for (const [name] of fromMap) {
|
|
2616
|
+
if (!toMap.has(name)) parts.push(`property "${name}" removed`);
|
|
2617
|
+
}
|
|
2618
|
+
for (const [name, fromProp] of fromMap) {
|
|
2619
|
+
const toProp = toMap.get(name);
|
|
2620
|
+
if (!toProp) continue;
|
|
2621
|
+
if (fromProp.type !== toProp.type) {
|
|
2622
|
+
parts.push(`property "${name}" type: ${fromProp.type} \u2192 ${toProp.type}`);
|
|
2623
|
+
}
|
|
2624
|
+
if (fromProp.required !== toProp.required) {
|
|
2625
|
+
parts.push(`property "${name}" required: ${fromProp.required} \u2192 ${toProp.required}`);
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
return parts.length > 0 ? parts.join(", ") : null;
|
|
2629
|
+
}
|
|
2630
|
+
function diffArrays(from, to) {
|
|
2631
|
+
const fromSet = new Set(from);
|
|
2632
|
+
const toSet = new Set(to);
|
|
2633
|
+
const added = to.filter((t) => !fromSet.has(t));
|
|
2634
|
+
const removed = from.filter((f) => !toSet.has(f));
|
|
2635
|
+
const parts = [];
|
|
2636
|
+
if (added.length > 0) parts.push(`+[${added.join(", ")}]`);
|
|
2637
|
+
if (removed.length > 0) parts.push(`-[${removed.join(", ")}]`);
|
|
2638
|
+
return parts.length > 0 ? parts.join(" ") : null;
|
|
2639
|
+
}
|
|
2640
|
+
|
|
2641
|
+
// src/portability/MigrationEngine.ts
|
|
2642
|
+
var MigrationEngine = class {
|
|
2643
|
+
/**
|
|
2644
|
+
* Analyse and migrate vocabulary differences between source and target.
|
|
2645
|
+
*
|
|
2646
|
+
* @param sourceVocabulary - vocabulary from the export archive
|
|
2647
|
+
* @param targetVocabulary - vocabulary in the target repository
|
|
2648
|
+
* @param conflictMode - how to handle differences
|
|
2649
|
+
* @param migratedBy - actor performing the migration
|
|
2650
|
+
*/
|
|
2651
|
+
migrate(sourceVocabulary, targetVocabulary, conflictMode, migratedBy) {
|
|
2652
|
+
const diff = diffVocabularies(targetVocabulary, sourceVocabulary);
|
|
2653
|
+
if (!diff.hasChanges) {
|
|
2654
|
+
return { success: true, mergedVocabulary: targetVocabulary, warnings: [] };
|
|
2655
|
+
}
|
|
2656
|
+
const warnings = [];
|
|
2657
|
+
switch (conflictMode) {
|
|
2658
|
+
case "reject":
|
|
2659
|
+
return {
|
|
2660
|
+
success: false,
|
|
2661
|
+
warnings: [],
|
|
2662
|
+
reason: `Vocabulary mismatch: ${diff.changes.length} difference(s) found. Changes: ${diff.changes.map(describeChange).join("; ")}`
|
|
2663
|
+
};
|
|
2664
|
+
case "prompt":
|
|
2665
|
+
return {
|
|
2666
|
+
success: false,
|
|
2667
|
+
warnings: diff.changes.map((c) => ({
|
|
2668
|
+
code: "vocabulary_mismatch",
|
|
2669
|
+
message: describeChange(c)
|
|
2670
|
+
})),
|
|
2671
|
+
reason: "Vocabulary differences require manual resolution"
|
|
2672
|
+
};
|
|
2673
|
+
case "extend": {
|
|
2674
|
+
const merged = this.extendVocabulary(
|
|
2675
|
+
sourceVocabulary,
|
|
2676
|
+
targetVocabulary,
|
|
2677
|
+
diff.changes,
|
|
2678
|
+
migratedBy,
|
|
2679
|
+
warnings
|
|
2680
|
+
);
|
|
2681
|
+
return { success: true, mergedVocabulary: merged, warnings };
|
|
2682
|
+
}
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2685
|
+
/** Extend the target vocabulary with types from the source */
|
|
2686
|
+
extendVocabulary(source, target, changes, migratedBy, warnings) {
|
|
2687
|
+
const merged = {
|
|
2688
|
+
...target,
|
|
2689
|
+
entityTypes: [...target.entityTypes],
|
|
2690
|
+
relationshipTypes: [...target.relationshipTypes]
|
|
2691
|
+
};
|
|
2692
|
+
for (const change of changes) {
|
|
2693
|
+
if (change.changeType === "added") {
|
|
2694
|
+
if (change.category === "entity_type") {
|
|
2695
|
+
const sourceType = source.entityTypes.find((t) => t.type === change.typeName);
|
|
2696
|
+
if (sourceType) {
|
|
2697
|
+
merged.entityTypes.push(
|
|
2698
|
+
createEntityTypeDefinition(
|
|
2699
|
+
{ type: sourceType.type, description: sourceType.description, properties: sourceType.properties },
|
|
2700
|
+
migratedBy
|
|
2701
|
+
)
|
|
2702
|
+
);
|
|
2703
|
+
warnings.push({
|
|
2704
|
+
code: "entity_type_added",
|
|
2705
|
+
message: `Entity type "${change.typeName}" added from source vocabulary`
|
|
2706
|
+
});
|
|
2707
|
+
}
|
|
2708
|
+
} else {
|
|
2709
|
+
const sourceType = source.relationshipTypes.find((t) => t.type === change.typeName);
|
|
2710
|
+
if (sourceType) {
|
|
2711
|
+
merged.relationshipTypes.push(
|
|
2712
|
+
createRelationshipTypeDefinition(
|
|
2713
|
+
{
|
|
2714
|
+
type: sourceType.type,
|
|
2715
|
+
description: sourceType.description,
|
|
2716
|
+
allowedSourceTypes: sourceType.allowedSourceTypes,
|
|
2717
|
+
allowedTargetTypes: sourceType.allowedTargetTypes,
|
|
2718
|
+
bidirectional: sourceType.bidirectional,
|
|
2719
|
+
properties: sourceType.properties
|
|
2720
|
+
},
|
|
2721
|
+
migratedBy
|
|
2722
|
+
)
|
|
2723
|
+
);
|
|
2724
|
+
warnings.push({
|
|
2725
|
+
code: "relationship_type_added",
|
|
2726
|
+
message: `Relationship type "${change.typeName}" added from source vocabulary`
|
|
2727
|
+
});
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
} else if (change.changeType === "modified") {
|
|
2731
|
+
warnings.push({
|
|
2732
|
+
code: "vocabulary_conflict",
|
|
2733
|
+
message: `Type "${change.typeName}" differs between source and target: ${change.details ?? "unknown"}. Target version kept.`
|
|
2734
|
+
});
|
|
2735
|
+
} else if (change.changeType === "removed") {
|
|
2736
|
+
warnings.push({
|
|
2737
|
+
code: "vocabulary_extra",
|
|
2738
|
+
message: `Type "${change.typeName}" exists in target but not in source \u2014 kept as-is`
|
|
2739
|
+
});
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2742
|
+
const bumped = incrementVersion(merged.version, "minor");
|
|
2743
|
+
merged.version = compareVersions(source.version, bumped) > 0 ? source.version : bumped;
|
|
2744
|
+
merged.lastModified = (/* @__PURE__ */ new Date()).toISOString();
|
|
2745
|
+
merged.modifiedBy = migratedBy;
|
|
2746
|
+
return merged;
|
|
2747
|
+
}
|
|
2748
|
+
};
|
|
2749
|
+
function compareVersions(a, b) {
|
|
2750
|
+
const pa = a.split(".").map(Number);
|
|
2751
|
+
const pb = b.split(".").map(Number);
|
|
2752
|
+
for (let i = 0; i < 3; i++) {
|
|
2753
|
+
const diff = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
2754
|
+
if (diff !== 0) return diff;
|
|
2755
|
+
}
|
|
2756
|
+
return 0;
|
|
2757
|
+
}
|
|
2758
|
+
function describeChange(change) {
|
|
2759
|
+
const label = change.category === "entity_type" ? "entity type" : "relationship type";
|
|
2760
|
+
switch (change.changeType) {
|
|
2761
|
+
case "added":
|
|
2762
|
+
return `${label} "${change.typeName}" exists in source but not target`;
|
|
2763
|
+
case "removed":
|
|
2764
|
+
return `${label} "${change.typeName}" exists in target but not source`;
|
|
2765
|
+
case "modified":
|
|
2766
|
+
return `${label} "${change.typeName}" differs: ${change.details ?? "unknown"}`;
|
|
2767
|
+
}
|
|
2768
|
+
}
|
|
2769
|
+
|
|
2770
|
+
// src/portability/RepositoryImporter.ts
|
|
2771
|
+
var RepositoryImporter = class {
|
|
2772
|
+
storage;
|
|
2773
|
+
actorId;
|
|
2774
|
+
migrationEngine = new MigrationEngine();
|
|
2775
|
+
constructor(config) {
|
|
2776
|
+
this.storage = config.storage;
|
|
2777
|
+
this.actorId = config.actorId;
|
|
2778
|
+
}
|
|
2779
|
+
/**
|
|
2780
|
+
* Import an export archive according to the provided options.
|
|
2781
|
+
* Convenience wrapper around importStream() — use for small repositories.
|
|
2782
|
+
* For large repositories, use importStream() directly.
|
|
2783
|
+
*/
|
|
2784
|
+
async import(archive, options) {
|
|
2785
|
+
const validationWarnings = this.validateArchive(archive);
|
|
2786
|
+
if (validationWarnings.length > 0) {
|
|
2787
|
+
const hasCritical = validationWarnings.some((w) => w.code === "invalid_archive");
|
|
2788
|
+
if (hasCritical) {
|
|
2789
|
+
return {
|
|
2790
|
+
success: false,
|
|
2791
|
+
repositoryId: options.target.repositoryId,
|
|
2792
|
+
statistics: { entitiesImported: 0, entitiesSkipped: 0, relationshipsImported: 0, relationshipsSkipped: 0, vocabularyExtensions: 0 },
|
|
2793
|
+
warnings: validationWarnings
|
|
2794
|
+
};
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2797
|
+
const header = {
|
|
2798
|
+
manifest: archive.manifest,
|
|
2799
|
+
vocabulary: archive.vocabulary
|
|
2800
|
+
};
|
|
2801
|
+
async function* toChunks() {
|
|
2802
|
+
yield { entities: archive.entities };
|
|
2803
|
+
yield { relationships: archive.relationships };
|
|
2804
|
+
}
|
|
2805
|
+
return this.importStream(header, toChunks(), options);
|
|
2806
|
+
}
|
|
2807
|
+
/**
|
|
2808
|
+
* Streaming import — processes data from an async iterable of chunks.
|
|
2809
|
+
* For large repositories, this avoids loading everything into memory at once.
|
|
2810
|
+
*
|
|
2811
|
+
* The caller provides the header (manifest + vocabulary) up front,
|
|
2812
|
+
* then streams ImportChunks containing batches of entities and/or relationships.
|
|
2813
|
+
*/
|
|
2814
|
+
async importStream(header, chunks, options) {
|
|
2815
|
+
const warnings = [];
|
|
2816
|
+
if (!header.manifest) {
|
|
2817
|
+
warnings.push({ code: "invalid_archive", message: "Header is missing manifest" });
|
|
2818
|
+
}
|
|
2819
|
+
if (!header.vocabulary) {
|
|
2820
|
+
warnings.push({ code: "invalid_archive", message: "Header is missing vocabulary" });
|
|
2821
|
+
}
|
|
2822
|
+
if (options.target.mode === "create") {
|
|
2823
|
+
return this.importStreamCreate(header, chunks, options, warnings);
|
|
2824
|
+
} else {
|
|
2825
|
+
return this.importStreamMerge(header, chunks, options, warnings);
|
|
2826
|
+
}
|
|
2827
|
+
}
|
|
2828
|
+
/** Streaming create mode — set up repo then pipe chunks through importBulk */
|
|
2829
|
+
async importStreamCreate(header, chunks, options, warnings) {
|
|
2830
|
+
const target = options.target;
|
|
2831
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2832
|
+
await this.storage.createRepository({
|
|
2833
|
+
repositoryId: target.repositoryId,
|
|
2834
|
+
type: target.config.type,
|
|
2835
|
+
label: target.config.label,
|
|
2836
|
+
description: target.config.description,
|
|
2837
|
+
governanceConfig: target.config.governance ?? { mode: "open" },
|
|
2838
|
+
createdAt: now,
|
|
2839
|
+
createdBy: this.actorId
|
|
2840
|
+
});
|
|
2841
|
+
await this.storage.saveVocabulary(target.repositoryId, header.vocabulary);
|
|
2842
|
+
let entitiesImported = 0;
|
|
2843
|
+
let relationshipsImported = 0;
|
|
2844
|
+
for await (const chunk of chunks) {
|
|
2845
|
+
const bulkResult = await this.storage.importBulk(target.repositoryId, [chunk]);
|
|
2846
|
+
entitiesImported += bulkResult.entitiesImported;
|
|
2847
|
+
relationshipsImported += bulkResult.relationshipsImported;
|
|
2848
|
+
for (const e of bulkResult.errors) {
|
|
2849
|
+
warnings.push({
|
|
2850
|
+
code: "import_error",
|
|
2851
|
+
message: `Failed to import ${e.item}: ${e.error}`,
|
|
2852
|
+
id: e.item
|
|
2853
|
+
});
|
|
2854
|
+
}
|
|
2855
|
+
}
|
|
2856
|
+
return {
|
|
2857
|
+
success: true,
|
|
2858
|
+
repositoryId: target.repositoryId,
|
|
2859
|
+
statistics: {
|
|
2860
|
+
entitiesImported,
|
|
2861
|
+
entitiesSkipped: 0,
|
|
2862
|
+
relationshipsImported,
|
|
2863
|
+
relationshipsSkipped: 0,
|
|
2864
|
+
vocabularyExtensions: 0
|
|
2865
|
+
},
|
|
2866
|
+
warnings
|
|
2867
|
+
};
|
|
2868
|
+
}
|
|
2869
|
+
/** Streaming merge mode — process chunks with per-entity conflict resolution */
|
|
2870
|
+
async importStreamMerge(header, chunks, options, warnings) {
|
|
2871
|
+
const target = options.target;
|
|
2872
|
+
const repositoryId = target.repositoryId;
|
|
2873
|
+
const existingRepo = await this.storage.getRepository(repositoryId);
|
|
2874
|
+
if (!existingRepo) {
|
|
2875
|
+
return {
|
|
2876
|
+
success: false,
|
|
2877
|
+
repositoryId,
|
|
2878
|
+
statistics: { entitiesImported: 0, entitiesSkipped: 0, relationshipsImported: 0, relationshipsSkipped: 0, vocabularyExtensions: 0 },
|
|
2879
|
+
warnings: [{ code: "repository_not_found", message: `Repository "${repositoryId}" not found` }]
|
|
2880
|
+
};
|
|
2881
|
+
}
|
|
2882
|
+
const targetVocabulary = await this.storage.getVocabulary(repositoryId);
|
|
2883
|
+
const conflictMode = options.vocabularyConflict ?? "reject";
|
|
2884
|
+
const migrationResult = this.migrationEngine.migrate(
|
|
2885
|
+
header.vocabulary,
|
|
2886
|
+
targetVocabulary,
|
|
2887
|
+
conflictMode,
|
|
2888
|
+
this.actorId
|
|
2889
|
+
);
|
|
2890
|
+
warnings.push(...migrationResult.warnings);
|
|
2891
|
+
if (!migrationResult.success) {
|
|
2892
|
+
return {
|
|
2893
|
+
success: false,
|
|
2894
|
+
repositoryId,
|
|
2895
|
+
statistics: { entitiesImported: 0, entitiesSkipped: 0, relationshipsImported: 0, relationshipsSkipped: 0, vocabularyExtensions: 0 },
|
|
2896
|
+
warnings: [...warnings, { code: "vocabulary_migration_failed", message: migrationResult.reason ?? "Vocabulary migration failed" }]
|
|
2897
|
+
};
|
|
2898
|
+
}
|
|
2899
|
+
if (migrationResult.mergedVocabulary && migrationResult.mergedVocabulary !== targetVocabulary) {
|
|
2900
|
+
await this.storage.saveVocabulary(repositoryId, migrationResult.mergedVocabulary);
|
|
2901
|
+
}
|
|
2902
|
+
const entityConflict = options.entityConflict ?? "skip";
|
|
2903
|
+
let entitiesImported = 0;
|
|
2904
|
+
let entitiesSkipped = 0;
|
|
2905
|
+
let relationshipsImported = 0;
|
|
2906
|
+
let relationshipsSkipped = 0;
|
|
2907
|
+
for await (const chunk of chunks) {
|
|
2908
|
+
if (chunk.entities) {
|
|
2909
|
+
for (const entity of chunk.entities) {
|
|
2910
|
+
let existing = await this.storage.getEntity(repositoryId, entity.id);
|
|
2911
|
+
if (!existing && entity.slug) {
|
|
2912
|
+
existing = await this.storage.getEntityBySlug(repositoryId, entity.slug);
|
|
2913
|
+
}
|
|
2914
|
+
if (existing) {
|
|
2915
|
+
const existingId = existing.id;
|
|
2916
|
+
switch (entityConflict) {
|
|
2917
|
+
case "skip":
|
|
2918
|
+
entitiesSkipped++;
|
|
2919
|
+
warnings.push({
|
|
2920
|
+
code: "entity_skipped",
|
|
2921
|
+
message: `Entity "${entity.slug}" already exists \u2014 skipped`,
|
|
2922
|
+
id: existingId
|
|
2923
|
+
});
|
|
2924
|
+
continue;
|
|
2925
|
+
case "overwrite":
|
|
2926
|
+
await this.storage.updateEntity(repositoryId, existingId, {
|
|
2927
|
+
label: entity.label,
|
|
2928
|
+
slug: entity.slug,
|
|
2929
|
+
summary: entity.summary,
|
|
2930
|
+
properties: entity.properties,
|
|
2931
|
+
data: entity.data,
|
|
2932
|
+
dataFormat: entity.dataFormat,
|
|
2933
|
+
provenance: entity.provenance,
|
|
2934
|
+
embedding: entity.embedding
|
|
2935
|
+
});
|
|
2936
|
+
entitiesImported++;
|
|
2937
|
+
warnings.push({
|
|
2938
|
+
code: "entity_overwritten",
|
|
2939
|
+
message: `Entity "${entity.slug}" overwritten`,
|
|
2940
|
+
id: existingId
|
|
2941
|
+
});
|
|
2942
|
+
continue;
|
|
2943
|
+
case "rename": {
|
|
2944
|
+
const renamedEntity = { ...entity, id: generateId(), slug: `${entity.slug}-imported` };
|
|
2945
|
+
await this.storage.createEntity(repositoryId, renamedEntity);
|
|
2946
|
+
entitiesImported++;
|
|
2947
|
+
warnings.push({
|
|
2948
|
+
code: "entity_renamed",
|
|
2949
|
+
message: `Entity "${entity.slug}" slug renamed to "${renamedEntity.slug}"`,
|
|
2950
|
+
id: entity.id
|
|
2951
|
+
});
|
|
2952
|
+
continue;
|
|
2953
|
+
}
|
|
2954
|
+
}
|
|
2955
|
+
} else {
|
|
2956
|
+
await this.storage.createEntity(repositoryId, entity);
|
|
2957
|
+
entitiesImported++;
|
|
2958
|
+
}
|
|
2959
|
+
}
|
|
2960
|
+
}
|
|
2961
|
+
if (chunk.relationships) {
|
|
2962
|
+
for (const rel of chunk.relationships) {
|
|
2963
|
+
const existing = await this.storage.getRelationship(repositoryId, rel.id);
|
|
2964
|
+
if (existing) {
|
|
2965
|
+
relationshipsSkipped++;
|
|
2966
|
+
warnings.push({
|
|
2967
|
+
code: "relationship_skipped",
|
|
2968
|
+
message: `Relationship "${rel.id}" already exists \u2014 skipped`,
|
|
2969
|
+
relationshipId: rel.id
|
|
2970
|
+
});
|
|
2971
|
+
} else {
|
|
2972
|
+
const sourceExists = await this.storage.getEntity(repositoryId, rel.sourceEntityId);
|
|
2973
|
+
const targetExists = await this.storage.getEntity(repositoryId, rel.targetEntityId);
|
|
2974
|
+
if (!sourceExists || !targetExists) {
|
|
2975
|
+
relationshipsSkipped++;
|
|
2976
|
+
warnings.push({
|
|
2977
|
+
code: "relationship_orphaned",
|
|
2978
|
+
message: `Relationship "${rel.id}" skipped \u2014 source or target entity missing`,
|
|
2979
|
+
relationshipId: rel.id
|
|
2980
|
+
});
|
|
2981
|
+
} else {
|
|
2982
|
+
await this.storage.createRelationship(repositoryId, rel);
|
|
2983
|
+
relationshipsImported++;
|
|
2984
|
+
}
|
|
2985
|
+
}
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2988
|
+
}
|
|
2989
|
+
const vocabExtensions = migrationResult.warnings.filter(
|
|
2990
|
+
(w) => w.code === "entity_type_added" || w.code === "relationship_type_added"
|
|
2991
|
+
).length;
|
|
2992
|
+
return {
|
|
2993
|
+
success: true,
|
|
2994
|
+
repositoryId,
|
|
2995
|
+
statistics: {
|
|
2996
|
+
entitiesImported,
|
|
2997
|
+
entitiesSkipped,
|
|
2998
|
+
relationshipsImported,
|
|
2999
|
+
relationshipsSkipped,
|
|
3000
|
+
vocabularyExtensions: vocabExtensions
|
|
3001
|
+
},
|
|
3002
|
+
warnings
|
|
3003
|
+
};
|
|
3004
|
+
}
|
|
3005
|
+
/** Basic archive validation */
|
|
3006
|
+
validateArchive(archive) {
|
|
3007
|
+
const warnings = [];
|
|
3008
|
+
if (!archive.manifest) {
|
|
3009
|
+
warnings.push({ code: "invalid_archive", message: "Archive is missing manifest" });
|
|
3010
|
+
}
|
|
3011
|
+
if (!archive.vocabulary) {
|
|
3012
|
+
warnings.push({ code: "invalid_archive", message: "Archive is missing vocabulary" });
|
|
3013
|
+
}
|
|
3014
|
+
if (!archive.entities) {
|
|
3015
|
+
warnings.push({ code: "invalid_archive", message: "Archive is missing entities array" });
|
|
3016
|
+
}
|
|
3017
|
+
if (!archive.relationships) {
|
|
3018
|
+
warnings.push({ code: "invalid_archive", message: "Archive is missing relationships array" });
|
|
3019
|
+
}
|
|
3020
|
+
return warnings;
|
|
3021
|
+
}
|
|
3022
|
+
};
|
|
3023
|
+
|
|
3024
|
+
// src/core/DeepMemory.ts
|
|
3025
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
3026
|
+
function generateId() {
|
|
3027
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
3028
|
+
const r = Math.random() * 16 | 0;
|
|
3029
|
+
return (c === "x" ? r : r & 3 | 8).toString(16);
|
|
3030
|
+
});
|
|
3031
|
+
}
|
|
3032
|
+
function isValidUuid(value) {
|
|
3033
|
+
return UUID_RE.test(value);
|
|
3034
|
+
}
|
|
3035
|
+
var DeepMemory = class {
|
|
3036
|
+
storage;
|
|
3037
|
+
search;
|
|
3038
|
+
embedding;
|
|
3039
|
+
/** Reserved for future distributed locking support */
|
|
3040
|
+
lock;
|
|
3041
|
+
provenance;
|
|
3042
|
+
globalEventBus;
|
|
3043
|
+
initialised = false;
|
|
3044
|
+
constructor(config) {
|
|
3045
|
+
this.storage = config.storage;
|
|
3046
|
+
this.search = config.search;
|
|
3047
|
+
this.embedding = config.embedding;
|
|
3048
|
+
this.lock = config.lock;
|
|
3049
|
+
this.provenance = new ProvenanceTracker(config.provenance);
|
|
3050
|
+
this.globalEventBus = new EventBus(config.provenance);
|
|
3051
|
+
}
|
|
3052
|
+
/** Initialise the storage provider (call once before use) */
|
|
3053
|
+
async ensureInitialised() {
|
|
3054
|
+
if (!this.initialised) {
|
|
3055
|
+
if (this.storage.initialise) {
|
|
3056
|
+
await this.storage.initialise();
|
|
3057
|
+
}
|
|
3058
|
+
this.initialised = true;
|
|
3059
|
+
}
|
|
3060
|
+
}
|
|
3061
|
+
/** Create or migrate the storage schema. Call once at deployment / startup, not per-request. */
|
|
3062
|
+
async ensureSchema() {
|
|
3063
|
+
await this.ensureInitialised();
|
|
3064
|
+
if (this.storage.ensureSchema) {
|
|
3065
|
+
await this.storage.ensureSchema();
|
|
3066
|
+
}
|
|
3067
|
+
}
|
|
3068
|
+
validateRepositoryId(repositoryId) {
|
|
3069
|
+
if (!isValidUuid(repositoryId)) {
|
|
3070
|
+
throw new InvalidInputError(
|
|
3071
|
+
"repositoryId",
|
|
3072
|
+
`Repository ID "${repositoryId}" is not a valid UUID`,
|
|
3073
|
+
`Provide a valid UUID (e.g. "550e8400-e29b-41d4-a716-446655440000") or omit repositoryId to auto-generate one.`
|
|
3074
|
+
);
|
|
3075
|
+
}
|
|
3076
|
+
}
|
|
3077
|
+
/** Create a new memory repository */
|
|
3078
|
+
async createRepository(config) {
|
|
3079
|
+
await this.ensureInitialised();
|
|
3080
|
+
const repositoryId = config.repositoryId ?? generateId();
|
|
3081
|
+
this.validateRepositoryId(repositoryId);
|
|
3082
|
+
const context = this.provenance.getContext();
|
|
3083
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3084
|
+
const governanceConfig = config.governance ?? { mode: "open" };
|
|
3085
|
+
const metadata = { ...config.metadata };
|
|
3086
|
+
if (this.embedding) {
|
|
3087
|
+
if (!metadata.embeddingModelId) {
|
|
3088
|
+
metadata.embeddingModelId = this.embedding.modelId();
|
|
3089
|
+
}
|
|
3090
|
+
if (metadata.embeddingDimensions === void 0) {
|
|
3091
|
+
try {
|
|
3092
|
+
metadata.embeddingDimensions = this.embedding.dimensions();
|
|
3093
|
+
} catch {
|
|
3094
|
+
}
|
|
3095
|
+
}
|
|
3096
|
+
}
|
|
3097
|
+
await this.storage.createRepository({
|
|
3098
|
+
repositoryId,
|
|
3099
|
+
type: config.type,
|
|
3100
|
+
label: config.label,
|
|
3101
|
+
description: config.description,
|
|
3102
|
+
legal: config.legal,
|
|
3103
|
+
owner: config.owner,
|
|
3104
|
+
governanceConfig,
|
|
3105
|
+
metadata: Object.keys(metadata).length > 0 ? metadata : void 0,
|
|
3106
|
+
createdAt: now,
|
|
3107
|
+
createdBy: context.actorId
|
|
3108
|
+
});
|
|
3109
|
+
const vocabulary = config.vocabulary ? buildVocabulary(config.vocabulary, context.actorId) : createEmptyVocabulary(context.actorId);
|
|
3110
|
+
await this.storage.saveVocabulary(repositoryId, vocabulary);
|
|
3111
|
+
const eventBus = new EventBus(context, repositoryId);
|
|
3112
|
+
const vocabularyEngine = new VocabularyEngine({
|
|
3113
|
+
repositoryId,
|
|
3114
|
+
storageProvider: this.storage,
|
|
3115
|
+
governanceConfig,
|
|
3116
|
+
embeddingProvider: this.embedding
|
|
3117
|
+
});
|
|
3118
|
+
const repo = new MemoryRepository({
|
|
3119
|
+
repositoryId,
|
|
3120
|
+
storage: this.storage,
|
|
3121
|
+
search: this.search,
|
|
3122
|
+
embedding: this.embedding,
|
|
3123
|
+
vocabularyEngine,
|
|
3124
|
+
provenanceTracker: this.provenance,
|
|
3125
|
+
eventBus
|
|
3126
|
+
});
|
|
3127
|
+
await this.globalEventBus.emit("repository:created", {
|
|
3128
|
+
repositoryId,
|
|
3129
|
+
label: config.label
|
|
3130
|
+
});
|
|
3131
|
+
return repo;
|
|
3132
|
+
}
|
|
3133
|
+
/** Open an existing repository */
|
|
3134
|
+
async openRepository(repositoryId) {
|
|
3135
|
+
await this.ensureInitialised();
|
|
3136
|
+
this.validateRepositoryId(repositoryId);
|
|
3137
|
+
const storedRepo = await this.storage.getRepository(repositoryId);
|
|
3138
|
+
if (!storedRepo) {
|
|
3139
|
+
throw new RepositoryNotFoundError(repositoryId);
|
|
3140
|
+
}
|
|
3141
|
+
const context = this.provenance.getContext();
|
|
3142
|
+
const eventBus = new EventBus(context, repositoryId);
|
|
3143
|
+
const vocabularyEngine = new VocabularyEngine({
|
|
3144
|
+
repositoryId,
|
|
3145
|
+
storageProvider: this.storage,
|
|
3146
|
+
governanceConfig: storedRepo.governanceConfig,
|
|
3147
|
+
embeddingProvider: this.embedding
|
|
3148
|
+
});
|
|
3149
|
+
const repo = new MemoryRepository({
|
|
3150
|
+
repositoryId,
|
|
3151
|
+
storage: this.storage,
|
|
3152
|
+
search: this.search,
|
|
3153
|
+
embedding: this.embedding,
|
|
3154
|
+
vocabularyEngine,
|
|
3155
|
+
provenanceTracker: this.provenance,
|
|
3156
|
+
eventBus
|
|
3157
|
+
});
|
|
3158
|
+
await this.globalEventBus.emit("repository:opened", { repositoryId });
|
|
3159
|
+
return repo;
|
|
3160
|
+
}
|
|
3161
|
+
/** List all repositories */
|
|
3162
|
+
async listRepositories(filter) {
|
|
3163
|
+
await this.ensureInitialised();
|
|
3164
|
+
const result = await this.storage.listRepositories(filter);
|
|
3165
|
+
return {
|
|
3166
|
+
items: result.items.map((stored) => ({
|
|
3167
|
+
repositoryId: stored.repositoryId,
|
|
3168
|
+
type: stored.type,
|
|
3169
|
+
label: stored.label,
|
|
3170
|
+
description: stored.description,
|
|
3171
|
+
governanceMode: stored.governanceConfig.mode
|
|
3172
|
+
})),
|
|
3173
|
+
total: result.total,
|
|
3174
|
+
hasMore: result.hasMore,
|
|
3175
|
+
limit: result.limit,
|
|
3176
|
+
offset: result.offset
|
|
3177
|
+
};
|
|
3178
|
+
}
|
|
3179
|
+
/** Update repository metadata and settings */
|
|
3180
|
+
async updateRepository(repositoryId, updates) {
|
|
3181
|
+
await this.ensureInitialised();
|
|
3182
|
+
this.validateRepositoryId(repositoryId);
|
|
3183
|
+
const updated = await this.storage.updateRepository(repositoryId, updates);
|
|
3184
|
+
await this.globalEventBus.emit("repository:updated", { repositoryId });
|
|
3185
|
+
return updated;
|
|
3186
|
+
}
|
|
3187
|
+
/** Delete a repository */
|
|
3188
|
+
async deleteRepository(repositoryId) {
|
|
3189
|
+
await this.ensureInitialised();
|
|
3190
|
+
this.validateRepositoryId(repositoryId);
|
|
3191
|
+
await this.storage.deleteRepository(repositoryId);
|
|
3192
|
+
await this.globalEventBus.emit("repository:deleted", { repositoryId });
|
|
3193
|
+
}
|
|
3194
|
+
/** Export a repository to a portable archive */
|
|
3195
|
+
async exportRepository(repositoryId) {
|
|
3196
|
+
await this.ensureInitialised();
|
|
3197
|
+
const exporter = new RepositoryExporter({
|
|
3198
|
+
storage: this.storage,
|
|
3199
|
+
provenance: this.provenance.getContext()
|
|
3200
|
+
});
|
|
3201
|
+
const archive = await exporter.export(repositoryId);
|
|
3202
|
+
await this.globalEventBus.emit("export:completed", {
|
|
3203
|
+
repositoryId,
|
|
3204
|
+
entityCount: archive.entities.length,
|
|
3205
|
+
relationshipCount: archive.relationships.length
|
|
3206
|
+
});
|
|
3207
|
+
return archive;
|
|
3208
|
+
}
|
|
3209
|
+
/** Import a repository from a portable archive */
|
|
3210
|
+
async importRepository(archive, options) {
|
|
3211
|
+
await this.ensureInitialised();
|
|
3212
|
+
const importer = new RepositoryImporter({
|
|
3213
|
+
storage: this.storage,
|
|
3214
|
+
actorId: this.provenance.getContext().actorId
|
|
3215
|
+
});
|
|
3216
|
+
const result = await importer.import(archive, options);
|
|
3217
|
+
if (result.success) {
|
|
3218
|
+
await this.globalEventBus.emit("import:completed", {
|
|
3219
|
+
repositoryId: result.repositoryId,
|
|
3220
|
+
entitiesImported: result.statistics.entitiesImported,
|
|
3221
|
+
relationshipsImported: result.statistics.relationshipsImported
|
|
3222
|
+
});
|
|
3223
|
+
} else {
|
|
3224
|
+
await this.globalEventBus.emit("import:failed", {
|
|
3225
|
+
repositoryId: result.repositoryId,
|
|
3226
|
+
error: result.warnings.map((w) => w.message).join("; ")
|
|
3227
|
+
});
|
|
3228
|
+
}
|
|
3229
|
+
return result;
|
|
3230
|
+
}
|
|
3231
|
+
/**
|
|
3232
|
+
* Stream a repository export as an async generator.
|
|
3233
|
+
* Yields: manifest → vocabulary → entity chunks → relationship chunks.
|
|
3234
|
+
* Use for large repositories to avoid loading everything into memory.
|
|
3235
|
+
*/
|
|
3236
|
+
async *exportRepositoryStream(repositoryId) {
|
|
3237
|
+
await this.ensureInitialised();
|
|
3238
|
+
const exporter = new RepositoryExporter({
|
|
3239
|
+
storage: this.storage,
|
|
3240
|
+
provenance: this.provenance.getContext()
|
|
3241
|
+
});
|
|
3242
|
+
await this.globalEventBus.emit("export:started", { repositoryId });
|
|
3243
|
+
let entityCount = 0;
|
|
3244
|
+
let relationshipCount = 0;
|
|
3245
|
+
for await (const item of exporter.exportStream(repositoryId)) {
|
|
3246
|
+
yield item;
|
|
3247
|
+
if (item.type === "entities") {
|
|
3248
|
+
entityCount += item.data.length;
|
|
3249
|
+
} else if (item.type === "relationships") {
|
|
3250
|
+
relationshipCount += item.data.length;
|
|
3251
|
+
}
|
|
3252
|
+
}
|
|
3253
|
+
await this.globalEventBus.emit("export:completed", {
|
|
3254
|
+
repositoryId,
|
|
3255
|
+
entityCount,
|
|
3256
|
+
relationshipCount
|
|
3257
|
+
});
|
|
3258
|
+
}
|
|
3259
|
+
/**
|
|
3260
|
+
* Import a repository from a stream of chunks.
|
|
3261
|
+
* Use for large repositories to avoid loading everything into memory.
|
|
3262
|
+
*/
|
|
3263
|
+
async importRepositoryStream(header, chunks, options) {
|
|
3264
|
+
await this.ensureInitialised();
|
|
3265
|
+
const importer = new RepositoryImporter({
|
|
3266
|
+
storage: this.storage,
|
|
3267
|
+
actorId: this.provenance.getContext().actorId
|
|
3268
|
+
});
|
|
3269
|
+
await this.globalEventBus.emit("import:started", { repositoryId: options.target.repositoryId });
|
|
3270
|
+
const result = await importer.importStream(header, chunks, options);
|
|
3271
|
+
if (result.success) {
|
|
3272
|
+
await this.globalEventBus.emit("import:completed", {
|
|
3273
|
+
repositoryId: result.repositoryId,
|
|
3274
|
+
entitiesImported: result.statistics.entitiesImported,
|
|
3275
|
+
relationshipsImported: result.statistics.relationshipsImported
|
|
3276
|
+
});
|
|
3277
|
+
} else {
|
|
3278
|
+
await this.globalEventBus.emit("import:failed", {
|
|
3279
|
+
repositoryId: result.repositoryId,
|
|
3280
|
+
error: result.warnings.map((w) => w.message).join("; ")
|
|
3281
|
+
});
|
|
3282
|
+
}
|
|
3283
|
+
return result;
|
|
3284
|
+
}
|
|
3285
|
+
/** Subscribe to global events */
|
|
3286
|
+
on(event, handler) {
|
|
3287
|
+
return this.globalEventBus.on(event, handler);
|
|
3288
|
+
}
|
|
3289
|
+
/** Update the provenance context (e.g., new conversation) */
|
|
3290
|
+
updateProvenance(context) {
|
|
3291
|
+
this.provenance.updateContext(context);
|
|
3292
|
+
this.globalEventBus.updateProvenance(this.provenance.getContext());
|
|
3293
|
+
}
|
|
3294
|
+
/** Dispose of all resources */
|
|
3295
|
+
async dispose() {
|
|
3296
|
+
this.globalEventBus.removeAllListeners();
|
|
3297
|
+
if (this.storage.dispose) {
|
|
3298
|
+
await this.storage.dispose();
|
|
3299
|
+
}
|
|
3300
|
+
this.initialised = false;
|
|
3301
|
+
}
|
|
3302
|
+
};
|
|
3303
|
+
|
|
3304
|
+
// src/relationships/PropertyFilterMatcher.ts
|
|
3305
|
+
function matchesPropertyFilters(properties, filters) {
|
|
3306
|
+
return filters.every((filter) => matchesSingleFilter(properties, filter));
|
|
3307
|
+
}
|
|
3308
|
+
function matchesSingleFilter(properties, filter) {
|
|
3309
|
+
const value = properties[filter.key];
|
|
3310
|
+
switch (filter.operator) {
|
|
3311
|
+
case "isNull":
|
|
3312
|
+
return value === null || value === void 0;
|
|
3313
|
+
case "isNotNull":
|
|
3314
|
+
return value !== null && value !== void 0;
|
|
3315
|
+
case "eq":
|
|
3316
|
+
return value === filter.value;
|
|
3317
|
+
case "neq":
|
|
3318
|
+
return value !== filter.value;
|
|
3319
|
+
case "gt":
|
|
3320
|
+
return typeof value === "number" && typeof filter.value === "number" ? value > filter.value : typeof value === "string" && typeof filter.value === "string" ? value > filter.value : false;
|
|
3321
|
+
case "lt":
|
|
3322
|
+
return typeof value === "number" && typeof filter.value === "number" ? value < filter.value : typeof value === "string" && typeof filter.value === "string" ? value < filter.value : false;
|
|
3323
|
+
case "gte":
|
|
3324
|
+
return typeof value === "number" && typeof filter.value === "number" ? value >= filter.value : typeof value === "string" && typeof filter.value === "string" ? value >= filter.value : false;
|
|
3325
|
+
case "lte":
|
|
3326
|
+
return typeof value === "number" && typeof filter.value === "number" ? value <= filter.value : typeof value === "string" && typeof filter.value === "string" ? value <= filter.value : false;
|
|
3327
|
+
case "contains":
|
|
3328
|
+
return typeof value === "string" && typeof filter.value === "string" ? value.toLowerCase().includes(filter.value.toLowerCase()) : false;
|
|
3329
|
+
default:
|
|
3330
|
+
return false;
|
|
3331
|
+
}
|
|
3332
|
+
}
|
|
3333
|
+
|
|
3334
|
+
// src/providers-builtin/InMemoryStorageProvider.ts
|
|
3335
|
+
var InMemoryStorageProvider = class {
|
|
3336
|
+
stores = /* @__PURE__ */ new Map();
|
|
3337
|
+
getStore(repositoryId) {
|
|
3338
|
+
const store = this.stores.get(repositoryId);
|
|
3339
|
+
if (!store) {
|
|
3340
|
+
throw new RepositoryNotFoundError(repositoryId);
|
|
3341
|
+
}
|
|
3342
|
+
return store;
|
|
3343
|
+
}
|
|
3344
|
+
// ─── Lifecycle ─────────────────────────────────────────────────────
|
|
3345
|
+
async initialise() {
|
|
3346
|
+
}
|
|
3347
|
+
async dispose() {
|
|
3348
|
+
this.stores.clear();
|
|
3349
|
+
}
|
|
3350
|
+
// ─── Repository ────────────────────────────────────────────────────
|
|
3351
|
+
async createRepository(config) {
|
|
3352
|
+
if (this.stores.has(config.repositoryId)) {
|
|
3353
|
+
throw new DuplicateRepositoryError(config.repositoryId);
|
|
3354
|
+
}
|
|
3355
|
+
const repository = {
|
|
3356
|
+
repositoryId: config.repositoryId,
|
|
3357
|
+
type: config.type,
|
|
3358
|
+
label: config.label,
|
|
3359
|
+
description: config.description,
|
|
3360
|
+
legal: config.legal,
|
|
3361
|
+
owner: config.owner,
|
|
3362
|
+
governanceConfig: config.governanceConfig,
|
|
3363
|
+
metadata: config.metadata,
|
|
3364
|
+
createdAt: config.createdAt,
|
|
3365
|
+
createdBy: config.createdBy
|
|
3366
|
+
};
|
|
3367
|
+
this.stores.set(config.repositoryId, {
|
|
3368
|
+
repository,
|
|
3369
|
+
vocabulary: createEmptyVocabulary(config.createdBy),
|
|
3370
|
+
vocabularyChangeLog: [],
|
|
3371
|
+
entities: /* @__PURE__ */ new Map(),
|
|
3372
|
+
slugIndex: /* @__PURE__ */ new Map(),
|
|
3373
|
+
relationships: /* @__PURE__ */ new Map()
|
|
3374
|
+
});
|
|
3375
|
+
return repository;
|
|
3376
|
+
}
|
|
3377
|
+
async getRepository(repositoryId) {
|
|
3378
|
+
return this.stores.get(repositoryId)?.repository ?? null;
|
|
3379
|
+
}
|
|
3380
|
+
async listRepositories(filter) {
|
|
3381
|
+
let items = Array.from(this.stores.values()).map((store) => ({
|
|
3382
|
+
repositoryId: store.repository.repositoryId,
|
|
3383
|
+
type: store.repository.type,
|
|
3384
|
+
label: store.repository.label,
|
|
3385
|
+
description: store.repository.description,
|
|
3386
|
+
governanceConfig: store.repository.governanceConfig
|
|
3387
|
+
}));
|
|
3388
|
+
if (filter?.type) {
|
|
3389
|
+
items = items.filter((r) => r.type === filter.type);
|
|
3390
|
+
}
|
|
3391
|
+
const total = items.length;
|
|
3392
|
+
const offset = filter?.offset ?? 0;
|
|
3393
|
+
const limit = filter?.limit ?? 20;
|
|
3394
|
+
const paged = items.slice(offset, offset + limit);
|
|
3395
|
+
return {
|
|
3396
|
+
items: paged,
|
|
3397
|
+
total,
|
|
3398
|
+
hasMore: offset + paged.length < total,
|
|
3399
|
+
limit,
|
|
3400
|
+
offset
|
|
3401
|
+
};
|
|
3402
|
+
}
|
|
3403
|
+
async updateRepository(repositoryId, updates) {
|
|
3404
|
+
const store = this.stores.get(repositoryId);
|
|
3405
|
+
if (!store) {
|
|
3406
|
+
throw new RepositoryNotFoundError(repositoryId);
|
|
3407
|
+
}
|
|
3408
|
+
const repo = store.repository;
|
|
3409
|
+
if (updates.label !== void 0) repo.label = updates.label;
|
|
3410
|
+
if (updates.description !== void 0) repo.description = updates.description;
|
|
3411
|
+
if (updates.type !== void 0) repo.type = updates.type;
|
|
3412
|
+
if (updates.legal !== void 0) repo.legal = updates.legal;
|
|
3413
|
+
if (updates.owner !== void 0) repo.owner = updates.owner;
|
|
3414
|
+
if (updates.governanceConfig !== void 0) repo.governanceConfig = updates.governanceConfig;
|
|
3415
|
+
if (updates.metadata !== void 0) repo.metadata = { ...repo.metadata, ...updates.metadata };
|
|
3416
|
+
return repo;
|
|
3417
|
+
}
|
|
3418
|
+
async deleteRepository(repositoryId) {
|
|
3419
|
+
if (!this.stores.has(repositoryId)) {
|
|
3420
|
+
throw new RepositoryNotFoundError(repositoryId);
|
|
3421
|
+
}
|
|
3422
|
+
this.stores.delete(repositoryId);
|
|
3423
|
+
}
|
|
3424
|
+
async getRepositoryStats(repositoryId) {
|
|
3425
|
+
const store = this.getStore(repositoryId);
|
|
3426
|
+
const entityTypeBreakdown = {};
|
|
3427
|
+
for (const entity of store.entities.values()) {
|
|
3428
|
+
entityTypeBreakdown[entity.entityType] = (entityTypeBreakdown[entity.entityType] ?? 0) + 1;
|
|
3429
|
+
}
|
|
3430
|
+
const relationshipTypeBreakdown = {};
|
|
3431
|
+
for (const rel of store.relationships.values()) {
|
|
3432
|
+
relationshipTypeBreakdown[rel.relationshipType] = (relationshipTypeBreakdown[rel.relationshipType] ?? 0) + 1;
|
|
3433
|
+
}
|
|
3434
|
+
return {
|
|
3435
|
+
entityCount: store.entities.size,
|
|
3436
|
+
relationshipCount: store.relationships.size,
|
|
3437
|
+
vocabularyVersion: store.vocabulary.version,
|
|
3438
|
+
entityTypeBreakdown,
|
|
3439
|
+
relationshipTypeBreakdown
|
|
3440
|
+
};
|
|
3441
|
+
}
|
|
3442
|
+
// ─── Vocabulary ────────────────────────────────────────────────────
|
|
3443
|
+
async getVocabulary(repositoryId) {
|
|
3444
|
+
const store = this.getStore(repositoryId);
|
|
3445
|
+
return store.vocabulary;
|
|
3446
|
+
}
|
|
3447
|
+
async saveVocabulary(repositoryId, vocabulary) {
|
|
3448
|
+
const store = this.getStore(repositoryId);
|
|
3449
|
+
store.vocabulary = vocabulary;
|
|
3450
|
+
}
|
|
3451
|
+
async getVocabularyChangeLog(repositoryId, options) {
|
|
3452
|
+
const store = this.getStore(repositoryId);
|
|
3453
|
+
const limit = options?.limit ?? 10;
|
|
3454
|
+
const offset = options?.offset ?? 0;
|
|
3455
|
+
const items = store.vocabularyChangeLog.slice(offset, offset + limit);
|
|
3456
|
+
return {
|
|
3457
|
+
items,
|
|
3458
|
+
total: store.vocabularyChangeLog.length,
|
|
3459
|
+
hasMore: offset + limit < store.vocabularyChangeLog.length,
|
|
3460
|
+
limit,
|
|
3461
|
+
offset
|
|
3462
|
+
};
|
|
3463
|
+
}
|
|
3464
|
+
// ─── Entities ──────────────────────────────────────────────────────
|
|
3465
|
+
async createEntity(repositoryId, entity) {
|
|
3466
|
+
const store = this.getStore(repositoryId);
|
|
3467
|
+
if (store.entities.has(entity.id)) {
|
|
3468
|
+
throw new DuplicateEntityError(entity.id);
|
|
3469
|
+
}
|
|
3470
|
+
store.entities.set(entity.id, entity);
|
|
3471
|
+
store.slugIndex.set(entity.slug, entity.id);
|
|
3472
|
+
return entity;
|
|
3473
|
+
}
|
|
3474
|
+
async getEntity(repositoryId, entityId) {
|
|
3475
|
+
const store = this.getStore(repositoryId);
|
|
3476
|
+
return store.entities.get(entityId) ?? null;
|
|
3477
|
+
}
|
|
3478
|
+
async getEntityBySlug(repositoryId, slug) {
|
|
3479
|
+
const store = this.getStore(repositoryId);
|
|
3480
|
+
const id = store.slugIndex.get(slug);
|
|
3481
|
+
if (!id) return null;
|
|
3482
|
+
return store.entities.get(id) ?? null;
|
|
3483
|
+
}
|
|
3484
|
+
async getEntities(repositoryId, entityIds) {
|
|
3485
|
+
const store = this.getStore(repositoryId);
|
|
3486
|
+
const result = /* @__PURE__ */ new Map();
|
|
3487
|
+
for (const id of entityIds) {
|
|
3488
|
+
const entity = store.entities.get(id);
|
|
3489
|
+
if (entity) {
|
|
3490
|
+
result.set(id, entity);
|
|
3491
|
+
}
|
|
3492
|
+
}
|
|
3493
|
+
return result;
|
|
3494
|
+
}
|
|
3495
|
+
async updateEntity(repositoryId, entityId, updates) {
|
|
3496
|
+
const store = this.getStore(repositoryId);
|
|
3497
|
+
const existing = store.entities.get(entityId);
|
|
3498
|
+
if (!existing) {
|
|
3499
|
+
throw new EntityNotFoundError(entityId);
|
|
3500
|
+
}
|
|
3501
|
+
const updated = {
|
|
3502
|
+
...existing,
|
|
3503
|
+
label: updates.label ?? existing.label,
|
|
3504
|
+
summary: updates.summary !== void 0 ? updates.summary : existing.summary,
|
|
3505
|
+
properties: updates.properties ?? existing.properties,
|
|
3506
|
+
data: updates.data !== void 0 ? updates.data : existing.data,
|
|
3507
|
+
dataFormat: updates.dataFormat !== void 0 ? updates.dataFormat : existing.dataFormat,
|
|
3508
|
+
provenance: updates.provenance,
|
|
3509
|
+
embedding: updates.embedding ?? existing.embedding
|
|
3510
|
+
};
|
|
3511
|
+
if (updates.slug && updates.slug !== existing.slug) {
|
|
3512
|
+
store.slugIndex.delete(existing.slug);
|
|
3513
|
+
updated.slug = updates.slug;
|
|
3514
|
+
store.slugIndex.set(updates.slug, entityId);
|
|
3515
|
+
}
|
|
3516
|
+
store.entities.set(entityId, updated);
|
|
3517
|
+
return updated;
|
|
3518
|
+
}
|
|
3519
|
+
async deleteEntity(repositoryId, entityId) {
|
|
3520
|
+
const store = this.getStore(repositoryId);
|
|
3521
|
+
const existing = store.entities.get(entityId);
|
|
3522
|
+
if (!existing) {
|
|
3523
|
+
throw new EntityNotFoundError(entityId);
|
|
3524
|
+
}
|
|
3525
|
+
store.slugIndex.delete(existing.slug);
|
|
3526
|
+
store.entities.delete(entityId);
|
|
3527
|
+
for (const [relId, rel] of store.relationships) {
|
|
3528
|
+
if (rel.sourceEntityId === entityId || rel.targetEntityId === entityId) {
|
|
3529
|
+
store.relationships.delete(relId);
|
|
3530
|
+
}
|
|
3531
|
+
}
|
|
3532
|
+
}
|
|
3533
|
+
async deleteEntitiesByType(repositoryId, entityType) {
|
|
3534
|
+
const store = this.getStore(repositoryId);
|
|
3535
|
+
const entityIds = /* @__PURE__ */ new Set();
|
|
3536
|
+
for (const [id, entity] of store.entities) {
|
|
3537
|
+
if (entity.entityType === entityType) {
|
|
3538
|
+
entityIds.add(id);
|
|
3539
|
+
store.slugIndex.delete(entity.slug);
|
|
3540
|
+
}
|
|
3541
|
+
}
|
|
3542
|
+
let deletedRelationships = 0;
|
|
3543
|
+
for (const [relId, rel] of store.relationships) {
|
|
3544
|
+
if (entityIds.has(rel.sourceEntityId) || entityIds.has(rel.targetEntityId)) {
|
|
3545
|
+
store.relationships.delete(relId);
|
|
3546
|
+
deletedRelationships++;
|
|
3547
|
+
}
|
|
3548
|
+
}
|
|
3549
|
+
for (const id of entityIds) {
|
|
3550
|
+
store.entities.delete(id);
|
|
3551
|
+
}
|
|
3552
|
+
return { deletedEntities: entityIds.size, deletedRelationships };
|
|
3553
|
+
}
|
|
3554
|
+
async findEntities(repositoryId, query) {
|
|
3555
|
+
const store = this.getStore(repositoryId);
|
|
3556
|
+
let matches = Array.from(store.entities.values());
|
|
3557
|
+
if (query.entityTypes && query.entityTypes.length > 0) {
|
|
3558
|
+
matches = matches.filter((e) => query.entityTypes.includes(e.entityType));
|
|
3559
|
+
}
|
|
3560
|
+
if (query.searchTerm) {
|
|
3561
|
+
const term = query.searchTerm.toLowerCase();
|
|
3562
|
+
matches = matches.filter(
|
|
3563
|
+
(e) => e.label.toLowerCase().includes(term) || e.summary && e.summary.toLowerCase().includes(term)
|
|
3564
|
+
);
|
|
3565
|
+
}
|
|
3566
|
+
if (query.properties) {
|
|
3567
|
+
const entries = Object.entries(query.properties);
|
|
3568
|
+
matches = matches.filter(
|
|
3569
|
+
(e) => entries.every(([key, value]) => e.properties[key] === value)
|
|
3570
|
+
);
|
|
3571
|
+
}
|
|
3572
|
+
if (query.provenance) {
|
|
3573
|
+
matches = matches.filter((e) => matchesProvenance(e.provenance, query.provenance));
|
|
3574
|
+
}
|
|
3575
|
+
const total = matches.length;
|
|
3576
|
+
const items = matches.slice(query.offset, query.offset + query.limit);
|
|
3577
|
+
return {
|
|
3578
|
+
items,
|
|
3579
|
+
total,
|
|
3580
|
+
hasMore: query.offset + query.limit < total,
|
|
3581
|
+
limit: query.limit,
|
|
3582
|
+
offset: query.offset
|
|
3583
|
+
};
|
|
3584
|
+
}
|
|
3585
|
+
// ─── Relationships ─────────────────────────────────────────────────
|
|
3586
|
+
async createRelationship(repositoryId, relationship) {
|
|
3587
|
+
const store = this.getStore(repositoryId);
|
|
3588
|
+
if (store.relationships.has(relationship.id)) {
|
|
3589
|
+
throw new DuplicateRelationshipError(relationship.id);
|
|
3590
|
+
}
|
|
3591
|
+
store.relationships.set(relationship.id, relationship);
|
|
3592
|
+
return relationship;
|
|
3593
|
+
}
|
|
3594
|
+
async getRelationship(repositoryId, relationshipId) {
|
|
3595
|
+
const store = this.getStore(repositoryId);
|
|
3596
|
+
return store.relationships.get(relationshipId) ?? null;
|
|
3597
|
+
}
|
|
3598
|
+
async getEntityRelationships(repositoryId, entityId, options) {
|
|
3599
|
+
const store = this.getStore(repositoryId);
|
|
3600
|
+
const direction = options?.direction ?? "both";
|
|
3601
|
+
let matches = Array.from(store.relationships.values());
|
|
3602
|
+
matches = matches.filter((rel) => {
|
|
3603
|
+
const isSource = rel.sourceEntityId === entityId;
|
|
3604
|
+
const isTarget = rel.targetEntityId === entityId;
|
|
3605
|
+
const isBidirectionalTarget = rel.bidirectional && isTarget;
|
|
3606
|
+
switch (direction) {
|
|
3607
|
+
case "outbound":
|
|
3608
|
+
return isSource || isBidirectionalTarget;
|
|
3609
|
+
case "inbound":
|
|
3610
|
+
return isTarget || rel.bidirectional && isSource;
|
|
3611
|
+
case "both":
|
|
3612
|
+
default:
|
|
3613
|
+
return isSource || isTarget;
|
|
3614
|
+
}
|
|
3615
|
+
});
|
|
3616
|
+
if (options?.relationshipTypes && options.relationshipTypes.length > 0) {
|
|
3617
|
+
matches = matches.filter((r) => options.relationshipTypes.includes(r.relationshipType));
|
|
3618
|
+
}
|
|
3619
|
+
if (options?.propertyFilters && options.propertyFilters.length > 0) {
|
|
3620
|
+
matches = matches.filter((r) => matchesPropertyFilters(r.properties, options.propertyFilters));
|
|
3621
|
+
}
|
|
3622
|
+
const total = matches.length;
|
|
3623
|
+
const limit = options?.limit ?? 10;
|
|
3624
|
+
const offset = options?.offset ?? 0;
|
|
3625
|
+
const items = matches.slice(offset, offset + limit);
|
|
3626
|
+
return {
|
|
3627
|
+
items,
|
|
3628
|
+
total,
|
|
3629
|
+
hasMore: offset + limit < total,
|
|
3630
|
+
limit,
|
|
3631
|
+
offset
|
|
3632
|
+
};
|
|
3633
|
+
}
|
|
3634
|
+
async deleteRelationship(repositoryId, relationshipId) {
|
|
3635
|
+
const store = this.getStore(repositoryId);
|
|
3636
|
+
if (!store.relationships.has(relationshipId)) {
|
|
3637
|
+
throw new RelationshipNotFoundError(relationshipId);
|
|
3638
|
+
}
|
|
3639
|
+
store.relationships.delete(relationshipId);
|
|
3640
|
+
}
|
|
3641
|
+
async deleteRelationshipsByType(repositoryId, relationshipType) {
|
|
3642
|
+
const store = this.getStore(repositoryId);
|
|
3643
|
+
let deletedRelationships = 0;
|
|
3644
|
+
for (const [relId, rel] of store.relationships) {
|
|
3645
|
+
if (rel.relationshipType === relationshipType) {
|
|
3646
|
+
store.relationships.delete(relId);
|
|
3647
|
+
deletedRelationships++;
|
|
3648
|
+
}
|
|
3649
|
+
}
|
|
3650
|
+
return { deletedRelationships };
|
|
3651
|
+
}
|
|
3652
|
+
// ─── Graph Traversal ───────────────────────────────────────────────
|
|
3653
|
+
async exploreNeighbourhood(repositoryId, entityId, options) {
|
|
3654
|
+
const store = this.getStore(repositoryId);
|
|
3655
|
+
if (!store.entities.has(entityId)) {
|
|
3656
|
+
throw new EntityNotFoundError(entityId);
|
|
3657
|
+
}
|
|
3658
|
+
const layers = [];
|
|
3659
|
+
const visited = /* @__PURE__ */ new Set([entityId]);
|
|
3660
|
+
let currentFrontier = /* @__PURE__ */ new Set([entityId]);
|
|
3661
|
+
for (let depth = 0; depth < options.depth; depth++) {
|
|
3662
|
+
const layer = {};
|
|
3663
|
+
const nextFrontier = /* @__PURE__ */ new Set();
|
|
3664
|
+
for (const frontierEntityId of currentFrontier) {
|
|
3665
|
+
for (const rel of store.relationships.values()) {
|
|
3666
|
+
const isSource = rel.sourceEntityId === frontierEntityId;
|
|
3667
|
+
const isTarget = rel.targetEntityId === frontierEntityId;
|
|
3668
|
+
let matchesDirection = false;
|
|
3669
|
+
let connectedEntityId;
|
|
3670
|
+
if (isSource && (options.direction === "outbound" || options.direction === "both")) {
|
|
3671
|
+
matchesDirection = true;
|
|
3672
|
+
connectedEntityId = rel.targetEntityId;
|
|
3673
|
+
} else if (isTarget && (options.direction === "inbound" || options.direction === "both")) {
|
|
3674
|
+
matchesDirection = true;
|
|
3675
|
+
connectedEntityId = rel.sourceEntityId;
|
|
3676
|
+
} else if (rel.bidirectional) {
|
|
3677
|
+
if (isSource && options.direction === "inbound") {
|
|
3678
|
+
matchesDirection = true;
|
|
3679
|
+
connectedEntityId = rel.targetEntityId;
|
|
3680
|
+
} else if (isTarget && options.direction === "outbound") {
|
|
3681
|
+
matchesDirection = true;
|
|
3682
|
+
connectedEntityId = rel.sourceEntityId;
|
|
3683
|
+
}
|
|
3684
|
+
}
|
|
3685
|
+
if (!matchesDirection || !connectedEntityId) continue;
|
|
3686
|
+
if (visited.has(connectedEntityId)) continue;
|
|
3687
|
+
if (options.relationshipTypes && !options.relationshipTypes.includes(rel.relationshipType)) {
|
|
3688
|
+
continue;
|
|
3689
|
+
}
|
|
3690
|
+
if (options.relationshipPropertyFilters && options.relationshipPropertyFilters.length > 0) {
|
|
3691
|
+
if (!matchesPropertyFilters(rel.properties, options.relationshipPropertyFilters)) {
|
|
3692
|
+
continue;
|
|
3693
|
+
}
|
|
3694
|
+
}
|
|
3695
|
+
const connectedEntity = store.entities.get(connectedEntityId);
|
|
3696
|
+
if (!connectedEntity) continue;
|
|
3697
|
+
if (options.entityTypes && !options.entityTypes.includes(connectedEntity.entityType)) {
|
|
3698
|
+
continue;
|
|
3699
|
+
}
|
|
3700
|
+
const relType = rel.relationshipType;
|
|
3701
|
+
if (!layer[relType]) {
|
|
3702
|
+
layer[relType] = { total: 0, entities: [], relationships: [] };
|
|
3703
|
+
}
|
|
3704
|
+
layer[relType].total++;
|
|
3705
|
+
const group = layer[relType];
|
|
3706
|
+
if (group.entities.length < options.limitPerType) {
|
|
3707
|
+
group.entities.push(connectedEntity);
|
|
3708
|
+
group.relationships.push(rel);
|
|
3709
|
+
}
|
|
3710
|
+
nextFrontier.add(connectedEntityId);
|
|
3711
|
+
visited.add(connectedEntityId);
|
|
3712
|
+
}
|
|
3713
|
+
}
|
|
3714
|
+
layers.push(layer);
|
|
3715
|
+
currentFrontier = nextFrontier;
|
|
3716
|
+
if (nextFrontier.size === 0) break;
|
|
3717
|
+
}
|
|
3718
|
+
return {
|
|
3719
|
+
centreId: entityId,
|
|
3720
|
+
layers
|
|
3721
|
+
};
|
|
3722
|
+
}
|
|
3723
|
+
async findPaths(repositoryId, sourceId, targetId, options) {
|
|
3724
|
+
const store = this.getStore(repositoryId);
|
|
3725
|
+
if (!store.entities.has(sourceId)) {
|
|
3726
|
+
throw new EntityNotFoundError(sourceId);
|
|
3727
|
+
}
|
|
3728
|
+
if (!store.entities.has(targetId)) {
|
|
3729
|
+
throw new EntityNotFoundError(targetId);
|
|
3730
|
+
}
|
|
3731
|
+
if (sourceId === targetId) {
|
|
3732
|
+
return { paths: [{ entityIds: [sourceId], relationshipIds: [] }], totalPaths: 1 };
|
|
3733
|
+
}
|
|
3734
|
+
const paths = [];
|
|
3735
|
+
const queue = [
|
|
3736
|
+
{ entityId: sourceId, path: [sourceId], relPath: [] }
|
|
3737
|
+
];
|
|
3738
|
+
const visitedAtDepth = /* @__PURE__ */ new Map();
|
|
3739
|
+
visitedAtDepth.set(sourceId, 0);
|
|
3740
|
+
while (queue.length > 0 && paths.length < options.limit + options.offset) {
|
|
3741
|
+
const current = queue.shift();
|
|
3742
|
+
if (current.path.length > options.maxDepth + 1) continue;
|
|
3743
|
+
for (const rel of store.relationships.values()) {
|
|
3744
|
+
if (options.relationshipTypes && !options.relationshipTypes.includes(rel.relationshipType)) {
|
|
3745
|
+
continue;
|
|
3746
|
+
}
|
|
3747
|
+
if (options.relationshipPropertyFilters && options.relationshipPropertyFilters.length > 0) {
|
|
3748
|
+
if (!matchesPropertyFilters(rel.properties, options.relationshipPropertyFilters)) {
|
|
3749
|
+
continue;
|
|
3750
|
+
}
|
|
3751
|
+
}
|
|
3752
|
+
let nextEntityId;
|
|
3753
|
+
if (rel.sourceEntityId === current.entityId) {
|
|
3754
|
+
nextEntityId = rel.targetEntityId;
|
|
3755
|
+
} else if (rel.targetEntityId === current.entityId) {
|
|
3756
|
+
nextEntityId = rel.sourceEntityId;
|
|
3757
|
+
}
|
|
3758
|
+
if (!nextEntityId) continue;
|
|
3759
|
+
if (current.path.includes(nextEntityId) && nextEntityId !== targetId) continue;
|
|
3760
|
+
if (options.entityTypes && nextEntityId !== targetId) {
|
|
3761
|
+
const nextEntity = store.entities.get(nextEntityId);
|
|
3762
|
+
if (nextEntity && !options.entityTypes.includes(nextEntity.entityType)) continue;
|
|
3763
|
+
}
|
|
3764
|
+
const newPath = [...current.path, nextEntityId];
|
|
3765
|
+
const newRelPath = [...current.relPath, rel.id];
|
|
3766
|
+
if (nextEntityId === targetId) {
|
|
3767
|
+
paths.push({ entityIds: newPath, relationshipIds: newRelPath });
|
|
3768
|
+
} else if (newPath.length <= options.maxDepth) {
|
|
3769
|
+
const prevDepth = visitedAtDepth.get(nextEntityId);
|
|
3770
|
+
if (prevDepth === void 0 || prevDepth >= newPath.length - 1) {
|
|
3771
|
+
visitedAtDepth.set(nextEntityId, newPath.length - 1);
|
|
3772
|
+
queue.push({ entityId: nextEntityId, path: newPath, relPath: newRelPath });
|
|
3773
|
+
}
|
|
3774
|
+
}
|
|
3775
|
+
}
|
|
3776
|
+
}
|
|
3777
|
+
const paginatedPaths = paths.slice(options.offset, options.offset + options.limit);
|
|
3778
|
+
return {
|
|
3779
|
+
paths: paginatedPaths,
|
|
3780
|
+
totalPaths: paths.length
|
|
3781
|
+
};
|
|
3782
|
+
}
|
|
3783
|
+
// ─── Timeline ──────────────────────────────────────────────────────
|
|
3784
|
+
async getTimeline(repositoryId, entityId, options) {
|
|
3785
|
+
const store = this.getStore(repositoryId);
|
|
3786
|
+
const entity = store.entities.get(entityId);
|
|
3787
|
+
if (!entity) {
|
|
3788
|
+
throw new EntityNotFoundError(entityId);
|
|
3789
|
+
}
|
|
3790
|
+
const events = [];
|
|
3791
|
+
events.push({
|
|
3792
|
+
timestamp: entity.provenance.createdAt,
|
|
3793
|
+
eventType: "entity:created",
|
|
3794
|
+
entityId
|
|
3795
|
+
});
|
|
3796
|
+
if (entity.provenance.modifiedAt !== entity.provenance.createdAt) {
|
|
3797
|
+
events.push({
|
|
3798
|
+
timestamp: entity.provenance.modifiedAt,
|
|
3799
|
+
eventType: "entity:updated",
|
|
3800
|
+
entityId
|
|
3801
|
+
});
|
|
3802
|
+
}
|
|
3803
|
+
for (const rel of store.relationships.values()) {
|
|
3804
|
+
if (rel.sourceEntityId === entityId || rel.targetEntityId === entityId) {
|
|
3805
|
+
events.push({
|
|
3806
|
+
timestamp: rel.provenance.createdAt,
|
|
3807
|
+
eventType: "relationship:created",
|
|
3808
|
+
entityId,
|
|
3809
|
+
relationshipId: rel.id
|
|
3810
|
+
});
|
|
3811
|
+
}
|
|
3812
|
+
}
|
|
3813
|
+
let filtered = events;
|
|
3814
|
+
if (options.timeRange) {
|
|
3815
|
+
const from = new Date(options.timeRange.from).getTime();
|
|
3816
|
+
const to = new Date(options.timeRange.to).getTime();
|
|
3817
|
+
filtered = filtered.filter((e) => {
|
|
3818
|
+
const t = new Date(e.timestamp).getTime();
|
|
3819
|
+
return t >= from && t <= to;
|
|
3820
|
+
});
|
|
3821
|
+
}
|
|
3822
|
+
if (options.eventTypes && options.eventTypes.length > 0) {
|
|
3823
|
+
filtered = filtered.filter((e) => options.eventTypes.includes(e.eventType));
|
|
3824
|
+
}
|
|
3825
|
+
if (options.provenance) {
|
|
3826
|
+
const prov = options.provenance;
|
|
3827
|
+
filtered = filtered.filter((e) => {
|
|
3828
|
+
if (e.eventType.startsWith("entity:")) {
|
|
3829
|
+
const ent = store.entities.get(e.entityId);
|
|
3830
|
+
return ent ? matchesProvenance(ent.provenance, prov) : false;
|
|
3831
|
+
}
|
|
3832
|
+
if (e.relationshipId) {
|
|
3833
|
+
const rel = store.relationships.get(e.relationshipId);
|
|
3834
|
+
return rel ? matchesProvenance(rel.provenance, prov) : false;
|
|
3835
|
+
}
|
|
3836
|
+
return true;
|
|
3837
|
+
});
|
|
3838
|
+
}
|
|
3839
|
+
filtered.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
3840
|
+
const total = filtered.length;
|
|
3841
|
+
const items = filtered.slice(options.offset, options.offset + options.limit);
|
|
3842
|
+
return {
|
|
3843
|
+
events: items,
|
|
3844
|
+
total
|
|
3845
|
+
};
|
|
3846
|
+
}
|
|
3847
|
+
// ─── Bulk Operations ───────────────────────────────────────────────
|
|
3848
|
+
async *exportAll(repositoryId) {
|
|
3849
|
+
const store = this.getStore(repositoryId);
|
|
3850
|
+
const entities = Array.from(store.entities.values());
|
|
3851
|
+
const batchSize = 100;
|
|
3852
|
+
for (let i = 0; i < entities.length; i += batchSize) {
|
|
3853
|
+
const batch = entities.slice(i, i + batchSize);
|
|
3854
|
+
yield {
|
|
3855
|
+
type: "entities",
|
|
3856
|
+
data: batch,
|
|
3857
|
+
sequence: Math.floor(i / batchSize),
|
|
3858
|
+
isLast: i + batchSize >= entities.length
|
|
3859
|
+
};
|
|
3860
|
+
}
|
|
3861
|
+
if (entities.length === 0) {
|
|
3862
|
+
yield { type: "entities", data: [], sequence: 0, isLast: true };
|
|
3863
|
+
}
|
|
3864
|
+
const relationships = Array.from(store.relationships.values());
|
|
3865
|
+
for (let i = 0; i < relationships.length; i += batchSize) {
|
|
3866
|
+
const batch = relationships.slice(i, i + batchSize);
|
|
3867
|
+
yield {
|
|
3868
|
+
type: "relationships",
|
|
3869
|
+
data: batch,
|
|
3870
|
+
sequence: Math.floor(i / batchSize),
|
|
3871
|
+
isLast: i + batchSize >= relationships.length
|
|
3872
|
+
};
|
|
3873
|
+
}
|
|
3874
|
+
if (relationships.length === 0) {
|
|
3875
|
+
yield { type: "relationships", data: [], sequence: 0, isLast: true };
|
|
3876
|
+
}
|
|
3877
|
+
}
|
|
3878
|
+
async importBulk(repositoryId, data) {
|
|
3879
|
+
const store = this.getStore(repositoryId);
|
|
3880
|
+
let entitiesImported = 0;
|
|
3881
|
+
let relationshipsImported = 0;
|
|
3882
|
+
const errors = [];
|
|
3883
|
+
for (const chunk of data) {
|
|
3884
|
+
if (chunk.entities) {
|
|
3885
|
+
for (const entity of chunk.entities) {
|
|
3886
|
+
try {
|
|
3887
|
+
store.entities.set(entity.id, entity);
|
|
3888
|
+
store.slugIndex.set(entity.slug, entity.id);
|
|
3889
|
+
entitiesImported++;
|
|
3890
|
+
} catch (err) {
|
|
3891
|
+
errors.push({
|
|
3892
|
+
item: entity.id,
|
|
3893
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3894
|
+
});
|
|
3895
|
+
}
|
|
3896
|
+
}
|
|
3897
|
+
}
|
|
3898
|
+
if (chunk.relationships) {
|
|
3899
|
+
for (const rel of chunk.relationships) {
|
|
3900
|
+
try {
|
|
3901
|
+
store.relationships.set(rel.id, rel);
|
|
3902
|
+
relationshipsImported++;
|
|
3903
|
+
} catch (err) {
|
|
3904
|
+
errors.push({
|
|
3905
|
+
item: rel.id,
|
|
3906
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3907
|
+
});
|
|
3908
|
+
}
|
|
3909
|
+
}
|
|
3910
|
+
}
|
|
3911
|
+
}
|
|
3912
|
+
return { entitiesImported, relationshipsImported, errors };
|
|
3913
|
+
}
|
|
3914
|
+
};
|
|
3915
|
+
function matchesProvenance(prov, filter) {
|
|
3916
|
+
if (filter.conversationIds && filter.conversationIds.length > 0) {
|
|
3917
|
+
const matchesCreated = prov.createdInConversation && filter.conversationIds.includes(prov.createdInConversation);
|
|
3918
|
+
const matchesModified = prov.modifiedInConversation && filter.conversationIds.includes(prov.modifiedInConversation);
|
|
3919
|
+
if (!matchesCreated && !matchesModified) return false;
|
|
3920
|
+
}
|
|
3921
|
+
if (filter.actors && filter.actors.length > 0) {
|
|
3922
|
+
const matchesCreator = filter.actors.includes(prov.createdBy);
|
|
3923
|
+
const matchesModifier = filter.actors.includes(prov.modifiedBy);
|
|
3924
|
+
if (!matchesCreator && !matchesModifier) return false;
|
|
3925
|
+
}
|
|
3926
|
+
if (filter.dateRange) {
|
|
3927
|
+
const from = new Date(filter.dateRange.from).getTime();
|
|
3928
|
+
const to = new Date(filter.dateRange.to).getTime();
|
|
3929
|
+
const created = new Date(prov.createdAt).getTime();
|
|
3930
|
+
const modified = new Date(prov.modifiedAt).getTime();
|
|
3931
|
+
if (!(created >= from && created <= to) && !(modified >= from && modified <= to)) return false;
|
|
3932
|
+
}
|
|
3933
|
+
return true;
|
|
3934
|
+
}
|
|
3935
|
+
|
|
3936
|
+
// src/providers-builtin/InMemorySearchProvider.ts
|
|
3937
|
+
var InMemorySearchProvider = class {
|
|
3938
|
+
indices = /* @__PURE__ */ new Map();
|
|
3939
|
+
getIndex(repositoryId) {
|
|
3940
|
+
if (!this.indices.has(repositoryId)) {
|
|
3941
|
+
this.indices.set(repositoryId, /* @__PURE__ */ new Map());
|
|
3942
|
+
}
|
|
3943
|
+
return this.indices.get(repositoryId);
|
|
3944
|
+
}
|
|
3945
|
+
async indexEntity(repositoryId, entity) {
|
|
3946
|
+
const index = this.getIndex(repositoryId);
|
|
3947
|
+
const parts = [entity.label, entity.summary ?? ""];
|
|
3948
|
+
if (entity.properties) {
|
|
3949
|
+
for (const value of Object.values(entity.properties)) {
|
|
3950
|
+
if (typeof value === "string") {
|
|
3951
|
+
parts.push(value);
|
|
3952
|
+
}
|
|
3953
|
+
}
|
|
3954
|
+
}
|
|
3955
|
+
if (entity.data) {
|
|
3956
|
+
parts.push(entity.data);
|
|
3957
|
+
}
|
|
3958
|
+
index.set(entity.entityId, {
|
|
3959
|
+
entityId: entity.entityId,
|
|
3960
|
+
entityType: entity.entityType,
|
|
3961
|
+
text: parts.join(" ").toLowerCase()
|
|
3962
|
+
});
|
|
3963
|
+
}
|
|
3964
|
+
async removeEntity(repositoryId, entityId) {
|
|
3965
|
+
this.getIndex(repositoryId).delete(entityId);
|
|
3966
|
+
}
|
|
3967
|
+
async search(repositoryId, query, options) {
|
|
3968
|
+
const index = this.getIndex(repositoryId);
|
|
3969
|
+
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
3970
|
+
if (terms.length === 0) {
|
|
3971
|
+
return { items: [], total: 0, hasMore: false, limit: options?.limit ?? 10, offset: options?.offset ?? 0 };
|
|
3972
|
+
}
|
|
3973
|
+
let hits = [];
|
|
3974
|
+
for (const entry of index.values()) {
|
|
3975
|
+
if (options?.entityTypes && options.entityTypes.length > 0) {
|
|
3976
|
+
if (!options.entityTypes.includes(entry.entityType)) continue;
|
|
3977
|
+
}
|
|
3978
|
+
let matchCount = 0;
|
|
3979
|
+
for (const term of terms) {
|
|
3980
|
+
if (entry.text.includes(term)) {
|
|
3981
|
+
matchCount++;
|
|
3982
|
+
}
|
|
3983
|
+
}
|
|
3984
|
+
if (matchCount > 0) {
|
|
3985
|
+
hits.push({ entry, score: matchCount / terms.length });
|
|
3986
|
+
}
|
|
3987
|
+
}
|
|
3988
|
+
hits.sort((a, b) => b.score - a.score);
|
|
3989
|
+
const total = hits.length;
|
|
3990
|
+
const limit = options?.limit ?? 10;
|
|
3991
|
+
const offset = options?.offset ?? 0;
|
|
3992
|
+
const page = hits.slice(offset, offset + limit);
|
|
3993
|
+
return {
|
|
3994
|
+
items: page.map((h) => ({
|
|
3995
|
+
id: h.entry.entityId,
|
|
3996
|
+
score: h.score
|
|
3997
|
+
})),
|
|
3998
|
+
total,
|
|
3999
|
+
hasMore: offset + limit < total,
|
|
4000
|
+
limit,
|
|
4001
|
+
offset
|
|
4002
|
+
};
|
|
4003
|
+
}
|
|
4004
|
+
async reindexRepository(repositoryId, entities) {
|
|
4005
|
+
this.indices.set(repositoryId, /* @__PURE__ */ new Map());
|
|
4006
|
+
for await (const entity of entities) {
|
|
4007
|
+
await this.indexEntity(repositoryId, entity);
|
|
4008
|
+
}
|
|
4009
|
+
}
|
|
4010
|
+
};
|
|
4011
|
+
|
|
4012
|
+
// src/providers-builtin/NoOpEmbeddingProvider.ts
|
|
4013
|
+
var NoOpEmbeddingProvider = class {
|
|
4014
|
+
async embed(_text) {
|
|
4015
|
+
throw new EmbeddingProviderRequiredError();
|
|
4016
|
+
}
|
|
4017
|
+
async embedBatch(_texts) {
|
|
4018
|
+
throw new EmbeddingProviderRequiredError();
|
|
4019
|
+
}
|
|
4020
|
+
dimensions() {
|
|
4021
|
+
return 0;
|
|
4022
|
+
}
|
|
4023
|
+
modelId() {
|
|
4024
|
+
return "none";
|
|
4025
|
+
}
|
|
4026
|
+
};
|
|
4027
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
4028
|
+
0 && (module.exports = {
|
|
4029
|
+
DeepMemory,
|
|
4030
|
+
DeepMemoryError,
|
|
4031
|
+
DuplicateEntityError,
|
|
4032
|
+
DuplicateRelationshipError,
|
|
4033
|
+
DuplicateRepositoryError,
|
|
4034
|
+
EmbeddingProviderRequiredError,
|
|
4035
|
+
EntityNotFoundError,
|
|
4036
|
+
ExportError,
|
|
4037
|
+
GovernanceDeniedError,
|
|
4038
|
+
ImportError,
|
|
4039
|
+
InMemorySearchProvider,
|
|
4040
|
+
InMemoryStorageProvider,
|
|
4041
|
+
InvalidInputError,
|
|
4042
|
+
MemoryRepository,
|
|
4043
|
+
NoOpEmbeddingProvider,
|
|
4044
|
+
OperationCancelledError,
|
|
4045
|
+
ProviderError,
|
|
4046
|
+
RelationshipConstraintError,
|
|
4047
|
+
RelationshipNotFoundError,
|
|
4048
|
+
RepositoryNotFoundError,
|
|
4049
|
+
VocabularyValidationError,
|
|
4050
|
+
generateId,
|
|
4051
|
+
isValidUuid,
|
|
4052
|
+
matchesPropertyFilters
|
|
4053
|
+
});
|
|
4054
|
+
//# sourceMappingURL=index.cjs.map
|