diff --git a/cmd/core-runtime/development_gate.go b/cmd/core-runtime/development_gate.go new file mode 100644 index 0000000..1d28a82 --- /dev/null +++ b/cmd/core-runtime/development_gate.go @@ -0,0 +1,32 @@ +package main + +import ( + "errors" + "time" + + "git.cin.su/m/Assistant/contracts/work" + workdomain "git.cin.su/m/Assistant/modules/work/domain" +) + +var errDevelopmentGateCreate = errors.New("development gate must start at evidence collected with revision one") + +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 +} diff --git a/cmd/core-runtime/development_gate_test.go b/cmd/core-runtime/development_gate_test.go new file mode 100644 index 0000000..68bdd43 --- /dev/null +++ b/cmd/core-runtime/development_gate_test.go @@ -0,0 +1,41 @@ +package main + +import ( + "testing" + "time" + + "git.cin.su/m/Assistant/contracts/work" +) + +func TestDevelopmentGateControllerRequiresSequentialVerifiedEvidence(t *testing.T) { + now := time.UnixMilli(10) + gate, err := prepareDevelopmentGate(work.DevelopmentGate{ID: "gate-1", Revision: 1, Stage: work.DevelopmentEvidenceCollected, EvidenceRefs: []string{"evidence-1"}}, now) + if err != nil { + t.Fatal(err) + } + gate.ProposalRef = "proposal-1" + gate, err = advanceDevelopmentGate(gate, work.DevelopmentProposalCreated, now) + if err != nil || gate.Revision != 2 { + t.Fatalf("proposal transition failed: %#v %v", gate, err) + } + gate.TaskPacketRef = "task-1" + gate, err = advanceDevelopmentGate(gate, work.DevelopmentTaskAdmitted, now) + if err != nil { + t.Fatal(err) + } + gate.WorktreeRef = "isolated-worktree" + gate, err = advanceDevelopmentGate(gate, work.DevelopmentIsolated, now) + if err != nil { + t.Fatal(err) + } + gate.ProducerRunRefs = []work.AgentRunID{"producer"} + gate.VerifierRunRefs = []work.AgentRunID{"verifier"} + gate.VerificationEvidence = "verification-1" + gate, err = advanceDevelopmentGate(gate, work.DevelopmentVerified, now) + if err != nil { + t.Fatal(err) + } + if _, err := advanceDevelopmentGate(gate, work.DevelopmentPromoted, now); err == nil { + t.Fatal("promotion bypassed canary") + } +} diff --git a/cmd/core-runtime/main.go b/cmd/core-runtime/main.go index 5830d35..4517451 100644 --- a/cmd/core-runtime/main.go +++ b/cmd/core-runtime/main.go @@ -256,6 +256,61 @@ func main() { w.WriteHeader(http.StatusCreated) _ = json.NewEncoder(w).Encode(scaffold) }) + mux.HandleFunc("POST /v1/creator/development/gates", func(w http.ResponseWriter, r *http.Request) { + if _, ok := authenticateCreator(w, r); !ok { + return + } + var gate work.DevelopmentGate + decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 64<<10)) + decoder.DisallowUnknownFields() + if err := decoder.Decode(&gate); err != nil { + http.Error(w, "invalid development gate", http.StatusBadRequest) + return + } + gate, err := prepareDevelopmentGate(gate, time.Now()) + if err != nil { + http.Error(w, "development gate rejected", http.StatusUnprocessableEntity) + return + } + if err := workRepository.SaveDevelopmentGate(r.Context(), gate, 0); err != nil { + http.Error(w, "development gate conflict", http.StatusConflict) + return + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + _ = json.NewEncoder(w).Encode(gate) + }) + mux.HandleFunc("POST /v1/creator/development/gates/{id}/advance", func(w http.ResponseWriter, r *http.Request) { + if _, ok := authenticateCreator(w, r); !ok { + return + } + var request struct { + ExpectedRevision int64 `json:"expected_revision"` + NextStage work.DevelopmentStage `json:"next_stage"` + } + decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 16<<10)) + decoder.DisallowUnknownFields() + if err := decoder.Decode(&request); err != nil || request.ExpectedRevision < 1 || request.NextStage == "" { + http.Error(w, "invalid development gate transition", http.StatusBadRequest) + return + } + gate, err := workRepository.LoadDevelopmentGate(r.Context(), r.PathValue("id")) + if err != nil || gate.Revision != request.ExpectedRevision { + http.Error(w, "development gate revision conflict", http.StatusConflict) + return + } + gate, err = advanceDevelopmentGate(gate, request.NextStage, time.Now()) + if err != nil { + http.Error(w, "development gate transition rejected", http.StatusUnprocessableEntity) + return + } + if err := workRepository.SaveDevelopmentGate(r.Context(), gate, request.ExpectedRevision); err != nil { + http.Error(w, "development gate conflict", http.StatusConflict) + return + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(gate) + }) mux.HandleFunc("GET /v1/creator/focus", func(w http.ResponseWriter, r *http.Request) { session, ok := authenticateCreator(w, r) if !ok { diff --git a/dev/ACTIVE_TASK.json b/dev/ACTIVE_TASK.json index a721b02..a133578 100644 --- a/dev/ACTIVE_TASK.json +++ b/dev/ACTIVE_TASK.json @@ -1,5 +1,5 @@ { - "task_file": "dev/tasks/DEV-000229-research-runtime-bridge.json", - "task_id": "DEV-000229", + "task_file": "dev/tasks/DEV-000230-development-gate-controller.json", + "task_id": "DEV-000230", "task_revision": 1 } diff --git a/dev/tasks/DEV-000230-development-gate-controller.json b/dev/tasks/DEV-000230-development-gate-controller.json new file mode 100644 index 0000000..f81b0c8 --- /dev/null +++ b/dev/tasks/DEV-000230-development-gate-controller.json @@ -0,0 +1,23 @@ +{ + "task_id": "DEV-000230", + "task_revision": 1, + "architecture_revision": "ADR-0018", + "base_repo_revision": "ec79f19a7935e1eb8f0f59c4c9ce2940b97c4855", + "risk_tier": "E", + "intent": "Expose the existing Work-owned governed development state machine through authenticated optimistic-concurrency routes without executing code, changing protected source or bypassing promotion gates.", + "requirement_ids": ["governed self-development", "durable gate progression", "canary rollback control"], + "allowed_change_scope": {"modules": ["core-runtime", "governed-development"], "paths": ["cmd/core-runtime/main.go", "cmd/core-runtime/development_gate.go", "cmd/core-runtime/development_gate_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000230-development-gate-controller.json", "docs/PROJECT_STATUS.md"]}, + "forbidden_change_scope": {"paths": ["architecture/**", "security/**", "contracts/**", "database/**", "deploy/**", "LLM-local/**"]}, + "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": ["Work DevelopmentGate state machine", "authenticated Creator session", "optimistic revision persistence"], "may_change": ["authenticated Core gate controller routes"]}, + "semantic_owner": "Work governed development gate", + "canonical_state": ["Work-owned development gate and existing proposal/scaffold records"], + "state_transitions": ["EVIDENCE_COLLECTED -> PROPOSAL_CREATED -> TASK_ADMITTED -> ISOLATED -> VERIFIED -> CANARY -> PROMOTED", "CANARY -> ROLLED_BACK"], + "side_effects_and_privileges": ["authenticated durable gate creation/transition only; no checkout, code execution, secret access, production mutation or automatic promotion"], + "persistence_and_migrations": ["none; existing development_gates table and repository are reused"], + "failure_retry_restart": ["optimistic revision conflict is explicit; invalid or skipped transitions are rejected; rollback remains available only from canary"], + "assumptions": ["Creator authentication is the explicit governance boundary for these operator routes", "actual worktree execution and deployment remain separate controlled systems"], + "acceptance_scenarios": ["gate creation requires evidence-collected revision one", "sequential transitions require their typed references", "verified cannot jump directly to promoted", "stale revision is rejected", "canary rollback is allowed", "unauthenticated requests are denied"], + "required_tests": ["go test ./cmd/core-runtime/...", "go vet ./cmd/core-runtime/...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI", "production health smoke"], + "terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_SPEC_GAP"] +} diff --git a/docs/PROJECT_STATUS.md b/docs/PROJECT_STATUS.md index 41d1a5d..b042991 100644 --- a/docs/PROJECT_STATUS.md +++ b/docs/PROJECT_STATUS.md @@ -24,7 +24,7 @@ - `COMPLETION_MATRIX = docs/evidence/audit-completion-matrix-2026-08-23-v3.json`; all seven objective areas are mapped to current repository/production proof, with external bridge, GPU promotion/quality and deep-acceptance gaps explicit. -- `ACTIVE_TASK = dev/tasks/DEV-000229-research-runtime-bridge.json revision 1`; authenticated Core now has a bounded adapter route to the existing Truth/Research runtime. The epistemics owner remains responsible for persistence, freshness, citations and conflicts. +- `ACTIVE_TASK = dev/tasks/DEV-000230-development-gate-controller.json revision 1`; authenticated Core now exposes Work-owned durable development gate creation and optimistic-concurrency progression. Code execution, protected-main access, automatic promotion and production mutation remain outside this controller. - `LINEAGE_DECISION = docs/GOVERNANCE_LINEAGE_DECISION_2026-08-22.md`; historical packets remain provenance and are not current authority. - `REGISTRY_STATUS = YAML and JSON semantic mirrors agree` - `PRODUCTION_DOMAIN = docker.cin.su`; shared docker-test is not used for this project.