package domain import ( "errors" "slices" "strings" "git.cin.su/m/Assistant/contracts/work" ) var ErrInvalidDevelopmentGate = errors.New("invalid governed self-development gate") var ErrInsufficientExtensionEvidence = errors.New("insufficient repeated extension evidence") var ErrInvalidExtensionProposal = errors.New("invalid extension proposal") // BuildExtensionProposal deterministically turns a repeated evidence signal // into a proposal. It retains every distinct evidence reference for lineage, // while deduplicating signal keys so repeated reports do not create duplicate // proposals. It never creates a task, mutates an implementation or grants // authority. func BuildExtensionProposal(evidence []work.ExtensionEvidence, request work.ExtensionProposalRequest) (work.ExtensionProposal, error) { owners := cleanStrings(request.AffectedOwners) contracts := cleanStrings(request.AffectedContracts) evaluation := cleanStrings(request.EvaluationPlan) alternatives := cleanStrings(request.Alternatives) assumptions := cleanStrings(request.Assumptions) if request.ID == "" || request.CreatedAt.IsZero() || strings.TrimSpace(request.DesiredOutcome) == "" || strings.TrimSpace(request.SmallestChangeLayer) == "" || strings.TrimSpace(request.RiskTier) == "" || strings.TrimSpace(request.RollbackPlan) == "" || len(owners) == 0 || len(contracts) == 0 || len(evaluation) == 0 || len(alternatives) == 0 || len(assumptions) == 0 { return work.ExtensionProposal{}, ErrInvalidExtensionProposal } refs := make([]string, 0, len(evidence)) refSeen := make(map[string]struct{}, len(evidence)) counts := make(map[string]int, len(evidence)) keys := make([]string, 0, len(evidence)) keySeen := make(map[string]struct{}, len(evidence)) for _, item := range evidence { ref := strings.TrimSpace(item.Ref) key := strings.TrimSpace(item.SignalKey) if ref == "" || key == "" { return work.ExtensionProposal{}, ErrInvalidExtensionProposal } if _, ok := refSeen[ref]; !ok { refs = append(refs, ref) refSeen[ref] = struct{}{} } counts[key]++ if _, ok := keySeen[key]; !ok { keys = append(keys, key) keySeen[key] = struct{}{} } } if len(refs) < 2 { return work.ExtensionProposal{}, ErrInsufficientExtensionEvidence } repeated := false for _, count := range counts { if count >= 2 { repeated = true break } } if !repeated { return work.ExtensionProposal{}, ErrInsufficientExtensionEvidence } return work.ExtensionProposal{ ID: request.ID, Stage: work.ExtensionProposalProposed, EvidenceRefs: refs, DeduplicatedSignalKeys: keys, DesiredOutcome: strings.TrimSpace(request.DesiredOutcome), SmallestChangeLayer: strings.TrimSpace(request.SmallestChangeLayer), AffectedOwners: owners, AffectedContracts: contracts, RiskTier: strings.TrimSpace(request.RiskTier), EvaluationPlan: evaluation, Alternatives: alternatives, RollbackPlan: strings.TrimSpace(request.RollbackPlan), Assumptions: assumptions, CreatedAtUnixMillis: request.CreatedAt.UnixMilli(), }, nil } func cleanStrings(values []string) []string { result := make([]string, 0, len(values)) seen := make(map[string]struct{}, len(values)) for _, value := range values { value = strings.TrimSpace(value) if value == "" { continue } if _, ok := seen[value]; ok { continue } seen[value] = struct{}{} result = append(result, value) } return result } func AdvanceDevelopmentGate(current work.DevelopmentGate, next work.DevelopmentStage) (work.DevelopmentGate, error) { if err := ValidateDevelopmentGate(current); err != nil { return work.DevelopmentGate{}, err } if !validDevelopmentTransition(current.Stage, next) { return work.DevelopmentGate{}, ErrInvalidDevelopmentGate } current.Stage = next switch next { case work.DevelopmentProposalCreated: if current.ProposalRef == "" { return work.DevelopmentGate{}, ErrInvalidDevelopmentGate } case work.DevelopmentTaskAdmitted: if current.TaskPacketRef == "" { return work.DevelopmentGate{}, ErrInvalidDevelopmentGate } case work.DevelopmentIsolated: if current.WorktreeRef == "" { return work.DevelopmentGate{}, ErrInvalidDevelopmentGate } case work.DevelopmentVerified: if current.VerificationEvidence == "" || len(current.VerifierRunRefs) == 0 || len(current.ProducerRunRefs) == 0 || sameRuns(current.ProducerRunRefs, current.VerifierRunRefs) { return work.DevelopmentGate{}, ErrInvalidDevelopmentGate } case work.DevelopmentCanary: if current.CanaryEvidence == "" { return work.DevelopmentGate{}, ErrInvalidDevelopmentGate } case work.DevelopmentPromoted: if current.PromotionRef == "" { return work.DevelopmentGate{}, ErrInvalidDevelopmentGate } case work.DevelopmentRolledBack: if current.RollbackRef == "" { return work.DevelopmentGate{}, ErrInvalidDevelopmentGate } } return current, nil } func ValidateDevelopmentGate(value work.DevelopmentGate) error { if value.ID == "" || len(value.EvidenceRefs) == 0 || !validDevelopmentStage(value.Stage) || duplicateStrings(value.EvidenceRefs) { return ErrInvalidDevelopmentGate } return nil } func validDevelopmentTransition(current, next work.DevelopmentStage) bool { if current == work.DevelopmentCanary && next == work.DevelopmentRolledBack { return true } if current == work.DevelopmentPromoted || current == work.DevelopmentRolledBack { return false } order := []work.DevelopmentStage{work.DevelopmentEvidenceCollected, work.DevelopmentProposalCreated, work.DevelopmentTaskAdmitted, work.DevelopmentIsolated, work.DevelopmentVerified, work.DevelopmentCanary, work.DevelopmentPromoted} index := func(value work.DevelopmentStage) int { return slices.Index(order, value) } return index(next) == index(current)+1 } func validDevelopmentStage(value work.DevelopmentStage) bool { return slices.Contains([]work.DevelopmentStage{work.DevelopmentEvidenceCollected, work.DevelopmentProposalCreated, work.DevelopmentTaskAdmitted, work.DevelopmentIsolated, work.DevelopmentVerified, work.DevelopmentCanary, work.DevelopmentPromoted, work.DevelopmentRolledBack}, value) } func duplicateStrings(values []string) bool { seen := map[string]bool{} for _, value := range values { if value == "" || seen[value] { return true } seen[value] = true } return false } func sameRuns(left, right []work.AgentRunID) bool { if len(left) != len(right) { return false } for _, value := range left { if !slices.Contains(right, value) { return false } } return true }