UNPKG

@restnfeel/agentc-starter-kit

Version:

한국어 기업용 CMS 모듈 - Task Master AI와 함께 빠르게 웹사이트를 구현할 수 있는 재사용 가능한 컴포넌트 시스템

734 lines (646 loc) 19.4 kB
/** * Configuration Control Board (CCB) Service * High-level API that integrates all CCB components and manages the complete lifecycle */ import { ChangeRequest, ConfigurationItem, ImpactAssessment, CCBDecision, CCBConfiguration, CCBMetrics, CCBSearchFilters, CCBSearchResult, ChangeRequestStatus, Priority, ChangeType, CCBMember, Vote, ImpactCategory, } from "../types/ccb.js"; import { ChangeRequestModel, ConfigurationItemModel, ImpactAssessmentModel, CCBDecisionModel, CCBMetricsModel, CCBConfigurationModel, } from "./models.js"; import { CCBWorkflow } from "./workflow.js"; export class CCBService { private static requests: Map<string, ChangeRequest> = new Map(); private static assessments: Map<string, ImpactAssessment> = new Map(); private static decisions: Map<string, CCBDecision> = new Map(); private static members: Map<string, CCBMember> = new Map(); // Initialize default CCB members static { this.initializeDefaultMembers(); } /** * Initialize default CCB members for AgentC */ private static initializeDefaultMembers(): void { const defaultMembers: CCBMember[] = [ { id: "ccb-chair", name: "CCB Chairperson", role: "chairperson", department: "Management", votingPower: 3, }, { id: "tech-lead", name: "Technical Lead", role: "technical_lead", department: "Engineering", votingPower: 2, }, { id: "product-manager", name: "Product Manager", role: "product_manager", department: "Product", votingPower: 2, }, { id: "security-officer", name: "Security Officer", role: "security_officer", department: "Security", votingPower: 2, }, { id: "qa-lead", name: "QA Lead", role: "qa_lead", department: "Quality Assurance", votingPower: 1, }, ]; defaultMembers.forEach((member) => this.members.set(member.id, member)); } // ======================================== // Change Request Management // ======================================== /** * Submit a new change request */ static submitChangeRequest(data: { title: string; description: string; requesterId: string; requesterName: string; priority: Priority; changeType: ChangeType; affectedItems: ConfigurationItem[]; justification: string; expectedBenefits: string; risksIdentified: string; proposedImplementationDate?: Date; }): { success: boolean; request?: ChangeRequest; errors?: string[] } { // Validate the request const errors = ChangeRequestModel.validateRequest(data); if (errors.length > 0) { return { success: false, errors }; } // Create the change request const request = ChangeRequestModel.create({ ...data, requestedAt: new Date(), }); // Store the request this.requests.set(request.id, request); // Add initial comment const requestWithComment = ChangeRequestModel.addComment(request, { authorId: data.requesterId, authorName: data.requesterName, content: `Change request submitted: ${data.title}`, isInternal: false, }); this.requests.set(request.id, requestWithComment); return { success: true, request: requestWithComment }; } /** * Get a change request by ID */ static getChangeRequest(id: string): ChangeRequest | undefined { return this.requests.get(id); } /** * Search change requests with filters */ static searchChangeRequests( filters: CCBSearchFilters = {}, page = 1, limit = 20 ): CCBSearchResult { let allRequests = Array.from(this.requests.values()); // Apply filters if (filters.status && filters.status.length > 0) { allRequests = allRequests.filter((r) => filters.status!.includes(r.status) ); } if (filters.priority && filters.priority.length > 0) { allRequests = allRequests.filter((r) => filters.priority!.includes(r.priority) ); } if (filters.changeType && filters.changeType.length > 0) { allRequests = allRequests.filter((r) => filters.changeType!.includes(r.changeType) ); } if (filters.requesterId) { allRequests = allRequests.filter( (r) => r.requesterId === filters.requesterId ); } if (filters.assignedReviewer) { allRequests = allRequests.filter( (r) => r.assignedReviewer === filters.assignedReviewer ); } if (filters.dateRange) { allRequests = allRequests.filter( (r) => r.createdAt >= filters.dateRange!.start && r.createdAt <= filters.dateRange!.end ); } // Sort by creation date (newest first) allRequests.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); // Paginate const startIndex = (page - 1) * limit; const endIndex = startIndex + limit; const paginatedRequests = allRequests.slice(startIndex, endIndex); return { items: paginatedRequests, totalCount: allRequests.length, page, limit, hasMore: endIndex < allRequests.length, }; } /** * Assign a reviewer to a change request */ static assignReviewer( requestId: string, reviewerId: string ): { success: boolean; request?: ChangeRequest; error?: string } { const request = this.requests.get(requestId); if (!request) { return { success: false, error: "Request not found" }; } const updatedRequest = ChangeRequestModel.assignReviewer( request, reviewerId ); this.requests.set(requestId, updatedRequest); return { success: true, request: updatedRequest }; } /** * Add a comment to a change request */ static addComment( requestId: string, comment: { authorId: string; authorName: string; content: string; isInternal?: boolean; } ): { success: boolean; request?: ChangeRequest; error?: string } { const request = this.requests.get(requestId); if (!request) { return { success: false, error: "Request not found" }; } const updatedRequest = ChangeRequestModel.addComment(request, { ...comment, isInternal: comment.isInternal || false, }); this.requests.set(requestId, updatedRequest); return { success: true, request: updatedRequest }; } // ======================================== // Impact Assessment Management // ======================================== /** * Create an impact assessment for a change request */ static createImpactAssessment(data: { changeRequestId: string; assessorId: string; assessorName: string; overallRisk: "low" | "medium" | "high" | "critical"; impacts: Array<{ category: ImpactCategory; description: string; severity: "minimal" | "moderate" | "significant" | "major"; likelihood: "low" | "medium" | "high"; estimatedCost?: number; scheduleImpact?: number; mitigationRequired: boolean; }>; estimatedEffort: { developmentHours: number; testingHours: number; deploymentHours: number; documentationHours: number; totalHours: number; estimatedCost: number; confidence: "low" | "medium" | "high"; }; resourceRequirements: Array<{ type: "developer" | "designer" | "qa" | "devops" | "product_manager"; skillLevel: "junior" | "mid" | "senior" | "lead"; hoursRequired: number; availability: "available" | "limited" | "unavailable"; }>; dependencies: Array<{ itemId: string; itemName: string; impactType: | "breaking_change" | "compatibility_issue" | "enhancement" | "no_impact"; description: string; actionRequired: string; }>; testingRequirements: string; rollbackPlan: string; mitigationStrategies: string[]; recommendations: string; }): { success: boolean; assessment?: ImpactAssessment; error?: string } { const request = this.requests.get(data.changeRequestId); if (!request) { return { success: false, error: "Change request not found" }; } const assessment = ImpactAssessmentModel.create(data); this.assessments.set(assessment.id, assessment); // Update request status to impact_assessment if currently under_review if (request.status === "under_review") { const transitionResult = CCBWorkflow.transition( request, "impact_assessment", "Impact assessment created" ); if (transitionResult.success && transitionResult.request) { this.requests.set(data.changeRequestId, transitionResult.request); } } return { success: true, assessment }; } /** * Approve an impact assessment */ static approveImpactAssessment( assessmentId: string, approverId: string ): { success: boolean; assessment?: ImpactAssessment; error?: string } { const assessment = this.assessments.get(assessmentId); if (!assessment) { return { success: false, error: "Assessment not found" }; } const approvedAssessment = ImpactAssessmentModel.approve( assessment, approverId ); this.assessments.set(assessmentId, approvedAssessment); return { success: true, assessment: approvedAssessment }; } // ======================================== // CCB Decision Management // ======================================== /** * Submit a CCB decision */ static submitCCBDecision(data: { changeRequestId: string; decision: "approved" | "rejected" | "deferred" | "escalated" | "remanded"; decidedBy: string; rationale: string; conditions?: string[]; nextReviewDate?: Date; escalationReason?: string; votes: Vote[]; }): { success: boolean; request?: ChangeRequest; decision?: CCBDecision; error?: string; } { const request = this.requests.get(data.changeRequestId); if (!request) { return { success: false, error: "Change request not found" }; } // Create the decision const decision = CCBDecisionModel.create({ ...data, votingMembers: Array.from(this.members.values()), }); this.decisions.set(decision.id, decision); // Process the decision through workflow const workflowResult = CCBWorkflow.processDecision(request, decision); if (!workflowResult.success) { return { success: false, error: workflowResult.error }; } if (workflowResult.request) { this.requests.set(data.changeRequestId, workflowResult.request); } return { success: true, request: workflowResult.request, decision, }; } /** * Get CCB members */ static getCCBMembers(): CCBMember[] { return Array.from(this.members.values()); } /** * Add CCB member */ static addCCBMember(member: CCBMember): void { this.members.set(member.id, member); } // ======================================== // Workflow Management // ======================================== /** * Transition a change request to a new status */ static transitionRequest( requestId: string, targetStatus: ChangeRequestStatus, reason?: string ): { success: boolean; request?: ChangeRequest; error?: string } { const request = this.requests.get(requestId); if (!request) { return { success: false, error: "Request not found" }; } const transitionResult = CCBWorkflow.transition( request, targetStatus, reason ); if (transitionResult.success && transitionResult.request) { this.requests.set(requestId, transitionResult.request); } return transitionResult; } /** * Check and apply auto-transitions for all requests */ static processAutoTransitions(): { processed: number; transitions: Array<{ requestId: string; fromStatus: ChangeRequestStatus; toStatus: ChangeRequestStatus; reason: string; }>; } { const transitions: Array<{ requestId: string; fromStatus: ChangeRequestStatus; toStatus: ChangeRequestStatus; reason: string; }> = []; const config = CCBConfigurationModel.getDefault(); for (const [requestId, request] of this.requests) { const autoTransition = CCBWorkflow.checkAutoTransitions(request, config); if (autoTransition.shouldTransition && autoTransition.targetStatus) { const transitionResult = CCBWorkflow.transition( request, autoTransition.targetStatus, autoTransition.reason ); if (transitionResult.success && transitionResult.request) { this.requests.set(requestId, transitionResult.request); transitions.push({ requestId, fromStatus: request.status, toStatus: autoTransition.targetStatus, reason: autoTransition.reason || "Auto-transition", }); } } } return { processed: transitions.length, transitions, }; } /** * Get workflow information for a request */ static getWorkflowInfo(requestId: string): { currentStatus: ChangeRequestStatus; allowedTransitions: ChangeRequestStatus[]; requiredActions: string[]; suggestedActions: string[]; } | null { const request = this.requests.get(requestId); if (!request) return null; return { currentStatus: request.status, allowedTransitions: CCBWorkflow.getAllowedTransitions(request.status), requiredActions: CCBWorkflow.getRequiredActions(request.status), suggestedActions: CCBWorkflow.suggestNextActions(request), }; } // ======================================== // Configuration Management // ======================================== /** * Get current CCB configuration */ static getConfiguration(): CCBConfiguration { return CCBConfigurationModel.getDefault(); } /** * Update CCB configuration */ static updateConfiguration( updates: Partial<CCBConfiguration> ): CCBConfiguration { return CCBConfigurationModel.update(updates); } // ======================================== // Metrics and Reporting // ======================================== /** * Get CCB metrics */ static getMetrics(): CCBMetrics { const allRequests = Array.from(this.requests.values()); return CCBMetricsModel.calculate(allRequests); } /** * Get dashboard summary */ static getDashboardSummary(): { metrics: CCBMetrics; recentRequests: ChangeRequest[]; pendingActions: Array<{ requestId: string; action: string; daysOverdue: number; }>; upcomingMeetings: Array<{ requestId: string; meetingDate: Date; priority: Priority; }>; } { const metrics = this.getMetrics(); const allRequests = Array.from(this.requests.values()); // Get 10 most recent requests const recentRequests = allRequests .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()) .slice(0, 10); // Get pending actions (requests overdue for review) const pendingActions: Array<{ requestId: string; action: string; daysOverdue: number; }> = []; const config = this.getConfiguration(); const now = new Date(); for (const request of allRequests) { const daysSinceUpdate = Math.floor( (now.getTime() - request.updatedAt.getTime()) / (1000 * 60 * 60 * 24) ); if ( request.status === "submitted" && !request.assignedReviewer && daysSinceUpdate > 1 ) { pendingActions.push({ requestId: request.id, action: "Assign reviewer", daysOverdue: daysSinceUpdate - 1, }); } if ( request.status === "under_review" && daysSinceUpdate > config.defaultReviewDays ) { pendingActions.push({ requestId: request.id, action: "Complete review", daysOverdue: daysSinceUpdate - config.defaultReviewDays, }); } } // Get upcoming CCB meetings const upcomingMeetings = allRequests .filter((r) => r.ccbMeetingDate && r.ccbMeetingDate > now) .sort((a, b) => a.ccbMeetingDate!.getTime() - b.ccbMeetingDate!.getTime()) .slice(0, 5) .map((r) => ({ requestId: r.id, meetingDate: r.ccbMeetingDate!, priority: r.priority, })); return { metrics, recentRequests, pendingActions, upcomingMeetings, }; } // ======================================== // Utility Methods // ======================================== /** * Get all change requests (for testing/admin) */ static getAllRequests(): ChangeRequest[] { return Array.from(this.requests.values()); } /** * Clear all data (for testing) */ static clearAllData(): void { this.requests.clear(); this.assessments.clear(); this.decisions.clear(); // Don't clear members as they're defaults } /** * Get system health check */ static getHealthCheck(): { status: "healthy" | "warning" | "error"; issues: string[]; stats: { totalRequests: number; oldestPendingRequest?: { id: string; daysPending: number }; configurationItems: number; ccbMembers: number; }; } { const issues: string[] = []; const allRequests = Array.from(this.requests.values()); const now = new Date(); // Check for very old pending requests const pendingRequests = allRequests.filter( (r) => !["approved", "rejected", "closed", "implemented"].includes(r.status) ); let oldestPendingRequest: { id: string; daysPending: number } | undefined; for (const request of pendingRequests) { const daysPending = Math.floor( (now.getTime() - request.createdAt.getTime()) / (1000 * 60 * 60 * 24) ); if (daysPending > 30) { issues.push( `Request ${request.id} has been pending for ${daysPending} days` ); } if ( !oldestPendingRequest || daysPending > oldestPendingRequest.daysPending ) { oldestPendingRequest = { id: request.id, daysPending }; } } // Check CCB configuration const config = this.getConfiguration(); if (config.quorumSize < 3) { issues.push("CCB quorum size is below recommended minimum of 3"); } const status = issues.length === 0 ? "healthy" : issues.length < 5 ? "warning" : "error"; return { status, issues, stats: { totalRequests: allRequests.length, oldestPendingRequest, configurationItems: ConfigurationItemModel.getAll().length, ccbMembers: this.members.size, }, }; } private validateMetrics(metrics: Record<string, unknown>): { isValid: boolean; errors: string[]; } { const errors: string[] = []; // Basic validation for required metrics if (typeof metrics.approvalTimeHours !== "number") { errors.push("approvalTimeHours must be a number"); } return { isValid: errors.length === 0, errors, }; } }