sigma-memory 0.2.1 → 0.2.2

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/src/ontology.ts CHANGED
@@ -1,354 +1,361 @@
1
- import { readFileSync, writeFileSync, existsSync, mkdirSync, appendFileSync } from 'fs';
2
- import { join, dirname } from 'path';
3
- import { randomBytes } from 'crypto';
4
- import type {
5
- MemoryConfig,
6
- OntologyEntity,
7
- OntologyRelation,
8
- OntologyJSONLEntry,
9
- OntologyEntityEntry,
10
- OntologyRelationEntry,
11
- OntologyDeleteEntry
12
- } from './types.js';
1
+ import { randomBytes } from "crypto";
2
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
3
+ import { dirname, join } from "path";
4
+ import type {
5
+ MemoryConfig,
6
+ OntologyDeleteEntry,
7
+ OntologyEntity,
8
+ OntologyEntityEntry,
9
+ OntologyJSONLEntry,
10
+ OntologyRelation,
11
+ OntologyRelationEntry,
12
+ } from "./types.js";
13
13
 
14
14
  export class OntologyManager {
15
- private config: MemoryConfig;
16
- private graphPath: string;
17
- private entities: Map<string, OntologyEntity> = new Map();
18
- private relations: Map<string, OntologyRelation> = new Map();
19
- private loaded = false;
20
-
21
- constructor(config: MemoryConfig) {
22
- this.config = config;
23
- this.graphPath = config.ontologyPath;
24
- this.ensureDirectories();
25
- }
26
-
27
- private ensureDirectories(): void {
28
- const dir = dirname(this.graphPath);
29
- if (!existsSync(dir)) {
30
- mkdirSync(dir, { recursive: true });
31
- }
32
- }
33
-
34
- private generateId(): string {
35
- return randomBytes(16).toString('hex');
36
- }
37
-
38
- private loadGraph(): void {
39
- if (this.loaded) return;
40
-
41
- this.entities.clear();
42
- this.relations.clear();
43
-
44
- if (!existsSync(this.graphPath)) {
45
- this.loaded = true;
46
- return;
47
- }
48
-
49
- const content = readFileSync(this.graphPath, 'utf8');
50
- const lines = content.trim().split('\n').filter(line => line.trim());
51
-
52
- for (const line of lines) {
53
- try {
54
- const entry: OntologyJSONLEntry = JSON.parse(line);
55
-
56
- switch (entry.kind) {
57
- case 'entity':
58
- this.entities.set(entry.id, {
59
- id: entry.id,
60
- type: entry.type,
61
- name: entry.name,
62
- properties: entry.properties,
63
- createdAt: entry.createdAt,
64
- updatedAt: entry.updatedAt
65
- });
66
- break;
67
-
68
- case 'relation':
69
- this.relations.set(entry.id, {
70
- id: entry.id,
71
- from: entry.from,
72
- to: entry.to,
73
- type: entry.type,
74
- properties: entry.properties,
75
- createdAt: entry.createdAt
76
- });
77
- break;
78
-
79
- case 'delete':
80
- // Delete entity or relation
81
- this.entities.delete(entry.targetId);
82
- this.relations.delete(entry.targetId);
83
- // Also delete all relations linked to this entity
84
- for (const [relationId, relation] of this.relations) {
85
- if (relation.from === entry.targetId || relation.to === entry.targetId) {
86
- this.relations.delete(relationId);
87
- }
88
- }
89
- break;
90
- }
91
- } catch (error) {
92
- // Skip malformed JSONL line
93
- }
94
- }
95
-
96
- this.loaded = true;
97
- }
98
-
99
- private appendToFile(entry: OntologyJSONLEntry): void {
100
- this.ensureDirectories();
101
- const line = JSON.stringify(entry) + '\n';
102
- appendFileSync(this.graphPath, line, 'utf8');
103
- }
104
-
105
- /**
106
- * Ajoute une entité
107
- */
108
- addEntity(entity: Omit<OntologyEntity, 'id' | 'createdAt' | 'updatedAt'>): string {
109
- this.loadGraph();
110
-
111
- const id = this.generateId();
112
- const now = new Date().toISOString();
113
-
114
- const newEntity: OntologyEntity = {
115
- ...entity,
116
- id,
117
- createdAt: now,
118
- updatedAt: now
119
- };
120
-
121
- this.entities.set(id, newEntity);
122
-
123
- const entry: OntologyEntityEntry = {
124
- kind: 'entity',
125
- ...newEntity
126
- };
127
-
128
- this.appendToFile(entry);
129
- return id;
130
- }
131
-
132
- /**
133
- * Ajoute une relation
134
- */
135
- addRelation(relation: Omit<OntologyRelation, 'id' | 'createdAt'>): string {
136
- this.loadGraph();
137
-
138
- // Verify source and destination entities exist
139
- if (!this.entities.has(relation.from)) {
140
- throw new Error(`Source entity not found: ${relation.from}`);
141
- }
142
- if (!this.entities.has(relation.to)) {
143
- throw new Error(`Target entity not found: ${relation.to}`);
144
- }
145
-
146
- const id = this.generateId();
147
- const now = new Date().toISOString();
148
-
149
- const newRelation: OntologyRelation = {
150
- ...relation,
151
- id,
152
- createdAt: now
153
- };
154
-
155
- this.relations.set(id, newRelation);
156
-
157
- const entry: OntologyRelationEntry = {
158
- kind: 'relation',
159
- ...newRelation
160
- };
161
-
162
- this.appendToFile(entry);
163
- return id;
164
- }
165
-
166
- /**
167
- * Recherche par id/type/nom
168
- */
169
- findEntity(query: { id?: string; type?: string; name?: string }): OntologyEntity[] {
170
- this.loadGraph();
171
-
172
- // Direct ID lookup - return exact match
173
- if (query.id) {
174
- const entity = this.entities.get(query.id);
175
- return entity ? [entity] : [];
176
- }
177
-
178
- const results: OntologyEntity[] = [];
179
-
180
- for (const entity of this.entities.values()) {
181
- let matches = true;
182
-
183
- if (query.type && entity.type !== query.type) {
184
- matches = false;
185
- }
186
-
187
- if (query.name && !entity.name.toLowerCase().includes(query.name.toLowerCase())) {
188
- matches = false;
189
- }
190
-
191
- if (matches) {
192
- results.push(entity);
193
- }
194
- }
195
-
196
- return results;
197
- }
198
-
199
- /**
200
- * Toutes les relations d'une entité
201
- */
202
- findRelations(entityId: string): OntologyRelation[] {
203
- this.loadGraph();
204
-
205
- const results: OntologyRelation[] = [];
206
-
207
- for (const relation of this.relations.values()) {
208
- if (relation.from === entityId || relation.to === entityId) {
209
- results.push(relation);
210
- }
211
- }
212
-
213
- return results;
214
- }
215
-
216
- /**
217
- * Retourne le graphe complet
218
- */
219
- getGraph(): { entities: OntologyEntity[]; relations: OntologyRelation[] } {
220
- this.loadGraph();
221
-
222
- return {
223
- entities: Array.from(this.entities.values()),
224
- relations: Array.from(this.relations.values())
225
- };
226
- }
227
-
228
- /**
229
- * Supprime entité + ses relations
230
- */
231
- removeEntity(id: string): void {
232
- this.loadGraph();
233
-
234
- if (!this.entities.has(id)) {
235
- throw new Error(`Entity not found: ${id}`);
236
- }
237
-
238
- // Mark as deleted in file
239
- const deleteEntry: OntologyDeleteEntry = {
240
- kind: 'delete',
241
- targetId: id,
242
- deletedAt: new Date().toISOString()
243
- };
244
-
245
- this.appendToFile(deleteEntry);
246
-
247
- // Remove from memory
248
- this.entities.delete(id);
249
-
250
- // Remove all linked relations
251
- for (const [relationId, relation] of this.relations) {
252
- if (relation.from === id || relation.to === id) {
253
- this.relations.delete(relationId);
254
- }
255
- }
256
- }
257
-
258
- /**
259
- * Trouve le chemin entre deux entités (BFS)
260
- */
261
- queryPath(fromId: string, toId: string, maxDepth = 5): Array<{ entity: OntologyEntity; relation?: OntologyRelation }> | null {
262
- this.loadGraph();
263
-
264
- if (!this.entities.has(fromId) || !this.entities.has(toId)) {
265
- return null;
266
- }
267
-
268
- if (fromId === toId) {
269
- return [{ entity: this.entities.get(fromId)! }];
270
- }
271
-
272
- // BFS pour trouver le chemin le plus court
273
- const queue: Array<{ entityId: string; path: Array<{ entity: OntologyEntity; relation?: OntologyRelation }> }> = [
274
- { entityId: fromId, path: [{ entity: this.entities.get(fromId)! }] }
275
- ];
276
-
277
- const visited = new Set<string>([fromId]);
278
-
279
- while (queue.length > 0) {
280
- const { entityId, path } = queue.shift()!;
281
-
282
- if (path.length > maxDepth) {
283
- continue;
284
- }
285
-
286
- // Trouve toutes les relations sortantes
287
- for (const relation of this.relations.values()) {
288
- if (relation.from === entityId) {
289
- const targetId = relation.to;
290
-
291
- if (targetId === toId) {
292
- // Found!
293
- return [...path, { entity: this.entities.get(targetId)!, relation }];
294
- }
295
-
296
- if (!visited.has(targetId)) {
297
- visited.add(targetId);
298
- queue.push({
299
- entityId: targetId,
300
- path: [...path, { entity: this.entities.get(targetId)!, relation }]
301
- });
302
- }
303
- }
304
-
305
- // Relations bidirectionnelles (from <-> to)
306
- if (relation.to === entityId) {
307
- const targetId = relation.from;
308
-
309
- if (targetId === toId) {
310
- // Found!
311
- return [...path, { entity: this.entities.get(targetId)!, relation }];
312
- }
313
-
314
- if (!visited.has(targetId)) {
315
- visited.add(targetId);
316
- queue.push({
317
- entityId: targetId,
318
- path: [...path, { entity: this.entities.get(targetId)!, relation }]
319
- });
320
- }
321
- }
322
- }
323
- }
324
-
325
- return null; // No path found
326
- }
327
-
328
- /**
329
- * Exporte tout le graphe en JSON lisible
330
- */
331
- export(): { entities: OntologyEntity[]; relations: OntologyRelation[] } {
332
- return this.getGraph();
333
- }
334
-
335
- /**
336
- * Statistiques : nombre d'entités par type, nombre de relations par type
337
- */
338
- stats(): { entitiesByType: Record<string, number>; relationsByType: Record<string, number> } {
339
- this.loadGraph();
340
-
341
- const entitiesByType: Record<string, number> = {};
342
- const relationsByType: Record<string, number> = {};
343
-
344
- for (const entity of this.entities.values()) {
345
- entitiesByType[entity.type] = (entitiesByType[entity.type] || 0) + 1;
346
- }
347
-
348
- for (const relation of this.relations.values()) {
349
- relationsByType[relation.type] = (relationsByType[relation.type] || 0) + 1;
350
- }
351
-
352
- return { entitiesByType, relationsByType };
353
- }
354
- }
15
+ private config: MemoryConfig;
16
+ private graphPath: string;
17
+ private entities: Map<string, OntologyEntity> = new Map();
18
+ private relations: Map<string, OntologyRelation> = new Map();
19
+ private loaded = false;
20
+
21
+ constructor(config: MemoryConfig) {
22
+ this.config = config;
23
+ this.graphPath = config.ontologyPath;
24
+ this.ensureDirectories();
25
+ }
26
+
27
+ private ensureDirectories(): void {
28
+ const dir = dirname(this.graphPath);
29
+ if (!existsSync(dir)) {
30
+ mkdirSync(dir, { recursive: true });
31
+ }
32
+ }
33
+
34
+ private generateId(): string {
35
+ return randomBytes(16).toString("hex");
36
+ }
37
+
38
+ private loadGraph(): void {
39
+ if (this.loaded) return;
40
+
41
+ this.entities.clear();
42
+ this.relations.clear();
43
+
44
+ if (!existsSync(this.graphPath)) {
45
+ this.loaded = true;
46
+ return;
47
+ }
48
+
49
+ const content = readFileSync(this.graphPath, "utf8");
50
+ const lines = content
51
+ .trim()
52
+ .split("\n")
53
+ .filter((line) => line.trim());
54
+
55
+ for (const line of lines) {
56
+ try {
57
+ const entry: OntologyJSONLEntry = JSON.parse(line);
58
+
59
+ switch (entry.kind) {
60
+ case "entity":
61
+ this.entities.set(entry.id, {
62
+ id: entry.id,
63
+ type: entry.type,
64
+ name: entry.name,
65
+ properties: entry.properties,
66
+ createdAt: entry.createdAt,
67
+ updatedAt: entry.updatedAt,
68
+ });
69
+ break;
70
+
71
+ case "relation":
72
+ this.relations.set(entry.id, {
73
+ id: entry.id,
74
+ from: entry.from,
75
+ to: entry.to,
76
+ type: entry.type,
77
+ properties: entry.properties,
78
+ createdAt: entry.createdAt,
79
+ });
80
+ break;
81
+
82
+ case "delete":
83
+ // Delete entity or relation
84
+ this.entities.delete(entry.targetId);
85
+ this.relations.delete(entry.targetId);
86
+ // Also delete all relations linked to this entity
87
+ for (const [relationId, relation] of this.relations) {
88
+ if (relation.from === entry.targetId || relation.to === entry.targetId) {
89
+ this.relations.delete(relationId);
90
+ }
91
+ }
92
+ break;
93
+ }
94
+ } catch (error) {
95
+ // Skip malformed JSONL line
96
+ }
97
+ }
98
+
99
+ this.loaded = true;
100
+ }
101
+
102
+ private appendToFile(entry: OntologyJSONLEntry): void {
103
+ this.ensureDirectories();
104
+ const line = JSON.stringify(entry) + "\n";
105
+ appendFileSync(this.graphPath, line, "utf8");
106
+ }
107
+
108
+ /**
109
+ * Ajoute une entité
110
+ */
111
+ addEntity(entity: Omit<OntologyEntity, "id" | "createdAt" | "updatedAt">): string {
112
+ this.loadGraph();
113
+
114
+ const id = this.generateId();
115
+ const now = new Date().toISOString();
116
+
117
+ const newEntity: OntologyEntity = {
118
+ ...entity,
119
+ id,
120
+ createdAt: now,
121
+ updatedAt: now,
122
+ };
123
+
124
+ this.entities.set(id, newEntity);
125
+
126
+ const entry: OntologyEntityEntry = {
127
+ kind: "entity",
128
+ ...newEntity,
129
+ };
130
+
131
+ this.appendToFile(entry);
132
+ return id;
133
+ }
134
+
135
+ /**
136
+ * Ajoute une relation
137
+ */
138
+ addRelation(relation: Omit<OntologyRelation, "id" | "createdAt">): string {
139
+ this.loadGraph();
140
+
141
+ // Verify source and destination entities exist
142
+ if (!this.entities.has(relation.from)) {
143
+ throw new Error(`Source entity not found: ${relation.from}`);
144
+ }
145
+ if (!this.entities.has(relation.to)) {
146
+ throw new Error(`Target entity not found: ${relation.to}`);
147
+ }
148
+
149
+ const id = this.generateId();
150
+ const now = new Date().toISOString();
151
+
152
+ const newRelation: OntologyRelation = {
153
+ ...relation,
154
+ id,
155
+ createdAt: now,
156
+ };
157
+
158
+ this.relations.set(id, newRelation);
159
+
160
+ const entry: OntologyRelationEntry = {
161
+ kind: "relation",
162
+ ...newRelation,
163
+ };
164
+
165
+ this.appendToFile(entry);
166
+ return id;
167
+ }
168
+
169
+ /**
170
+ * Recherche par id/type/nom
171
+ */
172
+ findEntity(query: { id?: string; type?: string; name?: string }): OntologyEntity[] {
173
+ this.loadGraph();
174
+
175
+ // Direct ID lookup - return exact match
176
+ if (query.id) {
177
+ const entity = this.entities.get(query.id);
178
+ return entity ? [entity] : [];
179
+ }
180
+
181
+ const results: OntologyEntity[] = [];
182
+
183
+ for (const entity of this.entities.values()) {
184
+ let matches = true;
185
+
186
+ if (query.type && entity.type !== query.type) {
187
+ matches = false;
188
+ }
189
+
190
+ if (query.name && !entity.name.toLowerCase().includes(query.name.toLowerCase())) {
191
+ matches = false;
192
+ }
193
+
194
+ if (matches) {
195
+ results.push(entity);
196
+ }
197
+ }
198
+
199
+ return results;
200
+ }
201
+
202
+ /**
203
+ * Toutes les relations d'une entité
204
+ */
205
+ findRelations(entityId: string): OntologyRelation[] {
206
+ this.loadGraph();
207
+
208
+ const results: OntologyRelation[] = [];
209
+
210
+ for (const relation of this.relations.values()) {
211
+ if (relation.from === entityId || relation.to === entityId) {
212
+ results.push(relation);
213
+ }
214
+ }
215
+
216
+ return results;
217
+ }
218
+
219
+ /**
220
+ * Retourne le graphe complet
221
+ */
222
+ getGraph(): { entities: OntologyEntity[]; relations: OntologyRelation[] } {
223
+ this.loadGraph();
224
+
225
+ return {
226
+ entities: Array.from(this.entities.values()),
227
+ relations: Array.from(this.relations.values()),
228
+ };
229
+ }
230
+
231
+ /**
232
+ * Supprime entité + ses relations
233
+ */
234
+ removeEntity(id: string): void {
235
+ this.loadGraph();
236
+
237
+ if (!this.entities.has(id)) {
238
+ throw new Error(`Entity not found: ${id}`);
239
+ }
240
+
241
+ // Mark as deleted in file
242
+ const deleteEntry: OntologyDeleteEntry = {
243
+ kind: "delete",
244
+ targetId: id,
245
+ deletedAt: new Date().toISOString(),
246
+ };
247
+
248
+ this.appendToFile(deleteEntry);
249
+
250
+ // Remove from memory
251
+ this.entities.delete(id);
252
+
253
+ // Remove all linked relations
254
+ for (const [relationId, relation] of this.relations) {
255
+ if (relation.from === id || relation.to === id) {
256
+ this.relations.delete(relationId);
257
+ }
258
+ }
259
+ }
260
+
261
+ /**
262
+ * Trouve le chemin entre deux entités (BFS)
263
+ */
264
+ queryPath(
265
+ fromId: string,
266
+ toId: string,
267
+ maxDepth = 5,
268
+ ): Array<{ entity: OntologyEntity; relation?: OntologyRelation }> | null {
269
+ this.loadGraph();
270
+
271
+ if (!this.entities.has(fromId) || !this.entities.has(toId)) {
272
+ return null;
273
+ }
274
+
275
+ if (fromId === toId) {
276
+ return [{ entity: this.entities.get(fromId)! }];
277
+ }
278
+
279
+ // BFS pour trouver le chemin le plus court
280
+ const queue: Array<{ entityId: string; path: Array<{ entity: OntologyEntity; relation?: OntologyRelation }> }> = [
281
+ { entityId: fromId, path: [{ entity: this.entities.get(fromId)! }] },
282
+ ];
283
+
284
+ const visited = new Set<string>([fromId]);
285
+
286
+ while (queue.length > 0) {
287
+ const { entityId, path } = queue.shift()!;
288
+
289
+ if (path.length > maxDepth) {
290
+ continue;
291
+ }
292
+
293
+ // Trouve toutes les relations sortantes
294
+ for (const relation of this.relations.values()) {
295
+ if (relation.from === entityId) {
296
+ const targetId = relation.to;
297
+
298
+ if (targetId === toId) {
299
+ // Found!
300
+ return [...path, { entity: this.entities.get(targetId)!, relation }];
301
+ }
302
+
303
+ if (!visited.has(targetId)) {
304
+ visited.add(targetId);
305
+ queue.push({
306
+ entityId: targetId,
307
+ path: [...path, { entity: this.entities.get(targetId)!, relation }],
308
+ });
309
+ }
310
+ }
311
+
312
+ // Relations bidirectionnelles (from <-> to)
313
+ if (relation.to === entityId) {
314
+ const targetId = relation.from;
315
+
316
+ if (targetId === toId) {
317
+ // Found!
318
+ return [...path, { entity: this.entities.get(targetId)!, relation }];
319
+ }
320
+
321
+ if (!visited.has(targetId)) {
322
+ visited.add(targetId);
323
+ queue.push({
324
+ entityId: targetId,
325
+ path: [...path, { entity: this.entities.get(targetId)!, relation }],
326
+ });
327
+ }
328
+ }
329
+ }
330
+ }
331
+
332
+ return null; // No path found
333
+ }
334
+
335
+ /**
336
+ * Exporte tout le graphe en JSON lisible
337
+ */
338
+ export(): { entities: OntologyEntity[]; relations: OntologyRelation[] } {
339
+ return this.getGraph();
340
+ }
341
+
342
+ /**
343
+ * Statistiques : nombre d'entités par type, nombre de relations par type
344
+ */
345
+ stats(): { entitiesByType: Record<string, number>; relationsByType: Record<string, number> } {
346
+ this.loadGraph();
347
+
348
+ const entitiesByType: Record<string, number> = {};
349
+ const relationsByType: Record<string, number> = {};
350
+
351
+ for (const entity of this.entities.values()) {
352
+ entitiesByType[entity.type] = (entitiesByType[entity.type] || 0) + 1;
353
+ }
354
+
355
+ for (const relation of this.relations.values()) {
356
+ relationsByType[relation.type] = (relationsByType[relation.type] || 0) + 1;
357
+ }
358
+
359
+ return { entitiesByType, relationsByType };
360
+ }
361
+ }