// src/rag/phase0/rag-initialization.ts
// Module A : Initialisation de l'infrastructure RAG (8 étapes atomiques)
// Responsabilités : A1-A8 - Logique métier pure, zéro MCP
import { promises as fs } from 'fs';
import path from 'path';
import { computeProjectId, isValidProjectPath } from './rag-state.js';
/**
* Mode d'initialisation
*/
export type RagInitializationMode = 'default' | 'memory-only' | 'full';
/**
* Résultat de l'initialisation
*/
export interface RagInitializationResult {
/** Statut de l'initialisation */
status: 'initialized' | 'already_initialized' | 'error';
/** Chemin vers la racine du projet */
projectPath: string;
/** Chemin vers le dossier /rag/ */
ragPath: string;
/** Hash du projet */
projectId: string;
/** Mode d'initialisation utilisé */
mode: RagInitializationMode;
/** Date d'initialisation */
initializedAt: string;
/** Détails de l'initialisation */
details: {
/** Étape A1 : Résolution du projet */
stepA1: { success: boolean; error?: string };
/** Étape A2 : Création de l'arborescence */
stepA2: { success: boolean; error?: string; directoriesCreated: string[] };
/** Étape A3 : Création .ragignore */
stepA3: { success: boolean; error?: string; fileCreated: boolean };
/** Étape A4 : Génération config RAG */
stepA4: { success: boolean; error?: string; configPath: string };
/** Étape A5 : Génération config DB */
stepA5: { success: boolean; error?: string; configPath: string };
/** Étape A6 : Initialisation SQLite */
stepA6: { success: boolean; error?: string; dbPath: string };
/** Étape A7 : Test vector DB */
stepA7: { success: boolean; error?: string; tested: boolean };
/** Étape A8 : Enregistrement MCP */
stepA8: { success: boolean; error?: string; registered: boolean };
};
/** Erreurs globales */
errors?: string[];
/** Avertissements */
warnings?: string[];
}
/**
* Configuration par défaut pour .ragignore
*/
const DEFAULT_RAGIGNORE_CONTENT = `# Fichiers et dossiers à ignorer pour le RAG
node_modules/
.git/
dist/
build/
rag/db
rag/*.log
*.tmp
*.log
.DS_Store
Thumbs.db
`;
/**
* Configuration RAG par défaut
*/
const DEFAULT_RAG_CONFIG = {
rag_initialized: true,
initialized_by: "init_rag",
rag_version: "1.0",
mode: "default",
infrastructure: {
memory_db: "sqlite://rag/db/memory/rag_memory.sqlite",
vector_db: "postgres://localhost:5432/rag_project",
config_version: "1.0"
}
};
/**
* Configuration DB par défaut
*/
const DEFAULT_DB_CONFIG = {
memory: {
type: "sqlite",
path: "./rag/db/memory/rag_memory.sqlite"
},
vectors: {
type: "postgres",
host: "localhost",
port: 5432,
database: "rag_project",
user: "rag",
password: "rag"
}
};
/**
* Type pour la configuration DB
*/
interface DbConfig {
memory: {
type: string;
path: string;
};
vectors: {
type: string;
host?: string;
port?: number;
database?: string;
user?: string;
password?: string;
};
}
/**
* Schéma SQL minimal pour SQLite
*/
const DEFAULT_SQL_SCHEMA = `-- Tables minimales pour la mémoire RAG
CREATE TABLE IF NOT EXISTS files_indexed (
id INTEGER PRIMARY KEY AUTOINCREMENT,
file_path TEXT NOT NULL,
file_hash TEXT NOT NULL,
indexed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_modified TIMESTAMP,
UNIQUE(file_path)
);
CREATE TABLE IF NOT EXISTS chunks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
file_path TEXT NOT NULL,
chunk_index INTEGER NOT NULL,
chunk_hash TEXT NOT NULL,
metadata JSON,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (file_path) REFERENCES files_indexed(file_path),
UNIQUE(file_path, chunk_index)
);
CREATE TABLE IF NOT EXISTS llm_cache (
id INTEGER PRIMARY KEY AUTOINCREMENT,
query_hash TEXT NOT NULL,
response TEXT NOT NULL,
model TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP,
UNIQUE(query_hash)
);
CREATE TABLE IF NOT EXISTS events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
event_type TEXT NOT NULL,
event_data JSON,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS project_state (
id INTEGER PRIMARY KEY AUTOINCREMENT,
key TEXT NOT NULL,
value TEXT NOT NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(key)
);
-- Index pour les performances
CREATE INDEX IF NOT EXISTS idx_files_indexed_path ON files_indexed(file_path);
CREATE INDEX IF NOT EXISTS idx_chunks_file ON chunks(file_path);
CREATE INDEX IF NOT EXISTS idx_llm_cache_hash ON llm_cache(query_hash);
CREATE INDEX IF NOT EXISTS idx_events_type ON events(event_type);
`;
// ========== ÉTAPES ATOMIQUES ==========
/**
* A1 — Résolution du projet
*
* @param projectPath Chemin du projet
* @returns { rootPath, projectIdHash }
*/
async function resolveProjectPath(projectPath: string): Promise<{ rootPath: string; projectIdHash: string }> {
try {
// Vérifier que le chemin existe et est un dossier
if (!await isValidProjectPath(projectPath)) {
throw new Error(`Chemin de projet invalide: ${projectPath}`);
}
// Normaliser le chemin
const rootPath = path.resolve(projectPath);
// Calculer le hash du projet
const projectIdHash = computeProjectId(rootPath);
return { rootPath, projectIdHash };
} catch (error) {
throw new Error(`Échec de la résolution du projet: ${error}`);
}
}
/**
* A2 — Création de l'arborescence
*
* @param rootPath Racine du projet
* @returns Liste des dossiers créés
*/
async function ensureRagDirectories(rootPath: string): Promise<string[]> {
const createdDirs: string[] = [];
// Dossiers à créer (uniquement si absents)
const directories = [
path.join(rootPath, 'rag'),
path.join(rootPath, 'rag', 'db'),
path.join(rootPath, 'rag', 'db', 'memory'),
path.join(rootPath, 'rag', 'db', 'metadata'),
path.join(rootPath, 'rag', 'config')
];
for (const dir of directories) {
try {
await fs.access(dir);
// Dossier existe déjà
} catch {
// Créer le dossier
await fs.mkdir(dir, { recursive: true });
createdDirs.push(dir);
}
}
return createdDirs;
}
/**
* A3 — Création .ragignore
*
* @param rootPath Racine du projet
* @returns true si le fichier a été créé
*/
async function ensureRagIgnore(rootPath: string): Promise<boolean> {
const ragIgnorePath = path.join(rootPath, '.ragignore');
try {
// Vérifier si le fichier existe déjà
await fs.access(ragIgnorePath);
return false; // Fichier existe déjà
} catch {
// Créer le fichier avec le contenu par défaut
await fs.writeFile(ragIgnorePath, DEFAULT_RAGIGNORE_CONTENT, 'utf-8');
return true; // Fichier créé
}
}
/**
* A4 — Génération config RAG
*
* @param rootPath Racine du projet
* @param projectId Hash du projet
* @param mode Mode d'initialisation
* @returns Chemin vers la config
*/
async function ensureRagConfig(
rootPath: string,
projectId: string,
mode: RagInitializationMode
): Promise<string> {
const configPath = path.join(rootPath, 'rag', 'config', 'rag.config.json');
let existingConfig = {};
try {
// Essayer de lire la config existante
const content = await fs.readFile(configPath, 'utf-8');
existingConfig = JSON.parse(content);
} catch {
// Fichier n'existe pas ou erreur de parsing, on part de zéro
}
// Fusionner avec la config par défaut
const config = {
...DEFAULT_RAG_CONFIG,
...existingConfig,
project_id: projectId,
mode,
initialized_at: new Date().toISOString()
};
// Écrire la config
await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf-8');
return configPath;
}
/**
* A5 — Génération config DB
*
* @param rootPath Racine du projet
* @param mode Mode d'initialisation
* @returns Chemin vers la config DB
*/
async function ensureDbConfig(
rootPath: string,
mode: RagInitializationMode
): Promise<string> {
const configPath = path.join(rootPath, 'rag', 'config', 'db.config.json');
let existingConfig = {};
try {
// Essayer de lire la config existante
const content = await fs.readFile(configPath, 'utf-8');
existingConfig = JSON.parse(content);
} catch {
// Fichier n'existe pas ou erreur de parsing
}
// Adapter la config selon le mode
let config: any = { ...DEFAULT_DB_CONFIG, ...existingConfig };
if (mode === 'memory-only') {
config.vectors = { type: 'none' };
}
// Écrire la config
await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf-8');
return configPath;
}
/**
* A6 — Initialisation SQLite
*
* @param sqlitePath Chemin vers le fichier SQLite
*/
async function initMemoryDatabase(sqlitePath: string): Promise<void> {
const sqlite3Module = await import('sqlite3');
const sqlite3 = sqlite3Module.default;
const { open } = await import('sqlite');
// Créer le dossier parent si nécessaire
const dir = path.dirname(sqlitePath);
await fs.mkdir(dir, { recursive: true });
// Ouvrir/creer la base de données
const db = await open({
filename: sqlitePath,
driver: sqlite3.Database
});
try {
// Exécuter le schéma SQL
await db.exec(DEFAULT_SQL_SCHEMA);
// Vérifier que les tables ont été créées
const tables = await db.all(
"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
);
const expectedTables = ['files_indexed', 'chunks', 'llm_cache', 'events', 'project_state'];
const createdTables = tables.map(t => t.name);
for (const expectedTable of expectedTables) {
if (!createdTables.includes(expectedTable)) {
throw new Error(`Table ${expectedTable} non créée`);
}
}
// Insérer l'état initial du projet
await db.run(
"INSERT OR REPLACE INTO project_state (key, value) VALUES (?, ?)",
['rag_initialized', 'true']
);
await db.run(
"INSERT OR REPLACE INTO project_state (key, value) VALUES (?, ?)",
['initialized_at', new Date().toISOString()]
);
} finally {
// Fermer la connexion
await db.close();
}
}
/**
* A7 — Test vector DB (si activée)
*
* @param dbConfig Configuration DB
* @returns true si le test réussit
*/
async function testVectorStoreConnection(dbConfig: any): Promise<boolean> {
// Pour l'instant, on simule simplement le test
// Dans une version future, on testerait réellement la connexion PostgreSQL
if (!dbConfig.vectors || dbConfig.vectors.type === 'none') {
return true; // Pas de DB vectorielle à tester
}
if (dbConfig.vectors.type === 'postgres') {
// Simulation de test de connexion
// Log silencieux pour MCP
return true;
}
// Type de DB non supporté
throw new Error(`Type de base de données vectorielle non supporté: ${dbConfig.vectors.type}`);
}
/**
* A8 — Enregistrement MCP
*
* @param projectInfo Informations du projet
* @returns true si l'enregistrement réussit
*/
async function registerProjectInRegistry(projectInfo: {
projectId: string;
rootPath: string;
ragPath: string;
mode: RagInitializationMode;
}): Promise<boolean> {
// Pour l'instant, on simule l'enregistrement
// Dans une version future, on enregistrerait dans un registry MCP global
// Log silencieux pour MCP
return true;
}
// ========== FONCTION PRINCIPALE ==========
/**
* Initialise l'infrastructure RAG pour un projet (8 étapes atomiques)
*
* @param projectPath Chemin vers le projet
* @param mode Mode d'initialisation
* @returns Résultat détaillé de l'initialisation
*/
export async function initializeRagInfrastructure(
projectPath: string,
mode: RagInitializationMode = 'default'
): Promise<RagInitializationResult> {
const result: RagInitializationResult = {
status: 'error',
projectPath,
ragPath: '',
projectId: '',
mode,
initializedAt: new Date().toISOString(),
details: {
stepA1: { success: false, error: 'Non exécutée' },
stepA2: { success: false, error: 'Non exécutée', directoriesCreated: [] },
stepA3: { success: false, error: 'Non exécutée', fileCreated: false },
stepA4: { success: false, error: 'Non exécutée', configPath: '' },
stepA5: { success: false, error: 'Non exécutée', configPath: '' },
stepA6: { success: false, error: 'Non exécutée', dbPath: '' },
stepA7: { success: false, error: 'Non exécutée', tested: false },
stepA8: { success: false, error: 'Non exécutée', registered: false }
},
errors: [],
warnings: []
};
try {
// ========== A1 — Résolution du projet ==========
try {
const { rootPath, projectIdHash } = await resolveProjectPath(projectPath);
result.projectPath = rootPath;
result.projectId = projectIdHash;
result.ragPath = path.join(rootPath, 'rag');
result.details.stepA1 = { success: true };
} catch (error) {
result.details.stepA1 = { success: false, error: String(error) };
result.errors?.push(`A1 échouée: ${error}`);
return result;
}
// ========== A2 — Création de l'arborescence ==========
try {
const createdDirs = await ensureRagDirectories(result.projectPath);
result.details.stepA2 = { success: true, directoriesCreated: createdDirs };
} catch (error) {
result.details.stepA2 = { success: false, error: String(error), directoriesCreated: [] };
result.errors?.push(`A2 échouée: ${error}`);
return result;
}
// ========== A3 — Création .ragignore ==========
try {
const fileCreated = await ensureRagIgnore(result.projectPath);
result.details.stepA3 = { success: true, fileCreated };
} catch (error) {
result.details.stepA3 = { success: false, error: String(error), fileCreated: false };
result.warnings?.push(`A3 échouée: ${error}`);
}
// ========== A4 — Génération config RAG ==========
try {
const configPath = await ensureRagConfig(result.projectPath, result.projectId, mode);
result.details.stepA4 = { success: true, configPath };
} catch (error) {
result.details.stepA4 = { success: false, error: String(error), configPath: '' };
result.errors?.push(`A4 échouée: ${error}`);
return result;
}
// ========== A5 — Génération config DB ==========
try {
const configPath = await ensureDbConfig(result.projectPath, mode);
result.details.stepA5 = { success: true, configPath };
} catch (error) {
result.details.stepA5 = { success: false, error: String(error), configPath: '' };
result.errors?.push(`A5 échouée: ${error}`);
return result;
}
// ========== A6 — Initialisation SQLite ==========
try {
const dbPath = path.join(result.projectPath, 'rag', 'db', 'memory', 'rag_memory.sqlite');
await initMemoryDatabase(dbPath);
result.details.stepA6 = { success: true, dbPath };
} catch (error) {
result.details.stepA6 = { success: false, error: String(error), dbPath: '' };
result.errors?.push(`A6 échouée: ${error}`);
return result;
}
// ========== A7 — Test vector DB ==========
try {
// Lire la config DB pour le test
const dbConfigPath = path.join(result.projectPath, 'rag', 'config', 'db.config.json');
const dbConfigContent = await fs.readFile(dbConfigPath, 'utf-8');
const dbConfig = JSON.parse(dbConfigContent);
const tested = await testVectorStoreConnection(dbConfig);
result.details.stepA7 = { success: true, tested };
} catch (error) {
result.details.stepA7 = { success: false, error: String(error), tested: false };
result.warnings?.push(`A7 échouée: ${error}`);
}
// ========== A8 — Enregistrement MCP ==========
try {
const registered = await registerProjectInRegistry({
projectId: result.projectId,
rootPath: result.projectPath,
ragPath: result.ragPath,
mode: result.mode
});
result.details.stepA8 = { success: true, registered };
} catch (error) {
result.details.stepA8 = { success: false, error: String(error), registered: false };
result.warnings?.push(`A8 échouée: ${error}`);
}
// ========== FINALISATION ==========
// Si on arrive ici, toutes les étapes critiques ont réussi
result.status = 'initialized';
return result;
} catch (error) {
// Erreur globale non capturée
result.errors?.push(`Erreur globale: ${error}`);
return result;
}
}