130 lines
4.8 KiB
Go
130 lines
4.8 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.cin.su/m/Assistant/adapters/developmentexecutor"
|
|
"git.cin.su/m/Assistant/contracts/work"
|
|
workdomain "git.cin.su/m/Assistant/modules/work/domain"
|
|
"git.cin.su/m/Assistant/modules/work/ports"
|
|
)
|
|
|
|
var errDevelopmentGateCreate = errors.New("development gate must start at evidence collected with revision one")
|
|
var errDevelopmentGateNotIsolated = errors.New("development gate is not isolated")
|
|
var errDevelopmentVerifierNotIndependent = errors.New("development verifier is not independent")
|
|
var errDevelopmentWorktreeMismatch = errors.New("verification worktree does not match gate")
|
|
var errDevelopmentChecksFailed = errors.New("development verification checks failed")
|
|
var errDevelopmentEvidenceRequired = errors.New("development transition evidence is required")
|
|
var errDevelopmentEvidenceStage = errors.New("development transition evidence does not match stage")
|
|
|
|
type developmentGateVerificationRequest struct {
|
|
GateID string
|
|
ExpectedRevision int64
|
|
VerifierRunID work.AgentRunID
|
|
Plan developmentexecutor.Plan
|
|
}
|
|
|
|
type developmentGateEvidenceRequest struct {
|
|
GateID string
|
|
ExpectedRevision int64
|
|
NextStage work.DevelopmentStage
|
|
EvidenceRef string
|
|
}
|
|
|
|
func recordDevelopmentGateEvidence(ctx context.Context, store ports.DevelopmentGateStore, request developmentGateEvidenceRequest, now time.Time) (work.DevelopmentGate, error) {
|
|
gate, err := store.LoadDevelopmentGate(ctx, request.GateID)
|
|
if err != nil {
|
|
return work.DevelopmentGate{}, err
|
|
}
|
|
if request.ExpectedRevision < 1 || gate.Revision != request.ExpectedRevision {
|
|
return gate, errDevelopmentGateNotIsolated
|
|
}
|
|
if strings.TrimSpace(request.EvidenceRef) == "" {
|
|
return gate, errDevelopmentEvidenceRequired
|
|
}
|
|
switch request.NextStage {
|
|
case work.DevelopmentCanary:
|
|
gate.CanaryEvidence = strings.TrimSpace(request.EvidenceRef)
|
|
case work.DevelopmentPromoted:
|
|
gate.PromotionRef = strings.TrimSpace(request.EvidenceRef)
|
|
case work.DevelopmentRolledBack:
|
|
gate.RollbackRef = strings.TrimSpace(request.EvidenceRef)
|
|
default:
|
|
return gate, errDevelopmentEvidenceStage
|
|
}
|
|
advanced, err := advanceDevelopmentGate(gate, request.NextStage, now)
|
|
if err != nil {
|
|
return gate, err
|
|
}
|
|
if err := store.SaveDevelopmentGate(ctx, advanced, request.ExpectedRevision); err != nil {
|
|
return gate, err
|
|
}
|
|
return advanced, nil
|
|
}
|
|
|
|
func verifyDevelopmentGate(ctx context.Context, store ports.DevelopmentGateStore, request developmentGateVerificationRequest, now time.Time, runner developmentexecutor.CommandRunner) (work.DevelopmentGate, developmentexecutor.Result, error) {
|
|
gate, err := store.LoadDevelopmentGate(ctx, request.GateID)
|
|
if err != nil {
|
|
return work.DevelopmentGate{}, developmentexecutor.Result{}, err
|
|
}
|
|
if request.ExpectedRevision < 1 || gate.Revision != request.ExpectedRevision || gate.Stage != work.DevelopmentIsolated {
|
|
return gate, developmentexecutor.Result{}, errDevelopmentGateNotIsolated
|
|
}
|
|
if request.VerifierRunID == "" || sameRun(request.VerifierRunID, gate.ProducerRunRefs) {
|
|
return gate, developmentexecutor.Result{}, errDevelopmentVerifierNotIndependent
|
|
}
|
|
if request.Plan.WorktreeID != gate.WorktreeRef {
|
|
return gate, developmentexecutor.Result{}, errDevelopmentWorktreeMismatch
|
|
}
|
|
result, err := developmentexecutor.Execute(ctx, request.Plan, runner)
|
|
if err != nil {
|
|
return gate, result, err
|
|
}
|
|
if !result.Passed {
|
|
return gate, result, errDevelopmentChecksFailed
|
|
}
|
|
gate.VerifierRunRefs = append(append([]work.AgentRunID(nil), gate.VerifierRunRefs...), request.VerifierRunID)
|
|
gate.VerificationEvidence = result.EvidenceRef
|
|
advanced, err := advanceDevelopmentGate(gate, work.DevelopmentVerified, now)
|
|
if err != nil {
|
|
return gate, result, err
|
|
}
|
|
if err := store.SaveDevelopmentGate(ctx, advanced, request.ExpectedRevision); err != nil {
|
|
return gate, result, err
|
|
}
|
|
return advanced, result, nil
|
|
}
|
|
|
|
func sameRun(value work.AgentRunID, runs []work.AgentRunID) bool {
|
|
for _, run := range runs {
|
|
if run == value {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func prepareDevelopmentGate(value work.DevelopmentGate, now time.Time) (work.DevelopmentGate, error) {
|
|
if value.Stage != work.DevelopmentEvidenceCollected || value.Revision != 1 {
|
|
return work.DevelopmentGate{}, errDevelopmentGateCreate
|
|
}
|
|
value.UpdatedAtUnixMillis = now.UnixMilli()
|
|
if err := workdomain.ValidateDevelopmentGate(value); err != nil {
|
|
return work.DevelopmentGate{}, err
|
|
}
|
|
return value, nil
|
|
}
|
|
|
|
func advanceDevelopmentGate(value work.DevelopmentGate, next work.DevelopmentStage, now time.Time) (work.DevelopmentGate, error) {
|
|
advanced, err := workdomain.AdvanceDevelopmentGate(value, next)
|
|
if err != nil {
|
|
return work.DevelopmentGate{}, err
|
|
}
|
|
advanced.Revision = value.Revision + 1
|
|
advanced.UpdatedAtUnixMillis = now.UnixMilli()
|
|
return advanced, nil
|
|
}
|