Files
m c60a575fda
CI / test (push) Successful in 1m3s
CI / test (pull_request) Successful in 57s
feat: add isolated governed scaffold candidate
2026-08-23 01:36:03 +03:00

234 lines
9.3 KiB
Go

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")
var ErrInvalidDevelopmentScaffold = errors.New("invalid development scaffold candidate")
// 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
}
// BuildDevelopmentScaffold creates only a typed, isolated candidate. The
// caller supplies the protected path set so this domain function cannot infer
// or widen repository authority from ambient process state.
func BuildDevelopmentScaffold(request work.DevelopmentScaffoldRequest) (work.DevelopmentScaffold, error) {
modules := cleanStrings(request.Modules)
allowed := cleanPaths(request.AllowedPaths)
forbidden := cleanPaths(request.ForbiddenPaths)
protected := cleanPaths(request.ProtectedPaths)
tests := cleanStrings(request.RequiredTests)
lineage := cleanStrings(request.ArtifactLineageRefs)
if request.ID == "" || request.ProposalRef == "" || strings.TrimSpace(request.BaseRepoRevision) == "" || strings.TrimSpace(request.WorktreeRef) == "" || strings.TrimSpace(request.TaskIntent) == "" || request.CreatedAt.IsZero() || len(modules) == 0 || len(allowed) == 0 || len(tests) == 0 || len(lineage) == 0 || request.ProtectedMainAccess != "DENIED" || request.ProductionAccess != "DENIED" || request.SecretAccess != "DENIED" {
return work.DevelopmentScaffold{}, ErrInvalidDevelopmentScaffold
}
for _, path := range allowed {
if unsafePath(path) || pathMatchesAny(path, forbidden) || pathMatchesAny(path, protected) {
return work.DevelopmentScaffold{}, ErrInvalidDevelopmentScaffold
}
}
worktree := strings.ToLower(strings.TrimSpace(request.WorktreeRef))
if worktree == "main" || worktree == "protected-main" || worktree == "production" {
return work.DevelopmentScaffold{}, ErrInvalidDevelopmentScaffold
}
return work.DevelopmentScaffold{ID: request.ID, Stage: work.DevelopmentScaffoldSandboxed, ProposalRef: request.ProposalRef, BaseRepoRevision: strings.TrimSpace(request.BaseRepoRevision), WorktreeRef: strings.TrimSpace(request.WorktreeRef), TaskIntent: strings.TrimSpace(request.TaskIntent), Modules: modules, AllowedPaths: allowed, ForbiddenPaths: forbidden, RequiredTests: tests, ArtifactLineageRefs: lineage, ProtectedMainAccess: "DENIED", ProductionAccess: "DENIED", SecretAccess: "DENIED", CreatedAtUnixMillis: request.CreatedAt.UnixMilli()}, nil
}
func cleanPaths(values []string) []string {
result := make([]string, 0, len(values))
seen := make(map[string]struct{}, len(values))
for _, value := range values {
value = strings.TrimSpace(strings.ReplaceAll(value, "\\", "/"))
if value == "" {
continue
}
if _, ok := seen[value]; ok {
continue
}
seen[value] = struct{}{}
result = append(result, value)
}
return result
}
func unsafePath(path string) bool {
return strings.HasPrefix(path, "/") || strings.HasPrefix(path, "~") || (len(path) > 1 && path[1] == ':') || slices.Contains(strings.Split(path, "/"), "..")
}
func pathMatchesAny(path string, scopes []string) bool {
for _, scope := range scopes {
scope = strings.TrimSuffix(scope, "/")
scope = strings.TrimSuffix(scope, "**")
scope = strings.TrimSuffix(scope, "/")
if path == scope || strings.HasPrefix(path, scope+"/") {
return true
}
}
return false
}
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
}