feat(work): add governed self-development gate
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
package work
|
||||
|
||||
type DevelopmentStage string
|
||||
|
||||
const (
|
||||
DevelopmentEvidenceCollected DevelopmentStage = "EVIDENCE_COLLECTED"
|
||||
DevelopmentProposalCreated DevelopmentStage = "PROPOSAL_CREATED"
|
||||
DevelopmentTaskAdmitted DevelopmentStage = "TASK_ADMITTED"
|
||||
DevelopmentIsolated DevelopmentStage = "ISOLATED"
|
||||
DevelopmentVerified DevelopmentStage = "VERIFIED"
|
||||
DevelopmentCanary DevelopmentStage = "CANARY"
|
||||
DevelopmentPromoted DevelopmentStage = "PROMOTED"
|
||||
DevelopmentRolledBack DevelopmentStage = "ROLLED_BACK"
|
||||
)
|
||||
|
||||
type DevelopmentGate struct {
|
||||
ID string
|
||||
Stage DevelopmentStage
|
||||
EvidenceRefs []string
|
||||
ProposalRef string
|
||||
TaskPacketRef string
|
||||
WorktreeRef string
|
||||
ProducerRunRefs []AgentRunID
|
||||
VerifierRunRefs []AgentRunID
|
||||
VerificationEvidence string
|
||||
CanaryEvidence string
|
||||
PromotionRef string
|
||||
RollbackRef string
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"task_file": "dev/tasks/DEV-000134-truth-bundle-compiler.json",
|
||||
"task_id": "DEV-000136",
|
||||
"task_id": "DEV-000137",
|
||||
"task_revision": 1
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000137",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "d38406f",
|
||||
"risk_tier": "E",
|
||||
"intent": "Add typed governed self-development gate requiring evidence, task packet, isolated worktree, independent verifier, canary, promotion or rollback.",
|
||||
"requirement_ids": ["WP-701", "Governed self-development", "Independent verification", "Rollback"],
|
||||
"allowed_change_scope": {"modules": ["work-agent-execution"], "paths": ["contracts/work/self_development.go", "modules/work/domain/self_development.go", "modules/work/domain/self_development_test.go", "tests/semantic_evolution/self_development_contract_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000137-governed-self-development-gate.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "deploy/**", "database/**", "cmd/**", "contracts/authority/**", ".gitea/**"]},
|
||||
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 0},
|
||||
"contracts": {"consume": ["Existing AgentRun and work graph contracts"], "may_change": ["Typed self-development stage gate"]},
|
||||
"semantic_owner": "work-agent-execution",
|
||||
"canonical_state": ["DevelopmentGate value object"],
|
||||
"state_transitions": ["evidence to proposal to task to isolation to independent verification to canary to promotion; canary may rollback"],
|
||||
"side_effects_and_privileges": ["No direct code mutation, provider invocation, or promotion side effect"],
|
||||
"persistence_and_migrations": ["Value object only; durable gate persistence follows execution-plane integration"],
|
||||
"failure_retry_restart": ["Invalid stage transitions and missing evidence fail closed"],
|
||||
"assumptions": ["Task packet and worktree identity are provided by the execution plane"],
|
||||
"acceptance_scenarios": ["same producer cannot verify output", "promotion is terminal", "canary can rollback"],
|
||||
"required_tests": ["go test ./...", "go vet ./...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"slices"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/work"
|
||||
)
|
||||
|
||||
var ErrInvalidDevelopmentGate = errors.New("invalid governed self-development gate")
|
||||
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/work"
|
||||
)
|
||||
|
||||
func TestGovernedSelfDevelopmentRequiresIndependentVerifierBeforeCanary(t *testing.T) {
|
||||
gate := work.DevelopmentGate{ID: "dev-1", Stage: work.DevelopmentEvidenceCollected, EvidenceRefs: []string{"obs-1"}, ProposalRef: "proposal-1", TaskPacketRef: "task-1", WorktreeRef: "worktree-1", ProducerRunRefs: []work.AgentRunID{"producer"}, VerifierRunRefs: []work.AgentRunID{"verifier"}, VerificationEvidence: "verify-1", CanaryEvidence: "canary-1", PromotionRef: "promotion-1"}
|
||||
for _, stage := range []work.DevelopmentStage{work.DevelopmentProposalCreated, work.DevelopmentTaskAdmitted, work.DevelopmentIsolated, work.DevelopmentVerified, work.DevelopmentCanary, work.DevelopmentPromoted} {
|
||||
var err error
|
||||
gate, err = AdvanceDevelopmentGate(gate, stage)
|
||||
if err != nil {
|
||||
t.Fatalf("stage %s: %v", stage, err)
|
||||
}
|
||||
}
|
||||
if _, err := AdvanceDevelopmentGate(gate, work.DevelopmentRolledBack); err == nil {
|
||||
t.Fatal("promoted gate must be terminal")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGovernedSelfDevelopmentRejectsSameProducerAndVerifier(t *testing.T) {
|
||||
gate := work.DevelopmentGate{ID: "dev-1", Stage: work.DevelopmentIsolated, EvidenceRefs: []string{"obs-1"}, ProducerRunRefs: []work.AgentRunID{"same"}, VerifierRunRefs: []work.AgentRunID{"same"}, VerificationEvidence: "verify-1"}
|
||||
if _, err := AdvanceDevelopmentGate(gate, work.DevelopmentVerified); err == nil {
|
||||
t.Fatal("same run cannot verify its own output")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package semantic_evolution_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/work"
|
||||
workdomain "git.cin.su/m/Assistant/modules/work/domain"
|
||||
)
|
||||
|
||||
func TestSelfDevelopmentRollbackIsAvailableOnlyFromCanary(t *testing.T) {
|
||||
gate := work.DevelopmentGate{ID: "dev-1", Stage: work.DevelopmentCanary, EvidenceRefs: []string{"obs"}, CanaryEvidence: "canary", RollbackRef: "rollback"}
|
||||
if _, err := workdomain.AdvanceDevelopmentGate(gate, work.DevelopmentRolledBack); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user