@restnfeel/agentc-starter-kit
Version:
한국어 기업용 CMS 모듈 - Task Master AI와 함께 빠르게 웹사이트를 구현할 수 있는 재사용 가능한 컴포넌트 시스템
462 lines (411 loc) • 12.5 kB
text/typescript
/**
* 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;
}
}