feat(work): add governed self-development gate
CI / test (push) Failing after 15s
CI / test (pull_request) Failing after 14s

This commit is contained in:
m
2026-08-22 13:46:01 +03:00
parent d38406fbc5
commit 1ccd43c552
6 changed files with 193 additions and 1 deletions
+29
View File
@@ -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 -1
View File
@@ -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"]
}
+97
View File
@@ -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)
}
}