From 1ccd43c5528f09badf7ca955bb02ce793eec17b1 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Sat, 22 Aug 2026 13:46:01 +0300 Subject: [PATCH] feat(work): add governed self-development gate --- contracts/work/self_development.go | 29 ++++++ dev/ACTIVE_TASK.json | 2 +- ...000137-governed-self-development-gate.json | 23 +++++ modules/work/domain/self_development.go | 97 +++++++++++++++++++ modules/work/domain/self_development_test.go | 28 ++++++ .../self_development_contract_test.go | 15 +++ 6 files changed, 193 insertions(+), 1 deletion(-) create mode 100644 contracts/work/self_development.go create mode 100644 dev/tasks/DEV-000137-governed-self-development-gate.json create mode 100644 modules/work/domain/self_development.go create mode 100644 modules/work/domain/self_development_test.go create mode 100644 tests/semantic_evolution/self_development_contract_test.go diff --git a/contracts/work/self_development.go b/contracts/work/self_development.go new file mode 100644 index 0000000..5e3b6ba --- /dev/null +++ b/contracts/work/self_development.go @@ -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 +} diff --git a/dev/ACTIVE_TASK.json b/dev/ACTIVE_TASK.json index e0ac6fc..56e69b3 100644 --- a/dev/ACTIVE_TASK.json +++ b/dev/ACTIVE_TASK.json @@ -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 } diff --git a/dev/tasks/DEV-000137-governed-self-development-gate.json b/dev/tasks/DEV-000137-governed-self-development-gate.json new file mode 100644 index 0000000..129b558 --- /dev/null +++ b/dev/tasks/DEV-000137-governed-self-development-gate.json @@ -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"] +} diff --git a/modules/work/domain/self_development.go b/modules/work/domain/self_development.go new file mode 100644 index 0000000..3fbac1f --- /dev/null +++ b/modules/work/domain/self_development.go @@ -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 +} diff --git a/modules/work/domain/self_development_test.go b/modules/work/domain/self_development_test.go new file mode 100644 index 0000000..da0e884 --- /dev/null +++ b/modules/work/domain/self_development_test.go @@ -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") + } +} diff --git a/tests/semantic_evolution/self_development_contract_test.go b/tests/semantic_evolution/self_development_contract_test.go new file mode 100644 index 0000000..b3de01c --- /dev/null +++ b/tests/semantic_evolution/self_development_contract_test.go @@ -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) + } +}