UNPKG

@restnfeel/agentc-starter-kit

Version:

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

462 lines (411 loc) 12.5 kB
/** * Configuration Control Board (CCB) Workflow Engine * Manages state transitions, approval processes, and automated actions */ import { ChangeRequest, ChangeRequestStatus, CCBWorkflowState, CCBDecision, CCBConfiguration, AutoTransitionRule, ImpactAssessment, } from "../types/ccb.js"; import { ChangeRequestModel, CCBDecisionModel, CCBConfigurationModel, } from "./models.js"; export class CCBWorkflow { private static readonly WORKFLOW_STATES: Record< ChangeRequestStatus, CCBWorkflowState > = { submitted: { currentStatus: "submitted", allowedTransitions: ["under_review", "rejected"], requiredActions: ["assign_reviewer", "initial_assessment"], autoTransitionRules: [ { condition: "no_reviewer_assigned_after_2_days", targetStatus: "escalated", delayDays: 2, }, ], }, under_review: { currentStatus: "under_review", allowedTransitions: [ "impact_assessment", "rejected", "approved", "deferred", ], requiredActions: ["technical_review", "risk_assessment"], autoTransitionRules: [ { condition: "review_incomplete_after_5_days", targetStatus: "escalated", delayDays: 5, }, ], }, impact_assessment: { currentStatus: "impact_assessment", allowedTransitions: ["approved", "rejected", "deferred", "remanded"], requiredActions: [ "impact_analysis", "resource_estimation", "risk_evaluation", ], autoTransitionRules: [], }, approved: { currentStatus: "approved", allowedTransitions: ["implemented", "deferred"], requiredActions: ["implementation_planning", "resource_allocation"], autoTransitionRules: [ { condition: "implementation_ready", targetStatus: "implemented", delayDays: 0, }, ], }, rejected: { currentStatus: "rejected", allowedTransitions: ["closed"], requiredActions: ["notify_requestor", "document_reasons"], autoTransitionRules: [ { condition: "auto_close_after_30_days", targetStatus: "closed", delayDays: 30, }, ], }, deferred: { currentStatus: "deferred", allowedTransitions: ["under_review", "rejected", "closed"], requiredActions: ["schedule_review", "notify_stakeholders"], autoTransitionRules: [], }, escalated: { currentStatus: "escalated", allowedTransitions: ["under_review", "approved", "rejected"], requiredActions: ["senior_review", "priority_assessment"], autoTransitionRules: [], }, remanded: { currentStatus: "remanded", allowedTransitions: ["under_review", "rejected"], requiredActions: ["address_concerns", "resubmit_proposal"], autoTransitionRules: [], }, implemented: { currentStatus: "implemented", allowedTransitions: ["closed"], requiredActions: ["verify_implementation", "post_implementation_review"], autoTransitionRules: [ { condition: "verification_complete", targetStatus: "closed", delayDays: 7, }, ], }, closed: { currentStatus: "closed", allowedTransitions: [], requiredActions: ["archive_request", "lessons_learned"], autoTransitionRules: [], }, }; /** * Validates if a status transition is allowed */ static canTransition( currentStatus: ChangeRequestStatus, targetStatus: ChangeRequestStatus ): boolean { const currentState = this.WORKFLOW_STATES[currentStatus]; return currentState.allowedTransitions.includes(targetStatus); } /** * Gets the workflow state for a given status */ static getWorkflowState(status: ChangeRequestStatus): CCBWorkflowState { return this.WORKFLOW_STATES[status]; } /** * Transitions a change request to a new status with validation */ static transition( request: ChangeRequest, targetStatus: ChangeRequestStatus, reason?: string ): { success: boolean; request?: ChangeRequest; error?: string } { if (!this.canTransition(request.status, targetStatus)) { return { success: false, error: `Invalid transition from ${request.status} to ${targetStatus}`, }; } const updatedRequest = ChangeRequestModel.updateStatus( request, targetStatus ); if (reason) { const updatedWithComment = ChangeRequestModel.addComment(updatedRequest, { authorId: "system", authorName: "CCB System", content: `Status changed to ${targetStatus}. Reason: ${reason}`, isInternal: true, }); return { success: true, request: updatedWithComment }; } return { success: true, request: updatedRequest }; } /** * Processes a CCB decision and updates the request status accordingly */ static processDecision( request: ChangeRequest, decision: CCBDecision, configuration: CCBConfiguration = CCBConfigurationModel.getDefault() ): { success: boolean; request?: ChangeRequest; error?: string } { const voteResult = CCBDecisionModel.calculateVoteResult( decision, configuration ); let targetStatus: ChangeRequestStatus; let reason: string; switch (decision.decision) { case "approved": if (voteResult.passed) { targetStatus = "approved"; reason = `Approved by CCB vote (${voteResult.percentage.toFixed( 1 )}% approval)`; } else { targetStatus = "rejected"; reason = `Insufficient votes for approval (${voteResult.percentage.toFixed( 1 )}% approval, ${configuration.votingThreshold}% required)`; } break; case "rejected": targetStatus = "rejected"; reason = `Rejected by CCB. Rationale: ${decision.rationale}`; break; case "deferred": targetStatus = "deferred"; reason = `Deferred by CCB. Next review: ${ decision.nextReviewDate?.toISOString() || "TBD" }`; break; case "escalated": targetStatus = "escalated"; reason = `Escalated by CCB. Reason: ${ decision.escalationReason || "No reason provided" }`; break; case "remanded": targetStatus = "remanded"; reason = `Remanded by CCB for revision. Rationale: ${decision.rationale}`; break; default: return { success: false, error: `Unknown decision type: ${decision.decision}`, }; } const transitionResult = this.transition(request, targetStatus, reason); if (!transitionResult.success || !transitionResult.request) { return transitionResult; } // Add the decision to the request const requestWithDecision: ChangeRequest = { ...transitionResult.request, decision: decision, }; return { success: true, request: requestWithDecision }; } /** * Checks if auto-transition rules should be applied */ static checkAutoTransitions( request: ChangeRequest, configuration: CCBConfiguration = CCBConfigurationModel.getDefault() ): { shouldTransition: boolean; targetStatus?: ChangeRequestStatus; reason?: string; } { const currentState = this.getWorkflowState(request.status); const daysSinceUpdate = Math.floor( (Date.now() - request.updatedAt.getTime()) / (1000 * 60 * 60 * 24) ); for (const rule of currentState.autoTransitionRules || []) { if ( this.evaluateAutoTransitionCondition( rule, request, daysSinceUpdate, configuration ) ) { return { shouldTransition: true, targetStatus: rule.targetStatus, reason: `Auto-transition triggered: ${rule.condition}`, }; } } return { shouldTransition: false }; } /** * Evaluates auto-transition conditions */ private static evaluateAutoTransitionCondition( rule: AutoTransitionRule, request: ChangeRequest, daysSinceUpdate: number, configuration: CCBConfiguration ): boolean { switch (rule.condition) { case "no_reviewer_assigned_after_2_days": return !request.assignedReviewer && daysSinceUpdate >= 2; case "review_incomplete_after_5_days": return ( daysSinceUpdate >= (rule.delayDays || configuration.defaultReviewDays) ); case "auto_close_after_30_days": return daysSinceUpdate >= 30; case "implementation_ready": return ( request.status === "approved" && this.isImplementationReady(request) ); case "verification_complete": return request.status === "implemented" && daysSinceUpdate >= 7; default: return false; } } /** * Checks if implementation is ready to proceed */ private static isImplementationReady(request: ChangeRequest): boolean { // Check if all pre-implementation requirements are met return Boolean( request.decision?.decision === "approved" && request.proposedImplementationDate && request.proposedImplementationDate <= new Date() ); } /** * Gets required actions for current status */ static getRequiredActions(status: ChangeRequestStatus): string[] { return this.getWorkflowState(status).requiredActions; } /** * Gets allowed next statuses for current status */ static getAllowedTransitions( status: ChangeRequestStatus ): ChangeRequestStatus[] { return this.getWorkflowState(status).allowedTransitions; } /** * Validates if a request can proceed to impact assessment */ static canProceedToImpactAssessment(request: ChangeRequest): { canProceed: boolean; missingRequirements: string[]; } { const missingRequirements: string[] = []; if (!request.assignedReviewer) { missingRequirements.push("Reviewer must be assigned"); } if (!request.justification?.trim()) { missingRequirements.push("Justification must be provided"); } if (!request.affectedItems || request.affectedItems.length === 0) { missingRequirements.push("Affected items must be identified"); } if ( request.priority === "critical" && !request.proposedImplementationDate ) { missingRequirements.push( "Critical requests must have proposed implementation date" ); } return { canProceed: missingRequirements.length === 0, missingRequirements, }; } /** * Validates if a request can be approved */ static canApprove( request: ChangeRequest, assessment?: ImpactAssessment ): { canApprove: boolean; blockers: string[]; } { const blockers: string[] = []; if (!assessment) { blockers.push("Impact assessment is required"); } else { if (!assessment.approved) { blockers.push("Impact assessment must be approved"); } if ( assessment.overallRisk === "critical" && !assessment.mitigationStrategies.length ) { blockers.push("Critical risk requires mitigation strategies"); } } if (request.priority === "critical" && !request.ccbMeetingDate) { blockers.push("Critical requests require CCB meeting"); } return { canApprove: blockers.length === 0, blockers, }; } /** * Suggests next actions based on current request state */ static suggestNextActions(request: ChangeRequest): string[] { const state = this.getWorkflowState(request.status); const suggestions: string[] = [...state.requiredActions]; // Add context-specific suggestions switch (request.status) { case "submitted": if (!request.assignedReviewer) { suggestions.unshift("Assign a reviewer"); } break; case "under_review": if (!request.comments.length) { suggestions.push("Add initial review comments"); } break; case "impact_assessment": suggestions.push("Complete impact assessment"); break; case "approved": if (!request.proposedImplementationDate) { suggestions.push("Set implementation date"); } break; } return suggestions; } }