@versatil/sdlc-framework
Version:
🚀 AI-Native SDLC framework with 11-MCP ecosystem, RAG memory, OPERA orchestration, and 6 specialized agents achieving ZERO CONTEXT LOSS. Features complete CI/CD pipeline with 7 GitHub workflows (MCP testing, security scanning, performance benchmarking),
790 lines (663 loc) • 24.1 kB
text/typescript
/**
* Multi-Project Manager with Intelligent Isolation
* Manages separate contexts, RAG systems, and documentation for different projects
*
* Features:
* - Project-specific RAG knowledge bases
* - Isolated agent configurations per project
* - Cross-project learning without contamination
* - Project-specific documentation systems
* - Intelligent context switching
* - Shared global learnings (optional)
*/
import { EventEmitter } from 'events';
import { promises as fs } from 'fs';
import { join, resolve, relative } from 'path';
import { createHash } from 'crypto';
export interface ProjectContext {
id: string;
path: string;
name: string;
type: string;
framework: string;
version: string;
signature: string;
metadata: {
createdAt: number;
lastAccessed: number;
accessCount: number;
size: number;
dependencies: string[];
technologies: string[];
patterns: string[];
complexity: 'simple' | 'medium' | 'complex';
};
isolation: {
ragNamespace: string;
agentConfigs: Record<string, any>;
documentationPath: string;
cacheNamespace: string;
learningPath: string;
};
configuration: {
enabledAgents: string[];
preferences: Record<string, any>;
customRules: any[];
qualityGates: any[];
};
}
export interface ProjectIsolationConfig {
strictIsolation: boolean;
allowCrossProjectLearning: boolean;
sharedGlobalKnowledge: boolean;
automaticContextSwitching: boolean;
ragPersistence: boolean;
documentationSeparation: boolean;
cacheIsolation: boolean;
}
export interface RAGNamespace {
projectId: string;
namespace: string;
vectorStore: string;
documentPath: string;
embeddings: any[];
metadata: {
documentCount: number;
lastUpdated: number;
averageChunkSize: number;
topics: string[];
};
}
export interface CrossProjectLearning {
patterns: Map<string, any>;
bestPractices: Map<string, any>;
commonSolutions: Map<string, any>;
antiPatterns: Map<string, any>;
performanceInsights: Map<string, any>;
}
export class MultiProjectManager extends EventEmitter {
private projects: Map<string, ProjectContext> = new Map();
private currentProject: string | null = null;
private ragNamespaces: Map<string, RAGNamespace> = new Map();
private crossProjectLearning: CrossProjectLearning;
private config: ProjectIsolationConfig;
private frameworkBasePath: string;
constructor(config: Partial<ProjectIsolationConfig> = {}) {
super();
this.config = {
strictIsolation: true,
allowCrossProjectLearning: true,
sharedGlobalKnowledge: false,
automaticContextSwitching: true,
ragPersistence: true,
documentationSeparation: true,
cacheIsolation: true,
...config
};
this.frameworkBasePath = join(process.env.HOME || '~', '.versatil-global');
this.crossProjectLearning = this.initializeCrossProjectLearning();
this.initialize();
}
private initializeCrossProjectLearning(): CrossProjectLearning {
return {
patterns: new Map(),
bestPractices: new Map(),
commonSolutions: new Map(),
antiPatterns: new Map(),
performanceInsights: new Map()
};
}
private async initialize(): Promise<void> {
try {
await fs.mkdir(this.frameworkBasePath, { recursive: true });
await this.loadGlobalProjects();
await this.loadCrossProjectLearning();
this.emit('initialized', {
projectCount: this.projects.size,
frameworkPath: this.frameworkBasePath
});
} catch (error) {
this.emit('error', {
phase: 'initialization',
error: error instanceof Error ? error.message : String(error)
});
}
}
async registerProject(projectPath: string): Promise<ProjectContext> {
try {
const absolutePath = resolve(projectPath);
const projectSignature = await this.generateProjectSignature(absolutePath);
// Check if project already exists
const existingProject = this.findProjectByPath(absolutePath);
if (existingProject) {
existingProject.metadata.lastAccessed = Date.now();
existingProject.metadata.accessCount++;
await this.saveProjectContext(existingProject);
return existingProject;
}
const projectContext = await this.createProjectContext(absolutePath, projectSignature);
await this.setupProjectIsolation(projectContext);
this.projects.set(projectContext.id, projectContext);
await this.saveProjectContext(projectContext);
this.emit('project_registered', {
projectId: projectContext.id,
name: projectContext.name,
path: projectContext.path
});
return projectContext;
} catch (error) {
this.emit('error', {
operation: 'registerProject',
projectPath,
error: error instanceof Error ? error.message : String(error)
});
throw error;
}
}
async switchToProject(projectPath: string): Promise<ProjectContext> {
try {
const absolutePath = resolve(projectPath);
let project = this.findProjectByPath(absolutePath);
if (!project) {
project = await this.registerProject(absolutePath);
}
// Switch context
this.currentProject = project.id;
project.metadata.lastAccessed = Date.now();
project.metadata.accessCount++;
// Load project-specific configurations
await this.loadProjectConfigurations(project);
await this.activateProjectRAG(project);
await this.switchAgentConfigurations(project);
this.emit('project_switched', {
projectId: project.id,
name: project.name,
path: project.path,
ragNamespace: project.isolation.ragNamespace
});
return project;
} catch (error) {
this.emit('error', {
operation: 'switchToProject',
projectPath,
error: error instanceof Error ? error.message : String(error)
});
throw error;
}
}
getCurrentProject(): ProjectContext | null {
if (!this.currentProject) return null;
return this.projects.get(this.currentProject) || null;
}
async getProjectRAGContext(projectId?: string): Promise<RAGNamespace | null> {
const id = projectId || this.currentProject;
if (!id) return null;
return this.ragNamespaces.get(id) || null;
}
async updateProjectDocumentation(
content: string,
category: string,
projectId?: string
): Promise<void> {
try {
const id = projectId || this.currentProject;
if (!id) throw new Error('No active project');
const project = this.projects.get(id);
if (!project) throw new Error('Project not found');
const docPath = join(project.isolation.documentationPath, `${category}.md`);
await fs.mkdir(project.isolation.documentationPath, { recursive: true });
await fs.writeFile(docPath, content);
// Update RAG with new documentation
await this.updateProjectRAG(project, docPath, content);
this.emit('documentation_updated', {
projectId: id,
category,
path: docPath
});
} catch (error) {
this.emit('error', {
operation: 'updateProjectDocumentation',
error: error instanceof Error ? error.message : String(error)
});
}
}
async queryProjectKnowledge(
query: string,
projectId?: string,
includeGlobal: boolean = false
): Promise<any[]> {
try {
const id = projectId || this.currentProject;
if (!id) throw new Error('No active project');
const ragNamespace = this.ragNamespaces.get(id);
if (!ragNamespace) return [];
// Query project-specific knowledge
const projectResults = await this.queryRAGNamespace(ragNamespace, query);
// Optionally include global/cross-project knowledge
let globalResults: any[] = [];
if (includeGlobal && this.config.sharedGlobalKnowledge) {
globalResults = await this.queryCrossProjectKnowledge(query);
}
return [...projectResults, ...globalResults];
} catch (error) {
this.emit('error', {
operation: 'queryProjectKnowledge',
error: error instanceof Error ? error.message : String(error)
});
return [];
}
}
async learnFromProject(
projectId: string,
learningType: 'pattern' | 'best_practice' | 'solution' | 'anti_pattern',
data: any
): Promise<void> {
try {
const project = this.projects.get(projectId);
if (!project) throw new Error('Project not found');
// Store in project-specific learning
await this.storeProjectLearning(project, learningType, data);
// Optionally contribute to cross-project learning
if (this.config.allowCrossProjectLearning) {
await this.contributeToCrossProjectLearning(learningType, data, project);
}
this.emit('learning_stored', {
projectId,
learningType,
dataSize: JSON.stringify(data).length
});
} catch (error) {
this.emit('error', {
operation: 'learnFromProject',
error: error instanceof Error ? error.message : String(error)
});
}
}
async getProjectInsights(projectId?: string): Promise<any> {
try {
const id = projectId || this.currentProject;
if (!id) throw new Error('No active project');
const project = this.projects.get(id);
if (!project) throw new Error('Project not found');
const insights = {
project: {
name: project.name,
type: project.type,
framework: project.framework,
complexity: project.metadata.complexity,
accessCount: project.metadata.accessCount
},
rag: await this.getRAGInsights(id),
agents: await this.getAgentInsights(project),
documentation: await this.getDocumentationInsights(project),
learnings: await this.getProjectLearningInsights(project),
recommendations: await this.getProjectRecommendations(project)
};
return insights;
} catch (error) {
this.emit('error', {
operation: 'getProjectInsights',
error: error instanceof Error ? error.message : String(error)
});
return {};
}
}
async exportProjectData(projectId: string, outputPath: string): Promise<void> {
try {
const project = this.projects.get(projectId);
if (!project) throw new Error('Project not found');
const exportData = {
version: '1.0.0',
timestamp: Date.now(),
project: project,
rag: this.ragNamespaces.get(projectId),
documentation: await this.exportProjectDocumentation(project),
learnings: await this.exportProjectLearnings(project),
configurations: await this.exportProjectConfigurations(project)
};
await fs.writeFile(outputPath, JSON.stringify(exportData, null, 2));
this.emit('project_exported', {
projectId,
outputPath,
size: JSON.stringify(exportData).length
});
} catch (error) {
this.emit('error', {
operation: 'exportProjectData',
error: error instanceof Error ? error.message : String(error)
});
}
}
async importProjectData(inputPath: string): Promise<string> {
try {
const data = await fs.readFile(inputPath, 'utf-8');
const importData = JSON.parse(data);
if (importData.version !== '1.0.0') {
throw new Error(`Incompatible version: ${importData.version}`);
}
const project: ProjectContext = importData.project;
// Import project context
this.projects.set(project.id, project);
await this.saveProjectContext(project);
// Import RAG namespace
if (importData.rag) {
this.ragNamespaces.set(project.id, importData.rag);
}
// Restore project isolation
await this.setupProjectIsolation(project);
// Import documentation and learnings
if (importData.documentation) {
await this.importProjectDocumentation(project, importData.documentation);
}
if (importData.learnings) {
await this.importProjectLearnings(project, importData.learnings);
}
this.emit('project_imported', {
projectId: project.id,
inputPath,
name: project.name
});
return project.id;
} catch (error) {
this.emit('error', {
operation: 'importProjectData',
error: error instanceof Error ? error.message : String(error)
});
throw error;
}
}
async listProjects(): Promise<ProjectContext[]> {
return Array.from(this.projects.values()).sort((a, b) =>
b.metadata.lastAccessed - a.metadata.lastAccessed
);
}
async removeProject(projectId: string, deleteData: boolean = false): Promise<void> {
try {
const project = this.projects.get(projectId);
if (!project) throw new Error('Project not found');
// Remove from memory
this.projects.delete(projectId);
this.ragNamespaces.delete(projectId);
// Optionally delete persistent data
if (deleteData) {
await this.deleteProjectData(project);
}
// Update current project if needed
if (this.currentProject === projectId) {
this.currentProject = null;
}
this.emit('project_removed', {
projectId,
name: project.name,
dataDeleted: deleteData
});
} catch (error) {
this.emit('error', {
operation: 'removeProject',
error: error instanceof Error ? error.message : String(error)
});
}
}
private async generateProjectSignature(projectPath: string): Promise<string> {
try {
const packageJson = await this.safeReadJson(join(projectPath, 'package.json'));
const tsConfig = await this.safeReadJson(join(projectPath, 'tsconfig.json'));
const signatureData = {
path: projectPath,
packageJson: packageJson?.name || '',
dependencies: Object.keys(packageJson?.dependencies || {}),
devDependencies: Object.keys(packageJson?.devDependencies || {}),
typescript: !!tsConfig,
timestamp: Date.now()
};
return createHash('md5')
.update(JSON.stringify(signatureData))
.digest('hex');
} catch (error) {
return createHash('md5')
.update(`${projectPath}-${Date.now()}`)
.digest('hex');
}
}
private async createProjectContext(
projectPath: string,
signature: string
): Promise<ProjectContext> {
const projectName = projectPath.split('/').pop() || 'unknown';
const projectId = `project_${signature}`;
const packageJson = await this.safeReadJson(join(projectPath, 'package.json'));
const projectType = this.detectProjectType(projectPath, packageJson);
const framework = this.detectFramework(packageJson);
return {
id: projectId,
path: projectPath,
name: projectName,
type: projectType,
framework,
version: packageJson?.version || '1.0.0',
signature,
metadata: {
createdAt: Date.now(),
lastAccessed: Date.now(),
accessCount: 1,
size: 0,
dependencies: Object.keys(packageJson?.dependencies || {}),
technologies: this.detectTechnologies(packageJson),
patterns: [],
complexity: 'medium'
},
isolation: {
ragNamespace: `rag_${projectId}`,
agentConfigs: {},
documentationPath: join(this.frameworkBasePath, 'projects', projectId, 'docs'),
cacheNamespace: `cache_${projectId}`,
learningPath: join(this.frameworkBasePath, 'projects', projectId, 'learning')
},
configuration: {
enabledAgents: [],
preferences: {},
customRules: [],
qualityGates: []
}
};
}
private async setupProjectIsolation(project: ProjectContext): Promise<void> {
const projectBasePath = join(this.frameworkBasePath, 'projects', project.id);
// Create isolated directories
await fs.mkdir(project.isolation.documentationPath, { recursive: true });
await fs.mkdir(project.isolation.learningPath, { recursive: true });
await fs.mkdir(join(projectBasePath, 'cache'), { recursive: true });
await fs.mkdir(join(projectBasePath, 'rag'), { recursive: true });
// Initialize RAG namespace
const ragNamespace: RAGNamespace = {
projectId: project.id,
namespace: project.isolation.ragNamespace,
vectorStore: join(projectBasePath, 'rag', 'vectors.json'),
documentPath: join(projectBasePath, 'rag', 'documents'),
embeddings: [],
metadata: {
documentCount: 0,
lastUpdated: Date.now(),
averageChunkSize: 0,
topics: []
}
};
this.ragNamespaces.set(project.id, ragNamespace);
await fs.mkdir(ragNamespace.documentPath, { recursive: true });
}
private findProjectByPath(projectPath: string): ProjectContext | undefined {
return Array.from(this.projects.values()).find(
project => project.path === projectPath
);
}
private detectProjectType(projectPath: string, packageJson: any): string {
if (packageJson?.dependencies?.react || packageJson?.devDependencies?.react) {
return 'react-application';
}
if (packageJson?.dependencies?.vue || packageJson?.devDependencies?.vue) {
return 'vue-application';
}
if (packageJson?.dependencies?.express || packageJson?.devDependencies?.express) {
return 'node-server';
}
if (packageJson?.dependencies?.typescript || packageJson?.devDependencies?.typescript) {
return 'typescript-application';
}
return 'generic-project';
}
private detectFramework(packageJson: any): string {
const deps = { ...packageJson?.dependencies, ...packageJson?.devDependencies };
if (deps.react) return 'react';
if (deps.vue) return 'vue';
if (deps.angular) return 'angular';
if (deps.svelte) return 'svelte';
if (deps.express) return 'express';
if (deps.fastify) return 'fastify';
if (deps.typescript) return 'typescript';
return 'vanilla';
}
private detectTechnologies(packageJson: any): string[] {
const deps = { ...packageJson?.dependencies, ...packageJson?.devDependencies };
const technologies = [];
// Frontend frameworks
if (deps.react) technologies.push('react');
if (deps.vue) technologies.push('vue');
if (deps.angular) technologies.push('angular');
// Backend frameworks
if (deps.express) technologies.push('express');
if (deps.fastify) technologies.push('fastify');
// Testing
if (deps.jest) technologies.push('jest');
if (deps.mocha) technologies.push('mocha');
if (deps.playwright) technologies.push('playwright');
// Build tools
if (deps.webpack) technologies.push('webpack');
if (deps.vite) technologies.push('vite');
if (deps.rollup) technologies.push('rollup');
// Languages
if (deps.typescript) technologies.push('typescript');
return technologies;
}
private async safeReadJson(filePath: string): Promise<any> {
try {
const content = await fs.readFile(filePath, 'utf-8');
return JSON.parse(content);
} catch (error) {
return null;
}
}
private async loadGlobalProjects(): Promise<void> {
try {
const projectsPath = join(this.frameworkBasePath, 'projects.json');
const content = await fs.readFile(projectsPath, 'utf-8');
const data = JSON.parse(content);
for (const [id, project] of Object.entries(data.projects || {})) {
this.projects.set(id, project as ProjectContext);
}
} catch (error) {
// No existing projects
}
}
private async saveProjectContext(project: ProjectContext): Promise<void> {
try {
const projectsPath = join(this.frameworkBasePath, 'projects.json');
const existingData = await this.safeReadJson(projectsPath) || { projects: {} };
existingData.projects[project.id] = project;
existingData.lastUpdated = Date.now();
await fs.writeFile(projectsPath, JSON.stringify(existingData, null, 2));
} catch (error) {
this.emit('error', {
operation: 'saveProjectContext',
error: error instanceof Error ? error.message : String(error)
});
}
}
private async loadCrossProjectLearning(): Promise<void> {
try {
const learningPath = join(this.frameworkBasePath, 'cross-project-learning.json');
const content = await fs.readFile(learningPath, 'utf-8');
const data = JSON.parse(content);
// Restore learning maps
this.crossProjectLearning.patterns = new Map(data.patterns || []);
this.crossProjectLearning.bestPractices = new Map(data.bestPractices || []);
this.crossProjectLearning.commonSolutions = new Map(data.commonSolutions || []);
this.crossProjectLearning.antiPatterns = new Map(data.antiPatterns || []);
this.crossProjectLearning.performanceInsights = new Map(data.performanceInsights || []);
} catch (error) {
// No existing cross-project learning
}
}
// Placeholder methods for advanced functionality
private async loadProjectConfigurations(project: ProjectContext): Promise<void> {
// Load project-specific agent configurations
}
private async activateProjectRAG(project: ProjectContext): Promise<void> {
// Activate project-specific RAG namespace
}
private async switchAgentConfigurations(project: ProjectContext): Promise<void> {
// Switch to project-specific agent configurations
}
private async updateProjectRAG(project: ProjectContext, docPath: string, content: string): Promise<void> {
// Update project RAG with new documentation
}
private async queryRAGNamespace(namespace: RAGNamespace, query: string): Promise<any[]> {
// Query project-specific RAG namespace
return [];
}
private async queryCrossProjectKnowledge(query: string): Promise<any[]> {
// Query cross-project knowledge base
return [];
}
private async storeProjectLearning(project: ProjectContext, type: string, data: any): Promise<void> {
// Store learning in project-specific location
}
private async contributeToCrossProjectLearning(type: string, data: any, project: ProjectContext): Promise<void> {
// Contribute anonymized learning to cross-project knowledge
}
private async getRAGInsights(projectId: string): Promise<any> {
const namespace = this.ragNamespaces.get(projectId);
return namespace?.metadata || {};
}
private async getAgentInsights(project: ProjectContext): Promise<any> {
return {
enabled: project.configuration.enabledAgents,
recommendations: []
};
}
private async getDocumentationInsights(project: ProjectContext): Promise<any> {
return {
path: project.isolation.documentationPath,
files: []
};
}
private async getProjectLearningInsights(project: ProjectContext): Promise<any> {
return {
patterns: 0,
solutions: 0,
bestPractices: 0
};
}
private async getProjectRecommendations(project: ProjectContext): Promise<any[]> {
return [];
}
private async exportProjectDocumentation(project: ProjectContext): Promise<any> {
return {};
}
private async exportProjectLearnings(project: ProjectContext): Promise<any> {
return {};
}
private async exportProjectConfigurations(project: ProjectContext): Promise<any> {
return project.configuration;
}
private async importProjectDocumentation(project: ProjectContext, data: any): Promise<void> {
// Import documentation data
}
private async importProjectLearnings(project: ProjectContext, data: any): Promise<void> {
// Import learning data
}
private async deleteProjectData(project: ProjectContext): Promise<void> {
const projectBasePath = join(this.frameworkBasePath, 'projects', project.id);
await fs.rmdir(projectBasePath, { recursive: true });
}
}
export default MultiProjectManager;