package swoosh import ( "crypto/sha256" "encoding/hex" "encoding/json" "errors" "fmt" "strings" ) var ( // ErrGuardRejected is returned when guard outcomes prevent a transition from applying. ErrGuardRejected = errors.New("guard rejected transition") // ErrUnknownTransition is returned when Reduce encounters an unhandled transition name. ErrUnknownTransition = errors.New("unknown transition") // ErrQuarantined indicates transitions are blocked while policy quarantine is active. ErrQuarantined = errors.New("transition blocked by quarantine") // ErrInvalidPhase signals an unexpected phase for the requested transition. ErrInvalidPhase = errors.New("invalid phase for transition") // ErrPrecondition signals a precondition failure unrelated to phases or guards. ErrPrecondition = errors.New("precondition failed") ) // Reduce applies a validated transition to the given state and returns a new state. // Reducer must remain deterministic and side-effect free. func Reduce(oldState OrchestratorState, t TransitionProposal, g GuardOutcome) (OrchestratorState, error) { newState := oldState changed, err := applyTransition(&newState, t, g) if err != nil { return OrchestratorState{}, err } if !changed { return oldState, nil } newState.HLCLast = t.HLC hash, err := computeStateHash(newState) if err != nil { return OrchestratorState{}, fmt.Errorf("compute state hash: %w", err) } newState.StateHash = hash return newState, nil } func applyTransition(state *OrchestratorState, t TransitionProposal, g GuardOutcome) (bool, error) { name := t.TransitionName switch name { case "": return false, fmt.Errorf("%w: empty transition name", ErrUnknownTransition) } if state.Policy.Quarantined && name != "TRANSITION_QUARANTINE_RELEASE" && name != "TRANSITION_QUARANTINE_CONFIRM_BLOCK" { return false, fmt.Errorf("%w: %s", ErrQuarantined, name) } switch name { case "BOOT_CONFIG_OK": if state.Boot.Licensed { return false, fmt.Errorf("%w: boot already licensed", ErrPrecondition) } return false, nil case "LICENSE_GRANTED": if state.Boot.Licensed { return false, nil } if !g.LicenseOK { return false, fmt.Errorf("%w: license check failed", ErrGuardRejected) } state.Boot.Licensed = true return true, nil case "INGESTION_SOURCES_RESOLVED": if state.Ingestion.Phase == "FETCH" && state.Ingestion.LastError == "" { return false, nil } if state.Ingestion.Phase != "" && state.Ingestion.Phase != "DISCOVER" { return false, fmt.Errorf("%w: ingestion phase %q", ErrInvalidPhase, state.Ingestion.Phase) } changed := false if state.Ingestion.Phase != "FETCH" { state.Ingestion.Phase = "FETCH" changed = true } if state.Ingestion.LastError != "" { state.Ingestion.LastError = "" changed = true } if changed { state.Ingestion.Epoch++ } return changed, nil case "INGESTION_BYTES_OK": if state.Ingestion.Phase == "VALIDATE" && state.Ingestion.LastError == "" { return false, nil } if state.Ingestion.Phase != "FETCH" { return false, fmt.Errorf("%w: ingestion phase %q", ErrInvalidPhase, state.Ingestion.Phase) } changed := false if state.Ingestion.Phase != "VALIDATE" { state.Ingestion.Phase = "VALIDATE" changed = true } if state.Ingestion.LastError != "" { state.Ingestion.LastError = "" changed = true } if changed { state.Ingestion.Epoch++ } return changed, nil case "INGESTION_SCHEMA_OK": if state.Ingestion.Phase == "INDEX" && state.Ingestion.LastError == "" { return false, nil } if state.Ingestion.Phase != "VALIDATE" { return false, fmt.Errorf("%w: ingestion phase %q", ErrInvalidPhase, state.Ingestion.Phase) } if !g.PolicyOK || !g.BackbeatOK { return false, fmt.Errorf("%w: policy=%t backbeat=%t", ErrGuardRejected, g.PolicyOK, g.BackbeatOK) } changed := false if state.Ingestion.Phase != "INDEX" { state.Ingestion.Phase = "INDEX" changed = true } if state.Ingestion.LastError != "" { state.Ingestion.LastError = "" changed = true } if changed { state.Ingestion.Epoch++ } return changed, nil case "INGESTION_CORPUS_BUILT": if state.Ingestion.Phase == "READY" && state.Ingestion.LastError == "" { return false, nil } if state.Ingestion.Phase != "INDEX" { return false, fmt.Errorf("%w: ingestion phase %q", ErrInvalidPhase, state.Ingestion.Phase) } changed := false if state.Ingestion.Phase != "READY" { state.Ingestion.Phase = "READY" changed = true } if state.Ingestion.LastError != "" { state.Ingestion.LastError = "" changed = true } if changed { state.Ingestion.Epoch++ } return changed, nil case "INGESTION_POLICY_VIOLATION": if g.PolicyOK { return false, fmt.Errorf("%w: policy guard true", ErrGuardRejected) } if state.Ingestion.Phase != "VALIDATE" && state.Ingestion.Phase != "INDEX" { return false, fmt.Errorf("%w: ingestion phase %q", ErrInvalidPhase, state.Ingestion.Phase) } rationale := joinRationale(g.Rationale) changed := false if !state.Policy.Quarantined { state.Policy.Quarantined = true changed = true } if state.Policy.Rationale != rationale { state.Policy.Rationale = rationale changed = true } if !state.Control.Degraded { state.Control.Degraded = true changed = true } if changed { state.Ingestion.Epoch++ } return changed, nil case "COUNCIL_PROFILES_LOADED": if state.Council.Phase == "ELECT" { return false, nil } if state.Council.Phase != "" && state.Council.Phase != "PLAN_ROLES" { return false, fmt.Errorf("%w: council phase %q", ErrInvalidPhase, state.Council.Phase) } state.Council.Phase = "ELECT" state.Council.Epoch++ return true, nil case "COUNCIL_QUORUM_CERT": if state.Council.Phase == "TOOLING_SYNC" { return false, nil } if state.Council.Phase != "ELECT" { return false, fmt.Errorf("%w: council phase %q", ErrInvalidPhase, state.Council.Phase) } if !g.QuorumOK || !g.PolicyOK { return false, fmt.Errorf("%w: quorum=%t policy=%t", ErrGuardRejected, g.QuorumOK, g.PolicyOK) } state.Council.Phase = "TOOLING_SYNC" state.Council.Epoch++ return true, nil case "COUNCIL_MCP_GREEN": if state.Council.Phase == "READY" && state.Council.MCPHealthGreen { return false, nil } if state.Council.Phase != "TOOLING_SYNC" { return false, fmt.Errorf("%w: council phase %q", ErrInvalidPhase, state.Council.Phase) } if !g.MCPHealthy { return false, fmt.Errorf("%w: mcp unhealthy", ErrGuardRejected) } changed := false if state.Council.Phase != "READY" { state.Council.Phase = "READY" changed = true } if !state.Council.MCPHealthGreen { state.Council.MCPHealthGreen = true changed = true } if changed { state.Council.Epoch++ } return changed, nil case "ENV_CAPACITY_OK": if state.Environment.Phase == "PROVISION" && state.Environment.CapacityOK && state.Environment.Health == "" { return false, nil } if state.Environment.Phase != "" && state.Environment.Phase != "ALLOCATE" { return false, fmt.Errorf("%w: environment phase %q", ErrInvalidPhase, state.Environment.Phase) } if !g.BackbeatOK { return false, fmt.Errorf("%w: backbeat guard false", ErrGuardRejected) } changed := false if state.Environment.Phase != "PROVISION" { state.Environment.Phase = "PROVISION" changed = true } if !state.Environment.CapacityOK { state.Environment.CapacityOK = true changed = true } if state.Environment.Health != "" { state.Environment.Health = "" changed = true } if changed { state.Environment.Epoch++ } return changed, nil case "ENV_INSTALLED": if state.Environment.Phase == "HEALTHCHECK" { return false, nil } if state.Environment.Phase != "PROVISION" { return false, fmt.Errorf("%w: environment phase %q", ErrInvalidPhase, state.Environment.Phase) } state.Environment.Phase = "HEALTHCHECK" state.Environment.Epoch++ return true, nil case "ENV_HEALTH_GREEN": if state.Environment.Phase == "READY" && state.Environment.Health == "green" && !state.Control.Degraded { return false, nil } if state.Environment.Phase != "HEALTHCHECK" { return false, fmt.Errorf("%w: environment phase %q", ErrInvalidPhase, state.Environment.Phase) } changed := false if state.Environment.Phase != "READY" { state.Environment.Phase = "READY" changed = true } if state.Environment.Health != "green" { state.Environment.Health = "green" changed = true } if state.Control.Degraded { state.Control.Degraded = false changed = true } if changed { state.Environment.Epoch++ } return changed, nil case "ENV_HEALTH_AMBER": if state.Environment.Phase == "DEGRADED" && state.Environment.Health == "amber" && state.Control.Degraded { return false, nil } if state.Environment.Phase != "HEALTHCHECK" { return false, fmt.Errorf("%w: environment phase %q", ErrInvalidPhase, state.Environment.Phase) } changed := false if state.Environment.Phase != "DEGRADED" { state.Environment.Phase = "DEGRADED" changed = true } if state.Environment.Health != "amber" { state.Environment.Health = "amber" changed = true } if !state.Control.Degraded { state.Control.Degraded = true changed = true } if changed { state.Environment.Epoch++ } return changed, nil case "ENV_RECOVERED_GREEN": if state.Environment.Phase == "READY" && state.Environment.Health == "green" && !state.Control.Degraded { return false, nil } if state.Environment.Phase != "DEGRADED" { return false, fmt.Errorf("%w: environment phase %q", ErrInvalidPhase, state.Environment.Phase) } if !g.MCPHealthy { return false, fmt.Errorf("%w: mcp unhealthy", ErrGuardRejected) } changed := false if state.Environment.Phase != "READY" { state.Environment.Phase = "READY" changed = true } if state.Environment.Health != "green" { state.Environment.Health = "green" changed = true } if state.Control.Degraded { state.Control.Degraded = false changed = true } if changed { state.Environment.Epoch++ } return changed, nil case "EXEC_PLAN_LOCKED": if state.Execution.Phase == "WORK" && state.Execution.PlanLocked && state.Execution.ActiveWindowID == t.WindowID { return false, nil } if state.Execution.Phase != "" && state.Execution.Phase != "PLAN" { return false, fmt.Errorf("%w: execution phase %q", ErrInvalidPhase, state.Execution.Phase) } if !g.BackbeatOK { return false, fmt.Errorf("%w: backbeat guard false", ErrGuardRejected) } if state.Ingestion.Phase != "READY" || state.Council.Phase != "READY" || state.Environment.Phase != "READY" { return false, fmt.Errorf("%w: prerequisites not ready", ErrPrecondition) } changed := false if state.Execution.Phase != "WORK" { state.Execution.Phase = "WORK" changed = true } if !state.Execution.PlanLocked { state.Execution.PlanLocked = true changed = true } if state.Execution.ActiveWindowID != t.WindowID { state.Execution.ActiveWindowID = t.WindowID changed = true } if changed { state.Execution.Epoch++ } return changed, nil case "EXEC_BEAT_REVIEW_GATE": if state.Execution.Phase == "REVIEW" { return false, nil } if state.Execution.Phase != "WORK" { return false, fmt.Errorf("%w: execution phase %q", ErrInvalidPhase, state.Execution.Phase) } if !g.BackbeatOK { return false, fmt.Errorf("%w: backbeat guard false", ErrGuardRejected) } state.Execution.Phase = "REVIEW" state.Execution.PlanLocked = true state.Execution.Epoch++ return true, nil case "EXEC_APPROVALS_THRESHOLD": if state.Execution.Phase == "REVERB" { return false, nil } if state.Execution.Phase != "REVIEW" { return false, fmt.Errorf("%w: execution phase %q", ErrInvalidPhase, state.Execution.Phase) } if !g.QuorumOK || !g.PolicyOK { return false, fmt.Errorf("%w: quorum=%t policy=%t", ErrGuardRejected, g.QuorumOK, g.PolicyOK) } state.Execution.Phase = "REVERB" state.Execution.Epoch++ return true, nil case "EXEC_CHANGES_REQUESTED": if state.Execution.Phase == "WORK" { return false, nil } if state.Execution.Phase != "REVIEW" { return false, fmt.Errorf("%w: execution phase %q", ErrInvalidPhase, state.Execution.Phase) } if !g.PolicyOK { return false, fmt.Errorf("%w: policy guard false", ErrGuardRejected) } state.Execution.Phase = "WORK" state.Execution.PlanLocked = true state.Execution.Epoch++ return true, nil case "EXEC_NEXT_WINDOW": if state.Execution.Phase == "PLAN" && !state.Execution.PlanLocked && state.Execution.ActiveWindowID == t.WindowID && state.Execution.Approvals == 0 { return false, nil } if state.Execution.Phase != "REVERB" { return false, fmt.Errorf("%w: execution phase %q", ErrInvalidPhase, state.Execution.Phase) } if !g.BackbeatOK { return false, fmt.Errorf("%w: backbeat guard false", ErrGuardRejected) } changed := false if state.Execution.Phase != "PLAN" { state.Execution.Phase = "PLAN" changed = true } if state.Execution.PlanLocked { state.Execution.PlanLocked = false changed = true } if state.Execution.ActiveWindowID != t.WindowID { state.Execution.ActiveWindowID = t.WindowID changed = true } if state.Execution.Approvals != 0 { state.Execution.Approvals = 0 changed = true } if changed { state.Execution.Epoch++ } return changed, nil case "CONTROL_PAUSE": needPause := !state.Control.Paused needRecoverReset := state.Control.Recovering if !needPause && !needRecoverReset { return false, nil } if !g.PolicyOK { return false, fmt.Errorf("%w: policy guard false", ErrGuardRejected) } if needPause { state.Control.Paused = true } if needRecoverReset { state.Control.Recovering = false } return true, nil case "CONTROL_RESUME": if !state.Control.Paused { return false, fmt.Errorf("%w: system not paused", ErrPrecondition) } if !g.PolicyOK { return false, fmt.Errorf("%w: policy guard false", ErrGuardRejected) } changed := false if state.Control.Paused { state.Control.Paused = false changed = true } if state.Control.Recovering { state.Control.Recovering = false changed = true } return changed, nil case "CONTROL_RECOVERY_ENTER": if state.Control.Recovering && state.Control.Degraded { return false, nil } if g.QuorumOK && !state.Control.Degraded { return false, fmt.Errorf("%w: no recovery trigger", ErrGuardRejected) } changed := false if !state.Control.Recovering { state.Control.Recovering = true changed = true } if !state.Control.Degraded { state.Control.Degraded = true changed = true } return changed, nil case "CONTROL_RECOVERY_RESOLVED": if !state.Control.Recovering { return false, fmt.Errorf("%w: not in recovery", ErrPrecondition) } if !g.QuorumOK && state.Environment.Health != "green" { return false, fmt.Errorf("%w: recovery not satisfied", ErrGuardRejected) } changed := false if state.Control.Recovering { state.Control.Recovering = false changed = true } if state.Control.Degraded { state.Control.Degraded = false changed = true } return changed, nil case "TRANSITION_QUARANTINE_ENTER": if g.PolicyOK { return false, fmt.Errorf("%w: policy guard true", ErrGuardRejected) } rationale := joinRationale(g.Rationale) changed := false if !state.Policy.Quarantined { state.Policy.Quarantined = true changed = true } if state.Policy.Rationale != rationale { state.Policy.Rationale = rationale changed = true } if !state.Control.Paused { state.Control.Paused = true changed = true } if state.Control.Recovering { state.Control.Recovering = false changed = true } if !state.Control.Degraded { state.Control.Degraded = true changed = true } return changed, nil case "TRANSITION_QUARANTINE_RELEASE": if !state.Policy.Quarantined { return false, fmt.Errorf("%w: not quarantined", ErrPrecondition) } if !g.PolicyOK { return false, fmt.Errorf("%w: policy guard false", ErrGuardRejected) } changed := false if state.Policy.Quarantined { state.Policy.Quarantined = false changed = true } if state.Control.Paused { state.Control.Paused = false changed = true } if state.Control.Degraded { state.Control.Degraded = false changed = true } if state.Control.Recovering { state.Control.Recovering = false changed = true } return changed, nil case "TRANSITION_QUARANTINE_CONFIRM_BLOCK": if !state.Policy.Quarantined { return false, fmt.Errorf("%w: not quarantined", ErrPrecondition) } changed := false if !state.Control.Paused { state.Control.Paused = true changed = true } if !state.Control.Degraded { state.Control.Degraded = true changed = true } if state.Control.Recovering { state.Control.Recovering = false changed = true } return changed, nil default: return false, fmt.Errorf("%w: %s", ErrUnknownTransition, name) } } func joinRationale(r []string) string { if len(r) == 0 { return "" } return strings.Join(r, "; ") } // ComputeStateHash calculates the SHA256 hash of the canonical JSON representation. // This is exported for use by main.go during genesis state initialization. func ComputeStateHash(state OrchestratorState) (string, error) { payload, err := canonicalJSON(state) if err != nil { return "", err } sum := sha256.Sum256(payload) return hex.EncodeToString(sum[:]), nil } // computeStateHash is the internal wrapper (for backwards compatibility) func computeStateHash(state OrchestratorState) (string, error) { return ComputeStateHash(state) } func canonicalJSON(value any) ([]byte, error) { buf, err := json.Marshal(value) if err != nil { return nil, err } return buf, nil }