package domain import ( "errors" "fmt" "git.cin.su/m/Assistant/contracts/capability" "git.cin.su/m/Assistant/contracts/work" ) var ErrInvalidTaskEvaluationGraph = errors.New("invalid task evaluation graph request") // BuildTaskEvaluationGraph turns one bounded profile into durable specialist // nodes. It deliberately has no provider, endpoint or hardware inputs: those // are resolved later by the cognitive worker for each binding. func BuildTaskEvaluationGraph(request work.TaskEvaluationGraphRequest) (work.WorkGraph, error) { if request.GraphID == "" || request.IdempotencyPrefix == "" || len(request.BindingIDs) == 0 || len(request.BindingIDs) != len(request.DispatchIDs) || request.Budget.MaxParallel < 1 || request.Budget.MaxAttempts < 1 { return work.WorkGraph{}, ErrInvalidTaskEvaluationGraph } if err := request.Profile.Validate(); err != nil { return work.WorkGraph{}, ErrInvalidTaskEvaluationGraph } if len(request.Cases) != len(request.Profile.RequiredCaseIDs) { return work.WorkGraph{}, ErrInvalidTaskEvaluationGraph } seenCases := make(map[string]struct{}, len(request.Cases)) for _, item := range request.Cases { if item.CaseID == "" || item.Prompt == "" || item.EvidenceArtifactRef == "" { return work.WorkGraph{}, ErrInvalidTaskEvaluationGraph } if _, exists := seenCases[item.CaseID]; exists { return work.WorkGraph{}, ErrInvalidTaskEvaluationGraph } seenCases[item.CaseID] = struct{}{} } for _, requiredID := range request.Profile.RequiredCaseIDs { if _, exists := seenCases[requiredID]; !exists { return work.WorkGraph{}, ErrInvalidTaskEvaluationGraph } } if request.Profile.Stage == capability.TaskEvaluationGeneral { if request.ParentGraphID != "" || request.ParentGraphRevision != 0 || request.ParentGraphState != "" || request.TriggerEvidenceRef != "" { return work.WorkGraph{}, ErrInvalidTaskEvaluationGraph } } else { if request.ParentGraphID == "" || request.ParentGraphRevision < 1 || request.TriggerEvidenceRef == "" || (request.ParentGraphState != work.GraphCompleted && request.ParentGraphState != work.GraphPartial) { return work.WorkGraph{}, ErrInvalidTaskEvaluationGraph } if request.Profile.ParentProfileID == "" { return work.WorkGraph{}, ErrInvalidTaskEvaluationGraph } } seenBindings := make(map[capability.CapabilityBindingID]struct{}, len(request.BindingIDs)) seenDispatches := make(map[string]struct{}, len(request.DispatchIDs)) nodes := make([]work.GraphNode, 0, len(request.BindingIDs)) for i, bindingID := range request.BindingIDs { if bindingID == "" || request.DispatchIDs[i] == "" { return work.WorkGraph{}, ErrInvalidTaskEvaluationGraph } if _, ok := seenBindings[bindingID]; ok { return work.WorkGraph{}, ErrInvalidTaskEvaluationGraph } if _, ok := seenDispatches[request.DispatchIDs[i]]; ok { return work.WorkGraph{}, ErrInvalidTaskEvaluationGraph } seenBindings[bindingID] = struct{}{} seenDispatches[request.DispatchIDs[i]] = struct{}{} nodes = append(nodes, work.GraphNode{ ID: work.GraphNodeID(fmt.Sprintf("%s-node-%d", request.GraphID, i+1)), DispatchID: request.DispatchIDs[i], IdempotencyKey: fmt.Sprintf("%s:%s:%d", request.IdempotencyPrefix, bindingID, request.Profile.Revision), State: work.GraphNodeBlocked, Purpose: work.GraphNodePurposeTaskEvaluation, TaskProfileID: request.Profile.ID, BindingID: bindingID, CaseIDs: append([]string(nil), request.Profile.RequiredCaseIDs...), TaskProfile: cloneTaskEvaluationProfile(request.Profile), TaskCases: append([]capability.TaskEvaluationCase(nil), request.Cases...), }) } return work.WorkGraph{ID: request.GraphID, Revision: 1, State: work.GraphDraft, Nodes: nodes, Budget: request.Budget, ParentGraphID: request.ParentGraphID, ParentGraphRevision: request.ParentGraphRevision, TriggerEvidenceRef: request.TriggerEvidenceRef}, nil } func cloneTaskEvaluationProfile(value capability.TaskEvaluationProfile) *capability.TaskEvaluationProfile { copyValue := value copyValue.RequiredCaseIDs = append([]string(nil), value.RequiredCaseIDs...) return ©Value }