import path from 'path'; import fs from 'fs-extra'; import yaml from 'yaml'; import chalk from 'chalk'; import { ProjectState } from './project-state'; /** * Artifact Manager - Strict protocols for modular asset creation and tracking * * Ensures AI subagents follow systematic artifact management: * - Standardized artifact types and metadata * - Atomic creation and versioning * - Dependency tracking between artifacts * - State consistency and rollback capabilities */ export interface ArtifactMetadata { id: string; name: string; type: ArtifactType; version: string; creator: string; createdAt: string; updatedAt: string; dependencies: string[]; tags: string[]; status: ArtifactStatus; checksum: string; size: number; path: string; description: string; } export type ArtifactType = | 'validation-report' | 'prd-document' | 'technical-architecture' | 'user-stories' | 'database-schema' | 'api-specification' | 'test-plan' | 'deployment-config' | 'project-roadmap' | 'concept-brief' | 'market-analysis' | 'competitive-analysis' | 'persona-definition' | 'wireframes' | 'style-guide'; export type ArtifactStatus = 'draft' | 'review' | 'approved' | 'deprecated' | 'archived'; export interface ArtifactCreationRequest { name: string; type: ArtifactType; content: string; creator: string; dependencies?: string[]; tags?: string[]; description?: string; } export interface ArtifactManagementProtocol { // Pre-creation validation validateArtifactRequest(request: ArtifactCreationRequest): Promise; // Atomic creation with metadata createArtifact(request: ArtifactCreationRequest): Promise; // Update with versioning updateArtifact(id: string, content: string, creator: string): Promise; // Status management updateArtifactStatus(id: string, status: ArtifactStatus, creator: string): Promise; // Dependency tracking addDependency(artifactId: string, dependencyId: string): Promise; getDependencyTree(artifactId: string): Promise; // Cleanup and archival archiveArtifact(id: string, reason: string): Promise; cleanupOrphanedArtifacts(): Promise; } export interface ValidationResult { valid: boolean; errors: string[]; warnings: string[]; } export class ArtifactManager implements ArtifactManagementProtocol { private projectState: ProjectState; private artifactsDir: string; private metadataFile: string; private artifactRegistry: Map = new Map(); constructor(projectState: ProjectState) { this.projectState = projectState; this.artifactsDir = path.join(projectState.getVcsysDir(), 'artifacts'); this.metadataFile = path.join(projectState.getVcsysDir(), 'artifact-registry.yaml'); this.initializeRegistry(); } private async initializeRegistry(): Promise { await fs.ensureDir(this.artifactsDir); if (await fs.pathExists(this.metadataFile)) { const registryData = await fs.readFile(this.metadataFile, 'utf-8'); const registry = yaml.parse(registryData) as Record; Object.entries(registry).forEach(([id, metadata]) => { this.artifactRegistry.set(id, metadata); }); } } private async saveRegistry(): Promise { const registryData = Object.fromEntries(this.artifactRegistry); await fs.writeFile(this.metadataFile, yaml.stringify(registryData, { indent: 2 })); } async validateArtifactRequest(request: ArtifactCreationRequest): Promise { const errors: string[] = []; const warnings: string[] = []; // Validate required fields if (!request.name || request.name.trim().length === 0) { errors.push('Artifact name is required'); } if (!request.content || request.content.trim().length === 0) { errors.push('Artifact content cannot be empty'); } if (!request.creator || request.creator.trim().length === 0) { errors.push('Creator identification is required'); } // Validate naming conventions if (request.name && !/^[a-z0-9-]+$/.test(request.name)) { errors.push('Artifact name must be lowercase alphanumeric with hyphens only'); } // Check for duplicate names const existingArtifact = Array.from(this.artifactRegistry.values()) .find(artifact => artifact.name === request.name && artifact.status !== 'archived'); if (existingArtifact) { errors.push(`Artifact with name '${request.name}' already exists`); } // Validate dependencies exist if (request.dependencies) { for (const depId of request.dependencies) { if (!this.artifactRegistry.has(depId)) { warnings.push(`Dependency '${depId}' not found - will be created as pending dependency`); } } } // Type-specific validations await this.validateArtifactTypeSpecific(request, errors, warnings); return { valid: errors.length === 0, errors, warnings }; } private async validateArtifactTypeSpecific( request: ArtifactCreationRequest, errors: string[], warnings: string[] ): Promise { switch (request.type) { case 'prd-document': if (!request.content.includes('# Product Requirements Document')) { warnings.push('PRD document should start with proper title format'); } if (!request.dependencies?.includes('validation-report')) { warnings.push('PRD documents typically depend on validation reports'); } break; case 'technical-architecture': if (!request.dependencies?.some(dep => this.artifactRegistry.get(dep)?.type === 'prd-document' )) { warnings.push('Technical architecture should depend on PRD document'); } break; case 'validation-report': if (request.content.length < 500) { warnings.push('Validation reports should be comprehensive (>500 characters)'); } break; } } async createArtifact(request: ArtifactCreationRequest): Promise { // Validate request const validation = await this.validateArtifactRequest(request); if (!validation.valid) { throw new Error(`Artifact validation failed: ${validation.errors.join(', ')}`); } // Generate unique ID and metadata const id = this.generateArtifactId(request); const timestamp = new Date().toISOString(); const filename = `${request.name}.md`; const filePath = path.join(this.artifactsDir, filename); const metadata: ArtifactMetadata = { id, name: request.name, type: request.type, version: '1.0.0', creator: request.creator, createdAt: timestamp, updatedAt: timestamp, dependencies: request.dependencies || [], tags: request.tags || [], status: 'draft', checksum: this.generateChecksum(request.content), size: Buffer.byteLength(request.content, 'utf8'), path: filename, description: request.description || `${request.type} created by ${request.creator}` }; // Write artifact content with metadata header const artifactContent = this.buildArtifactDocument(metadata, request.content); await fs.writeFile(filePath, artifactContent); // Update registry this.artifactRegistry.set(id, metadata); await this.saveRegistry(); // Update project state await this.projectState.recordArtifact( request.name, 'document', filename, request.creator ); console.log(chalk.green(`✅ Created artifact: ${request.name} (${id})`)); if (validation.warnings.length > 0) { console.log(chalk.yellow('⚠️ Warnings:')); validation.warnings.forEach(warning => console.log(chalk.yellow(` • ${warning}`))); } return metadata; } private buildArtifactDocument(metadata: ArtifactMetadata, content: string): string { const header = `--- # VC-SYS Artifact Metadata id: ${metadata.id} name: ${metadata.name} type: ${metadata.type} version: ${metadata.version} creator: ${metadata.creator} created: ${metadata.createdAt} updated: ${metadata.updatedAt} status: ${metadata.status} dependencies: ${JSON.stringify(metadata.dependencies)} tags: ${JSON.stringify(metadata.tags)} checksum: ${metadata.checksum} description: ${metadata.description} --- `; return header + content; } async updateArtifact(id: string, content: string, creator: string): Promise { const artifact = this.artifactRegistry.get(id); if (!artifact) { throw new Error(`Artifact not found: ${id}`); } // Create new version const versionParts = artifact.version.split('.'); const newVersion = `${versionParts[0]}.${parseInt(versionParts[1]) + 1}.0`; const updatedMetadata: ArtifactMetadata = { ...artifact, version: newVersion, updatedAt: new Date().toISOString(), checksum: this.generateChecksum(content), size: Buffer.byteLength(content, 'utf8') }; // Update file const filePath = path.join(this.artifactsDir, artifact.path); const artifactContent = this.buildArtifactDocument(updatedMetadata, content); await fs.writeFile(filePath, artifactContent); // Update registry this.artifactRegistry.set(id, updatedMetadata); await this.saveRegistry(); console.log(chalk.blue(`🔄 Updated artifact: ${artifact.name} (v${newVersion})`)); return updatedMetadata; } async updateArtifactStatus(id: string, status: ArtifactStatus, creator: string): Promise { const artifact = this.artifactRegistry.get(id); if (!artifact) { throw new Error(`Artifact not found: ${id}`); } artifact.status = status; artifact.updatedAt = new Date().toISOString(); this.artifactRegistry.set(id, artifact); await this.saveRegistry(); console.log(chalk.yellow(`📋 Status updated: ${artifact.name} → ${status}`)); } async addDependency(artifactId: string, dependencyId: string): Promise { const artifact = this.artifactRegistry.get(artifactId); if (!artifact) { throw new Error(`Artifact not found: ${artifactId}`); } if (!artifact.dependencies.includes(dependencyId)) { artifact.dependencies.push(dependencyId); artifact.updatedAt = new Date().toISOString(); this.artifactRegistry.set(artifactId, artifact); await this.saveRegistry(); console.log(chalk.blue(`🔗 Added dependency: ${artifact.name} → ${dependencyId}`)); } } async getDependencyTree(artifactId: string): Promise { const visited = new Set(); const dependencies: ArtifactMetadata[] = []; const collectDependencies = (id: string) => { if (visited.has(id)) return; visited.add(id); const artifact = this.artifactRegistry.get(id); if (artifact) { dependencies.push(artifact); artifact.dependencies.forEach(depId => collectDependencies(depId)); } }; collectDependencies(artifactId); return dependencies.slice(1); // Remove the root artifact itself } async archiveArtifact(id: string, reason: string): Promise { const artifact = this.artifactRegistry.get(id); if (!artifact) { throw new Error(`Artifact not found: ${id}`); } artifact.status = 'archived'; artifact.updatedAt = new Date().toISOString(); artifact.tags.push(`archived:${reason}`); this.artifactRegistry.set(id, artifact); await this.saveRegistry(); console.log(chalk.gray(`📦 Archived artifact: ${artifact.name} (${reason})`)); } async cleanupOrphanedArtifacts(): Promise { const orphaned: string[] = []; const referencedIds = new Set(); // Collect all referenced dependency IDs this.artifactRegistry.forEach(artifact => { artifact.dependencies.forEach(depId => referencedIds.add(depId)); }); // Find artifacts that are not referenced and not top-level documents this.artifactRegistry.forEach((artifact, id) => { const isTopLevel = ['prd-document', 'validation-report', 'project-roadmap'].includes(artifact.type); const isReferenced = referencedIds.has(id); if (!isTopLevel && !isReferenced && artifact.status !== 'archived') { orphaned.push(id); } }); // Archive orphaned artifacts for (const id of orphaned) { await this.archiveArtifact(id, 'orphaned-cleanup'); } return orphaned; } private generateArtifactId(request: ArtifactCreationRequest): string { const timestamp = Date.now().toString(36); const hash = this.generateChecksum(request.name + request.type).substring(0, 8); return `${request.type}-${hash}-${timestamp}`; } private generateChecksum(content: string): string { // Simple checksum for demonstration - would use crypto in production let hash = 0; for (let i = 0; i < content.length; i++) { const char = content.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; // Convert to 32-bit integer } return Math.abs(hash).toString(16); } // Public methods for querying artifacts getArtifactById(id: string): ArtifactMetadata | undefined { return this.artifactRegistry.get(id); } getArtifactsByType(type: ArtifactType): ArtifactMetadata[] { return Array.from(this.artifactRegistry.values()).filter(a => a.type === type); } getArtifactsByCreator(creator: string): ArtifactMetadata[] { return Array.from(this.artifactRegistry.values()).filter(a => a.creator === creator); } getArtifactsByStatus(status: ArtifactStatus): ArtifactMetadata[] { return Array.from(this.artifactRegistry.values()).filter(a => a.status === status); } getAllArtifacts(): ArtifactMetadata[] { return Array.from(this.artifactRegistry.values()); } }