sigma-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/dist/index.d.ts +33 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +182 -0
- package/dist/index.js.map +1 -0
- package/dist/notes.d.ts +40 -0
- package/dist/notes.d.ts.map +1 -0
- package/dist/notes.js +147 -0
- package/dist/notes.js.map +1 -0
- package/dist/ontology.d.ts +65 -0
- package/dist/ontology.d.ts.map +1 -0
- package/dist/ontology.js +280 -0
- package/dist/ontology.js.map +1 -0
- package/dist/qmd.d.ts +35 -0
- package/dist/qmd.d.ts.map +1 -0
- package/dist/qmd.js +162 -0
- package/dist/qmd.js.map +1 -0
- package/dist/types.d.ts +86 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +24 -0
- package/src/index.ts +178 -0
- package/src/notes.ts +163 -0
- package/src/ontology.ts +348 -0
- package/src/qmd.ts +180 -0
- package/src/types.ts +96 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { join } from 'path';
|
|
2
|
+
import { homedir } from 'os';
|
|
3
|
+
import { existsSync, mkdirSync } from 'fs';
|
|
4
|
+
import { NotesManager } from './notes.js';
|
|
5
|
+
import { OntologyManager } from './ontology.js';
|
|
6
|
+
import { QMDManager } from './qmd.js';
|
|
7
|
+
import type { MemoryConfig, UnifiedSearchResult, MemoryStatus } from './types.js';
|
|
8
|
+
|
|
9
|
+
export class SigmaMemory {
|
|
10
|
+
public readonly notes: NotesManager;
|
|
11
|
+
public readonly ontology: OntologyManager;
|
|
12
|
+
public readonly qmd: QMDManager;
|
|
13
|
+
private readonly config: MemoryConfig;
|
|
14
|
+
|
|
15
|
+
constructor(config?: Partial<MemoryConfig>) {
|
|
16
|
+
// Configuration par défaut
|
|
17
|
+
const defaultConfig: MemoryConfig = {
|
|
18
|
+
memoryDir: join(homedir(), '.phi', 'memory'),
|
|
19
|
+
projectMemoryDir: join(process.cwd(), '.phi', 'memory'),
|
|
20
|
+
ontologyPath: join(homedir(), '.phi', 'memory', 'ontology', 'graph.jsonl'),
|
|
21
|
+
qmdEnabled: true,
|
|
22
|
+
qmdCommand: 'qmd'
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
this.config = { ...defaultConfig, ...config };
|
|
26
|
+
|
|
27
|
+
// Initialise les managers
|
|
28
|
+
this.notes = new NotesManager(this.config);
|
|
29
|
+
this.ontology = new OntologyManager(this.config);
|
|
30
|
+
this.qmd = new QMDManager(this.config);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Recherche unifiée : cherche dans notes + ontology + QMD, combine les résultats
|
|
35
|
+
*/
|
|
36
|
+
async search(query: string): Promise<UnifiedSearchResult[]> {
|
|
37
|
+
const results: UnifiedSearchResult[] = [];
|
|
38
|
+
|
|
39
|
+
// Recherche dans les notes
|
|
40
|
+
try {
|
|
41
|
+
const notesResults = this.notes.search(query);
|
|
42
|
+
for (const result of notesResults) {
|
|
43
|
+
results.push({
|
|
44
|
+
source: 'notes',
|
|
45
|
+
type: 'note',
|
|
46
|
+
score: 0.8, // Score par défaut pour les notes
|
|
47
|
+
data: result
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.error('Notes search error:', error);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Recherche dans l'ontologie
|
|
55
|
+
try {
|
|
56
|
+
const entityResults = this.ontology.findEntity({ name: query });
|
|
57
|
+
for (const entity of entityResults) {
|
|
58
|
+
results.push({
|
|
59
|
+
source: 'ontology',
|
|
60
|
+
type: 'entity',
|
|
61
|
+
score: 0.9, // Score élevé pour les entités
|
|
62
|
+
data: entity
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Inclut aussi les relations de cette entité
|
|
66
|
+
const relations = this.ontology.findRelations(entity.id);
|
|
67
|
+
for (const relation of relations) {
|
|
68
|
+
results.push({
|
|
69
|
+
source: 'ontology',
|
|
70
|
+
type: 'relation',
|
|
71
|
+
score: 0.7,
|
|
72
|
+
data: relation
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.error('Ontology search error:', error);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Recherche QMD (vectorielle)
|
|
81
|
+
try {
|
|
82
|
+
const qmdResults = await this.qmd.search(query, 5);
|
|
83
|
+
for (const result of qmdResults) {
|
|
84
|
+
results.push({
|
|
85
|
+
source: 'qmd',
|
|
86
|
+
type: 'file',
|
|
87
|
+
score: result.score,
|
|
88
|
+
data: result
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
} catch (error) {
|
|
92
|
+
console.error('QMD search error:', error);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Trie par score décroissant
|
|
96
|
+
results.sort((a, b) => b.score - a.score);
|
|
97
|
+
|
|
98
|
+
return results;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Initialise tous les dossiers nécessaires
|
|
103
|
+
*/
|
|
104
|
+
async init(): Promise<void> {
|
|
105
|
+
// Crée les dossiers de base
|
|
106
|
+
if (!existsSync(this.config.memoryDir)) {
|
|
107
|
+
mkdirSync(this.config.memoryDir, { recursive: true });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!existsSync(this.config.projectMemoryDir)) {
|
|
111
|
+
mkdirSync(this.config.projectMemoryDir, { recursive: true });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Initialise QMD si activé
|
|
115
|
+
if (this.config.qmdEnabled && this.qmd.isAvailable()) {
|
|
116
|
+
try {
|
|
117
|
+
await this.qmd.update();
|
|
118
|
+
} catch (error) {
|
|
119
|
+
console.error('QMD initialization error:', error);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Status de tous les sous-systèmes
|
|
126
|
+
*/
|
|
127
|
+
async status(): Promise<MemoryStatus> {
|
|
128
|
+
// Status des notes
|
|
129
|
+
const notesList = this.notes.list();
|
|
130
|
+
const notesStatus = {
|
|
131
|
+
count: notesList.length,
|
|
132
|
+
totalSize: notesList.reduce((sum, note) => sum + note.size, 0),
|
|
133
|
+
lastModified: notesList.length > 0 ? notesList[0].date : null
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// Status de l'ontologie
|
|
137
|
+
const ontologyStats = this.ontology.stats();
|
|
138
|
+
const ontologyGraph = this.ontology.getGraph();
|
|
139
|
+
const ontologyStatus = {
|
|
140
|
+
entities: ontologyGraph.entities.length,
|
|
141
|
+
relations: ontologyGraph.relations.length,
|
|
142
|
+
entitiesByType: ontologyStats.entitiesByType,
|
|
143
|
+
relationsByType: ontologyStats.relationsByType
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// Status QMD
|
|
147
|
+
let qmdStatus: MemoryStatus['qmd'] = { available: false };
|
|
148
|
+
if (this.config.qmdEnabled && this.qmd.isAvailable()) {
|
|
149
|
+
const status = await this.qmd.status();
|
|
150
|
+
qmdStatus = {
|
|
151
|
+
available: true,
|
|
152
|
+
status: status || { files: 0, chunks: 0, lastUpdate: null }
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
notes: notesStatus,
|
|
158
|
+
ontology: ontologyStatus,
|
|
159
|
+
qmd: qmdStatus
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Configuration actuelle
|
|
165
|
+
*/
|
|
166
|
+
getConfig(): MemoryConfig {
|
|
167
|
+
return { ...this.config };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Exports pour une utilisation facile
|
|
172
|
+
export { NotesManager } from './notes.js';
|
|
173
|
+
export { OntologyManager } from './ontology.js';
|
|
174
|
+
export { QMDManager } from './qmd.js';
|
|
175
|
+
export * from './types.js';
|
|
176
|
+
|
|
177
|
+
// Export par défaut
|
|
178
|
+
export default SigmaMemory;
|
package/src/notes.ts
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, readdirSync, statSync, existsSync, mkdirSync, appendFileSync } from 'fs';
|
|
2
|
+
import { join, resolve } from 'path';
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
import type { MemoryConfig, Note } from './types.js';
|
|
5
|
+
|
|
6
|
+
export class NotesManager {
|
|
7
|
+
private config: MemoryConfig;
|
|
8
|
+
private notesDir: string;
|
|
9
|
+
|
|
10
|
+
constructor(config: MemoryConfig) {
|
|
11
|
+
this.config = config;
|
|
12
|
+
this.notesDir = join(config.memoryDir, 'notes');
|
|
13
|
+
this.ensureDirectories();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
private ensureDirectories(): void {
|
|
17
|
+
if (!existsSync(this.config.memoryDir)) {
|
|
18
|
+
mkdirSync(this.config.memoryDir, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
if (!existsSync(this.notesDir)) {
|
|
21
|
+
mkdirSync(this.notesDir, { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Écrit dans un fichier .md (date du jour si pas de nom)
|
|
27
|
+
*/
|
|
28
|
+
write(content: string, filename?: string): void {
|
|
29
|
+
const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
|
|
30
|
+
const file = filename || `${today}.md`;
|
|
31
|
+
const filePath = join(this.notesDir, file);
|
|
32
|
+
|
|
33
|
+
writeFileSync(filePath, content, 'utf8');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Lit un fichier
|
|
38
|
+
*/
|
|
39
|
+
read(filename: string): string {
|
|
40
|
+
const filePath = join(this.notesDir, filename);
|
|
41
|
+
if (!existsSync(filePath)) {
|
|
42
|
+
throw new Error(`File not found: ${filename}`);
|
|
43
|
+
}
|
|
44
|
+
return readFileSync(filePath, 'utf8');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Liste tous les fichiers .md avec leur taille et date
|
|
49
|
+
*/
|
|
50
|
+
list(): Array<{ name: string; size: number; date: string }> {
|
|
51
|
+
if (!existsSync(this.notesDir)) {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return readdirSync(this.notesDir)
|
|
56
|
+
.filter(file => file.endsWith('.md'))
|
|
57
|
+
.map(file => {
|
|
58
|
+
const filePath = join(this.notesDir, file);
|
|
59
|
+
const stats = statSync(filePath);
|
|
60
|
+
return {
|
|
61
|
+
name: file,
|
|
62
|
+
size: stats.size,
|
|
63
|
+
date: stats.mtime.toISOString()
|
|
64
|
+
};
|
|
65
|
+
})
|
|
66
|
+
.sort((a, b) => b.date.localeCompare(a.date));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Recherche full-text (grep-like) dans tous les .md
|
|
71
|
+
*/
|
|
72
|
+
search(query: string): Array<{ file: string; line: number; content: string }> {
|
|
73
|
+
if (!existsSync(this.notesDir)) {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const results: Array<{ file: string; line: number; content: string }> = [];
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
// Utilise grep pour une recherche efficace
|
|
81
|
+
const grepResult = execSync(
|
|
82
|
+
`grep -n "${query.replace(/"/g, '\\"')}" "${this.notesDir}"/*.md 2>/dev/null || true`,
|
|
83
|
+
{ encoding: 'utf8' }
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
if (grepResult.trim()) {
|
|
87
|
+
const lines = grepResult.trim().split('\n');
|
|
88
|
+
for (const line of lines) {
|
|
89
|
+
const match = line.match(/^(.+?):(\d+):(.+)$/);
|
|
90
|
+
if (match) {
|
|
91
|
+
const [, fullPath, lineNumber, content] = match;
|
|
92
|
+
const filename = fullPath.replace(this.notesDir + '/', '');
|
|
93
|
+
results.push({
|
|
94
|
+
file: filename,
|
|
95
|
+
line: parseInt(lineNumber),
|
|
96
|
+
content: content.trim()
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
} catch (error) {
|
|
102
|
+
// Fallback à une recherche en JavaScript si grep échoue
|
|
103
|
+
const files = readdirSync(this.notesDir).filter(f => f.endsWith('.md'));
|
|
104
|
+
for (const file of files) {
|
|
105
|
+
const filePath = join(this.notesDir, file);
|
|
106
|
+
const content = readFileSync(filePath, 'utf8');
|
|
107
|
+
const lines = content.split('\n');
|
|
108
|
+
|
|
109
|
+
lines.forEach((line, index) => {
|
|
110
|
+
if (line.toLowerCase().includes(query.toLowerCase())) {
|
|
111
|
+
results.push({
|
|
112
|
+
file,
|
|
113
|
+
line: index + 1,
|
|
114
|
+
content: line.trim()
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return results;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Retourne les notes des N derniers jours
|
|
126
|
+
*/
|
|
127
|
+
getRecent(days: number): Note[] {
|
|
128
|
+
const cutoffDate = new Date();
|
|
129
|
+
cutoffDate.setDate(cutoffDate.getDate() - days);
|
|
130
|
+
|
|
131
|
+
const files = this.list().filter(file => {
|
|
132
|
+
const fileDate = new Date(file.date);
|
|
133
|
+
return fileDate >= cutoffDate;
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
return files.map(file => {
|
|
137
|
+
const content = this.read(file.name);
|
|
138
|
+
return {
|
|
139
|
+
file: file.name,
|
|
140
|
+
date: file.date,
|
|
141
|
+
content
|
|
142
|
+
};
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Ajoute à un fichier existant
|
|
148
|
+
*/
|
|
149
|
+
append(content: string, filename?: string): void {
|
|
150
|
+
const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
|
|
151
|
+
const file = filename || `${today}.md`;
|
|
152
|
+
const filePath = join(this.notesDir, file);
|
|
153
|
+
|
|
154
|
+
// Ajoute une ligne vide si le fichier existe déjà et ne se termine pas par une ligne vide
|
|
155
|
+
if (existsSync(filePath)) {
|
|
156
|
+
const existingContent = readFileSync(filePath, 'utf8');
|
|
157
|
+
const separator = existingContent.endsWith('\n') ? '' : '\n';
|
|
158
|
+
appendFileSync(filePath, separator + content, 'utf8');
|
|
159
|
+
} else {
|
|
160
|
+
writeFileSync(filePath, content, 'utf8');
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
package/src/ontology.ts
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
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';
|
|
13
|
+
|
|
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
|
+
// Supprime l'entité ou la relation
|
|
81
|
+
this.entities.delete(entry.targetId);
|
|
82
|
+
this.relations.delete(entry.targetId);
|
|
83
|
+
// Supprime aussi toutes les relations liées à cette entité
|
|
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
|
+
console.error(`Error parsing JSONL line: ${line}`, error);
|
|
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
|
+
// Vérifie que les entités source et destination existent
|
|
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 type/nom
|
|
168
|
+
*/
|
|
169
|
+
findEntity(query: { type?: string; name?: string }): OntologyEntity[] {
|
|
170
|
+
this.loadGraph();
|
|
171
|
+
|
|
172
|
+
const results: OntologyEntity[] = [];
|
|
173
|
+
|
|
174
|
+
for (const entity of this.entities.values()) {
|
|
175
|
+
let matches = true;
|
|
176
|
+
|
|
177
|
+
if (query.type && entity.type !== query.type) {
|
|
178
|
+
matches = false;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (query.name && !entity.name.toLowerCase().includes(query.name.toLowerCase())) {
|
|
182
|
+
matches = false;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (matches) {
|
|
186
|
+
results.push(entity);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return results;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Toutes les relations d'une entité
|
|
195
|
+
*/
|
|
196
|
+
findRelations(entityId: string): OntologyRelation[] {
|
|
197
|
+
this.loadGraph();
|
|
198
|
+
|
|
199
|
+
const results: OntologyRelation[] = [];
|
|
200
|
+
|
|
201
|
+
for (const relation of this.relations.values()) {
|
|
202
|
+
if (relation.from === entityId || relation.to === entityId) {
|
|
203
|
+
results.push(relation);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return results;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Retourne le graphe complet
|
|
212
|
+
*/
|
|
213
|
+
getGraph(): { entities: OntologyEntity[]; relations: OntologyRelation[] } {
|
|
214
|
+
this.loadGraph();
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
entities: Array.from(this.entities.values()),
|
|
218
|
+
relations: Array.from(this.relations.values())
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Supprime entité + ses relations
|
|
224
|
+
*/
|
|
225
|
+
removeEntity(id: string): void {
|
|
226
|
+
this.loadGraph();
|
|
227
|
+
|
|
228
|
+
if (!this.entities.has(id)) {
|
|
229
|
+
throw new Error(`Entity not found: ${id}`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Marque comme supprimé dans le fichier
|
|
233
|
+
const deleteEntry: OntologyDeleteEntry = {
|
|
234
|
+
kind: 'delete',
|
|
235
|
+
targetId: id,
|
|
236
|
+
deletedAt: new Date().toISOString()
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
this.appendToFile(deleteEntry);
|
|
240
|
+
|
|
241
|
+
// Supprime de la mémoire
|
|
242
|
+
this.entities.delete(id);
|
|
243
|
+
|
|
244
|
+
// Supprime toutes les relations liées
|
|
245
|
+
for (const [relationId, relation] of this.relations) {
|
|
246
|
+
if (relation.from === id || relation.to === id) {
|
|
247
|
+
this.relations.delete(relationId);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Trouve le chemin entre deux entités (BFS)
|
|
254
|
+
*/
|
|
255
|
+
queryPath(fromId: string, toId: string, maxDepth = 5): Array<{ entity: OntologyEntity; relation?: OntologyRelation }> | null {
|
|
256
|
+
this.loadGraph();
|
|
257
|
+
|
|
258
|
+
if (!this.entities.has(fromId) || !this.entities.has(toId)) {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (fromId === toId) {
|
|
263
|
+
return [{ entity: this.entities.get(fromId)! }];
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// BFS pour trouver le chemin le plus court
|
|
267
|
+
const queue: Array<{ entityId: string; path: Array<{ entity: OntologyEntity; relation?: OntologyRelation }> }> = [
|
|
268
|
+
{ entityId: fromId, path: [{ entity: this.entities.get(fromId)! }] }
|
|
269
|
+
];
|
|
270
|
+
|
|
271
|
+
const visited = new Set<string>([fromId]);
|
|
272
|
+
|
|
273
|
+
while (queue.length > 0) {
|
|
274
|
+
const { entityId, path } = queue.shift()!;
|
|
275
|
+
|
|
276
|
+
if (path.length > maxDepth) {
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Trouve toutes les relations sortantes
|
|
281
|
+
for (const relation of this.relations.values()) {
|
|
282
|
+
if (relation.from === entityId) {
|
|
283
|
+
const targetId = relation.to;
|
|
284
|
+
|
|
285
|
+
if (targetId === toId) {
|
|
286
|
+
// Trouvé !
|
|
287
|
+
return [...path, { entity: this.entities.get(targetId)!, relation }];
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (!visited.has(targetId)) {
|
|
291
|
+
visited.add(targetId);
|
|
292
|
+
queue.push({
|
|
293
|
+
entityId: targetId,
|
|
294
|
+
path: [...path, { entity: this.entities.get(targetId)!, relation }]
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Relations bidirectionnelles (from <-> to)
|
|
300
|
+
if (relation.to === entityId) {
|
|
301
|
+
const targetId = relation.from;
|
|
302
|
+
|
|
303
|
+
if (targetId === toId) {
|
|
304
|
+
// Trouvé !
|
|
305
|
+
return [...path, { entity: this.entities.get(targetId)!, relation }];
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (!visited.has(targetId)) {
|
|
309
|
+
visited.add(targetId);
|
|
310
|
+
queue.push({
|
|
311
|
+
entityId: targetId,
|
|
312
|
+
path: [...path, { entity: this.entities.get(targetId)!, relation }]
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return null; // Aucun chemin trouvé
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Exporte tout le graphe en JSON lisible
|
|
324
|
+
*/
|
|
325
|
+
export(): { entities: OntologyEntity[]; relations: OntologyRelation[] } {
|
|
326
|
+
return this.getGraph();
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Statistiques : nombre d'entités par type, nombre de relations par type
|
|
331
|
+
*/
|
|
332
|
+
stats(): { entitiesByType: Record<string, number>; relationsByType: Record<string, number> } {
|
|
333
|
+
this.loadGraph();
|
|
334
|
+
|
|
335
|
+
const entitiesByType: Record<string, number> = {};
|
|
336
|
+
const relationsByType: Record<string, number> = {};
|
|
337
|
+
|
|
338
|
+
for (const entity of this.entities.values()) {
|
|
339
|
+
entitiesByType[entity.type] = (entitiesByType[entity.type] || 0) + 1;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
for (const relation of this.relations.values()) {
|
|
343
|
+
relationsByType[relation.type] = (relationsByType[relation.type] || 0) + 1;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return { entitiesByType, relationsByType };
|
|
347
|
+
}
|
|
348
|
+
}
|