Files
Assistant/modules/work/domain/graph_scheduler.go
m 38f8f816d5
CI / test (push) Successful in 1m8s
CI / test (pull_request) Successful in 1m33s
feat: gate graph synthesis on independent verification
2026-08-23 01:43:35 +03:00

108 lines
4.3 KiB
Go

package domain
import (
"errors"
"sort"
"git.cin.su/m/Assistant/contracts/work"
)
var (
ErrGraphBudgetExceeded = errors.New("work graph budget exceeded")
ErrConflictUnresolved = errors.New("conflict resolution is not explicit")
ErrGraphSynthesisRejected = errors.New("graph synthesis rejected")
)
// BuildVerifiedGraphSynthesis resolves a conflict only after an independent
// verifier has evaluated all candidates. The verifier may not be any result
// producer, and every candidate remains in the returned lineage.
func BuildVerifiedGraphSynthesis(conflict work.ConflictSet, candidates []work.ResultCandidate, selectedCandidate string, verifierRunID work.AgentRunID, verifierIdentity string, evidence []work.EvidenceRef, summary string, verified bool) (work.ConflictSet, work.GraphSynthesisCandidate, error) {
if conflict.ID == "" || conflict.GraphID == "" || conflict.GraphRevision < 1 || conflict.NodeID == "" || conflict.Resolved || len(conflict.CandidateIDs) < 2 || selectedCandidate == "" || verifierRunID == "" || verifierIdentity == "" || len(evidence) == 0 || summary == "" || !verified {
return work.ConflictSet{}, work.GraphSynthesisCandidate{}, ErrGraphSynthesisRejected
}
candidateByID := make(map[string]work.ResultCandidate, len(candidates))
for _, candidate := range candidates {
if candidate.ID == "" || candidate.GraphID != conflict.GraphID || candidate.GraphRevision != conflict.GraphRevision || candidate.NodeID != conflict.NodeID || candidate.AuthorRunID == "" {
return work.ConflictSet{}, work.GraphSynthesisCandidate{}, ErrGraphSynthesisRejected
}
candidateByID[candidate.ID] = candidate
if candidate.AuthorRunID == verifierRunID {
return work.ConflictSet{}, work.GraphSynthesisCandidate{}, ErrGraphSynthesisRejected
}
}
ordered := append([]string(nil), conflict.CandidateIDs...)
for _, id := range ordered {
if _, ok := candidateByID[id]; !ok {
return work.ConflictSet{}, work.GraphSynthesisCandidate{}, ErrGraphSynthesisRejected
}
}
if !containsString(ordered, selectedCandidate) {
return work.ConflictSet{}, work.GraphSynthesisCandidate{}, ErrGraphSynthesisRejected
}
dissent := make([]string, 0, len(ordered)-1)
for _, id := range ordered {
if id != selectedCandidate {
dissent = append(dissent, id)
}
}
conflict.Resolved = true
synthesis := work.GraphSynthesisCandidate{ID: conflict.ID + ":synthesis", GraphID: conflict.GraphID, GraphRevision: conflict.GraphRevision, NodeID: conflict.NodeID, CandidateIDs: ordered, SelectedCandidateID: selectedCandidate, DissentCandidateIDs: dissent, VerifierRunID: verifierRunID, VerifierIdentity: verifierIdentity, EvidenceRefs: append([]work.EvidenceRef(nil), evidence...), Summary: summary, State: work.GraphSynthesisVerified}
return conflict, synthesis, nil
}
func containsString(values []string, target string) bool {
for _, value := range values {
if value == target {
return true
}
}
return false
}
func ScheduleReadyNodes(graph work.WorkGraph, active int) ([]work.GraphNodeID, error) {
if err := ValidateWorkGraph(graph); err != nil {
return nil, err
}
if active < 0 || active > graph.Budget.MaxParallel {
return nil, ErrGraphBudgetExceeded
}
ready, err := ReadyGraphNodes(graph)
if err != nil {
return nil, err
}
available := graph.Budget.MaxParallel - active
if len(ready) > available {
ready = ready[:available]
}
return ready, nil
}
func AdmitGraph(graph work.WorkGraph) (work.WorkGraph, error) {
if err := ValidateWorkGraph(graph); err != nil {
return work.WorkGraph{}, err
}
for _, node := range graph.Nodes {
if node.Attempts > graph.Budget.MaxAttempts {
return work.WorkGraph{}, ErrGraphBudgetExceeded
}
}
graph.State = work.GraphAdmitted
return graph, nil
}
func ResolveGraphConflict(conflict work.ConflictSet, selectedCandidate string) (work.ConflictSet, error) {
if conflict.ID == "" || conflict.GraphID == "" || conflict.GraphRevision < 1 || conflict.NodeID == "" || conflict.Resolved || selectedCandidate == "" {
return work.ConflictSet{}, ErrConflictUnresolved
}
candidates := append([]string(nil), conflict.CandidateIDs...)
sort.Strings(candidates)
for _, candidate := range candidates {
if candidate == selectedCandidate {
conflict.CandidateIDs = candidates
conflict.Resolved = true
return conflict, nil
}
}
return work.ConflictSet{}, ErrConflictUnresolved
}