394 lines
20 KiB
Go
394 lines
20 KiB
Go
package service
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"browser.local/platform/domain"
|
|
"browser.local/platform/repo"
|
|
)
|
|
|
|
type scumWorkflowTemplateDefinition struct {
|
|
Key string
|
|
Title string
|
|
Steps []scumWorkflowStepDefinition
|
|
}
|
|
|
|
type scumWorkflowStepDefinition struct {
|
|
Key string
|
|
DependsOn []string
|
|
OperationKey string
|
|
QueryTemplateKey string
|
|
Capability string
|
|
TargetKey string
|
|
MutatesState bool
|
|
MaxAttempts int
|
|
Summary string
|
|
}
|
|
|
|
func (svc *CoreService) CreateSCUMWorkflowForSession(sessionID, serverID string, request domain.SCUMWorkflowInstance) (domain.SCUMWorkflowInstance, error) {
|
|
request = domain.CopySCUMWorkflowInstance(request)
|
|
user, err := svc.GetCurrentUser(sessionID)
|
|
if err != nil {
|
|
return domain.SCUMWorkflowInstance{}, err
|
|
}
|
|
if err := svc.authorizeServerLifecycle(sessionID, serverID); err != nil {
|
|
return domain.SCUMWorkflowInstance{}, err
|
|
}
|
|
instance, err := svc.store.ServerInstances().Get(serverID)
|
|
if err != nil {
|
|
return domain.SCUMWorkflowInstance{}, err
|
|
}
|
|
plugin, err := svc.store.GamePlugins().Get(instance.PluginID)
|
|
if err != nil {
|
|
return domain.SCUMWorkflowInstance{}, err
|
|
}
|
|
template, ok := scumWorkflowTemplates()[request.TemplateKey]
|
|
if !ok {
|
|
return domain.SCUMWorkflowInstance{}, validationError("SCUM workflow template is not declared")
|
|
}
|
|
if strings.TrimSpace(request.IdempotencyKey) == "" || len(request.IdempotencyKey) > 120 {
|
|
return domain.SCUMWorkflowInstance{}, validationError("workflow idempotency key is required")
|
|
}
|
|
if existing, err := svc.store.SCUMWorkflowInstances().List(domain.SCUMWorkflowInstanceFilter{ServerInstanceID: serverID, IdempotencyKey: request.IdempotencyKey}); err == nil && len(existing) > 0 {
|
|
return domain.CopySCUMWorkflowInstance(existing[0]), nil
|
|
} else if err != nil {
|
|
return domain.SCUMWorkflowInstance{}, err
|
|
}
|
|
stamp := svc.now()
|
|
workflow := domain.SCUMWorkflowInstance{ID: "scum-workflow-" + fingerprintID(serverID, request.IdempotencyKey), ServerInstanceID: serverID, PluginID: plugin.ID, TemplateKey: template.Key, RequestedBy: user.ID, IdempotencyKey: request.IdempotencyKey, Status: domain.SCUMWorkflowQueued, Input: domain.CopyGameClientBridgePayload(request.Input), SafeSummary: domain.SCUMSafeSummary{Title: template.Title, Message: "SCUM workflow queued with typed steps and safe summaries."}, CreatedAt: stamp, UpdatedAt: stamp}
|
|
if err := svc.store.SCUMWorkflowInstances().Create(workflow); err != nil {
|
|
return domain.SCUMWorkflowInstance{}, err
|
|
}
|
|
for index, step := range template.Steps {
|
|
maxAttempts := step.MaxAttempts
|
|
if maxAttempts == 0 {
|
|
maxAttempts = 1
|
|
}
|
|
record := domain.SCUMWorkflowStep{ID: fmt.Sprintf("%s.step.%02d.%s", workflow.ID, index+1, step.Key), WorkflowID: workflow.ID, ServerInstanceID: serverID, StepKey: step.Key, DependsOn: domain.CopyStringSlice(step.DependsOn), Status: domain.SCUMWorkflowStepQueued, OperationKey: step.OperationKey, QueryTemplateKey: step.QueryTemplateKey, Capability: step.Capability, TargetKey: step.TargetKey, MaxAttempts: maxAttempts, MutatesState: step.MutatesState, SafeSummary: domain.SCUMSafeSummary{Title: step.Key, Message: step.Summary}, CreatedAt: stamp, UpdatedAt: stamp}
|
|
if err := svc.store.SCUMWorkflowSteps().Create(record); err != nil {
|
|
return domain.SCUMWorkflowInstance{}, err
|
|
}
|
|
}
|
|
_, err = svc.recordAuditEventWithID(user.ID, "scum.workflow.create", "scum-workflow", workflow.ID, domain.AuditResultQueued, "typed SCUM workflow queued")
|
|
return domain.CopySCUMWorkflowInstance(workflow), err
|
|
}
|
|
|
|
func (svc *CoreService) ListSCUMWorkflowsForSession(sessionID string, filter domain.SCUMWorkflowInstanceFilter) ([]domain.SCUMWorkflowInstance, error) {
|
|
if err := svc.authorizeServerLifecycle(sessionID, filter.ServerInstanceID); err != nil {
|
|
return nil, err
|
|
}
|
|
values, err := svc.store.SCUMWorkflowInstances().List(filter)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
limitSCUMProjectionSlice(&values, filter.Limit)
|
|
return values, nil
|
|
}
|
|
|
|
func (svc *CoreService) ListSCUMWorkflowStepsForSession(sessionID string, filter domain.SCUMWorkflowStepFilter) ([]domain.SCUMWorkflowStep, error) {
|
|
if err := svc.authorizeServerLifecycle(sessionID, filter.ServerInstanceID); err != nil {
|
|
return nil, err
|
|
}
|
|
values, err := svc.store.SCUMWorkflowSteps().List(filter)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
limitSCUMProjectionSlice(&values, filter.Limit)
|
|
return values, nil
|
|
}
|
|
|
|
func (svc *CoreService) DispatchNextSCUMWorkflowSteps(serverID string, limit int) ([]domain.SCUMWorkflowStep, error) {
|
|
if limit <= 0 {
|
|
limit = 1
|
|
}
|
|
workflows, err := svc.store.SCUMWorkflowInstances().List(domain.SCUMWorkflowInstanceFilter{ServerInstanceID: serverID})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
sort.SliceStable(workflows, func(i, j int) bool {
|
|
if workflows[i].CreatedAt.Equal(workflows[j].CreatedAt) {
|
|
return workflows[i].IdempotencyKey < workflows[j].IdempotencyKey
|
|
}
|
|
return workflows[i].CreatedAt.Before(workflows[j].CreatedAt)
|
|
})
|
|
dispatched := []domain.SCUMWorkflowStep{}
|
|
activeMutating, err := svc.hasActiveSCUMMutatingStep(serverID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, workflow := range workflows {
|
|
if !scumWorkflowRunnable(workflow.Status) || len(dispatched) >= limit {
|
|
continue
|
|
}
|
|
steps, err := svc.sortedSCUMWorkflowSteps(workflow.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, step := range steps {
|
|
if len(dispatched) >= limit || !scumWorkflowStepRunnable(step.Status) || !scumWorkflowDependenciesConfirmed(step, steps) {
|
|
continue
|
|
}
|
|
if step.MutatesState && activeMutating {
|
|
return dispatched, nil
|
|
}
|
|
if blocked, err := svc.blockSCUMStepIfRunUnavailable(workflow, step); err != nil || blocked.ID != "" {
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
dispatched = append(dispatched, blocked)
|
|
return dispatched, nil
|
|
}
|
|
step.Status = domain.SCUMWorkflowStepRunning
|
|
step.Attempt++
|
|
step.UpdatedAt = svc.now()
|
|
if err := svc.store.SCUMWorkflowSteps().Update(step); err != nil {
|
|
return nil, err
|
|
}
|
|
workflow.Status = domain.SCUMWorkflowRunning
|
|
workflow.CurrentStepKey = step.StepKey
|
|
workflow.UpdatedAt = step.UpdatedAt
|
|
if err := svc.store.SCUMWorkflowInstances().Update(workflow); err != nil {
|
|
return nil, err
|
|
}
|
|
dispatched = append(dispatched, domain.CopySCUMWorkflowStep(step))
|
|
if step.MutatesState {
|
|
activeMutating = true
|
|
return dispatched, nil
|
|
}
|
|
}
|
|
}
|
|
return dispatched, nil
|
|
}
|
|
|
|
func (svc *CoreService) CompleteSCUMWorkflowStep(stepID string, status domain.SCUMWorkflowStepStatus, confirmation domain.SCUMOperationConfirmation) (domain.SCUMWorkflowInstance, error) {
|
|
step, err := svc.store.SCUMWorkflowSteps().Get(stepID)
|
|
if err != nil {
|
|
return domain.SCUMWorkflowInstance{}, err
|
|
}
|
|
workflow, err := svc.store.SCUMWorkflowInstances().Get(step.WorkflowID)
|
|
if err != nil {
|
|
return domain.SCUMWorkflowInstance{}, err
|
|
}
|
|
if !scumWorkflowStepTerminal(status) {
|
|
return domain.SCUMWorkflowInstance{}, validationError("SCUM workflow step completion status must be terminal")
|
|
}
|
|
stamp := svc.now()
|
|
step.Status = status
|
|
step.Confirmation = domain.CopySCUMOperationConfirmation(confirmation)
|
|
step.CompletedAt = stamp
|
|
step.UpdatedAt = stamp
|
|
if err := svc.store.SCUMWorkflowSteps().Update(step); err != nil {
|
|
return domain.SCUMWorkflowInstance{}, err
|
|
}
|
|
return svc.refreshSCUMWorkflowStatus(workflow)
|
|
}
|
|
|
|
func (svc *CoreService) RetrySCUMWorkflowStep(stepID string) (domain.SCUMWorkflowStep, error) {
|
|
step, err := svc.store.SCUMWorkflowSteps().Get(stepID)
|
|
if err != nil {
|
|
return domain.SCUMWorkflowStep{}, err
|
|
}
|
|
workflow, err := svc.store.SCUMWorkflowInstances().Get(step.WorkflowID)
|
|
if err != nil {
|
|
return domain.SCUMWorkflowStep{}, err
|
|
}
|
|
if step.Attempt >= step.MaxAttempts {
|
|
return domain.SCUMWorkflowStep{}, validationError("SCUM workflow step retry limit reached")
|
|
}
|
|
if step.MutatesState && step.Status == domain.SCUMWorkflowStepUnknown && step.Confirmation.Status != "confirmed" {
|
|
step.SafeSummary = domain.SCUMSafeSummary{Title: "确认后才能重试", Message: "State-changing SCUM step is unknown; workflow must run confirmation/readback before retry to avoid duplicate effects."}
|
|
step.UpdatedAt = svc.now()
|
|
if err := svc.store.SCUMWorkflowSteps().Update(step); err != nil {
|
|
return domain.SCUMWorkflowStep{}, err
|
|
}
|
|
return domain.CopySCUMWorkflowStep(step), nil
|
|
}
|
|
step.Status = domain.SCUMWorkflowStepQueued
|
|
step.Confirmation = domain.SCUMOperationConfirmation{}
|
|
step.CompletedAt = time.Time{}
|
|
step.UpdatedAt = svc.now()
|
|
if err := svc.store.SCUMWorkflowSteps().Update(step); err != nil {
|
|
return domain.SCUMWorkflowStep{}, err
|
|
}
|
|
workflow.Status = domain.SCUMWorkflowQueued
|
|
workflow.BlockerReason = ""
|
|
workflow.UpdatedAt = step.UpdatedAt
|
|
if err := svc.store.SCUMWorkflowInstances().Update(workflow); err != nil {
|
|
return domain.SCUMWorkflowStep{}, err
|
|
}
|
|
return domain.CopySCUMWorkflowStep(step), nil
|
|
}
|
|
|
|
func (svc *CoreService) sortedSCUMWorkflowSteps(workflowID string) ([]domain.SCUMWorkflowStep, error) {
|
|
steps, err := svc.store.SCUMWorkflowSteps().List(domain.SCUMWorkflowStepFilter{WorkflowID: workflowID})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
sort.SliceStable(steps, func(i, j int) bool {
|
|
if steps[i].CreatedAt.Equal(steps[j].CreatedAt) {
|
|
return steps[i].ID < steps[j].ID
|
|
}
|
|
return steps[i].CreatedAt.Before(steps[j].CreatedAt)
|
|
})
|
|
return steps, nil
|
|
}
|
|
|
|
func (svc *CoreService) hasActiveSCUMMutatingStep(serverID string) (bool, error) {
|
|
mutates := true
|
|
for _, status := range []domain.SCUMWorkflowStepStatus{domain.SCUMWorkflowStepRunning, domain.SCUMWorkflowStepConfirming} {
|
|
steps, err := svc.store.SCUMWorkflowSteps().List(domain.SCUMWorkflowStepFilter{ServerInstanceID: serverID, Status: status, MutatesState: &mutates})
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if len(steps) > 0 {
|
|
return true, nil
|
|
}
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
func (svc *CoreService) blockSCUMStepIfRunUnavailable(workflow domain.SCUMWorkflowInstance, step domain.SCUMWorkflowStep) (domain.SCUMWorkflowStep, error) {
|
|
if strings.TrimSpace(step.Capability) == "" {
|
|
return domain.SCUMWorkflowStep{}, nil
|
|
}
|
|
instance, err := svc.store.ServerInstances().Get(workflow.ServerInstanceID)
|
|
if err != nil {
|
|
return domain.SCUMWorkflowStep{}, err
|
|
}
|
|
endpoint, err := svc.store.RunEndpoints().Get(instance.RunEndpointID)
|
|
if err != nil {
|
|
if err == repo.ErrNotFound {
|
|
return svc.blockSCUMWorkflowStep(workflow, step, "Run unavailable", "No bound run endpoint is available for this typed SCUM workflow step.")
|
|
}
|
|
return domain.SCUMWorkflowStep{}, err
|
|
}
|
|
if err := svc.validateRunnableEndpoint(endpoint, step.Capability); err != nil {
|
|
return svc.blockSCUMWorkflowStep(workflow, step, "Run unavailable", "Bound run cannot currently claim the declared workflow capability.")
|
|
}
|
|
return domain.SCUMWorkflowStep{}, nil
|
|
}
|
|
|
|
func (svc *CoreService) blockSCUMWorkflowStep(workflow domain.SCUMWorkflowInstance, step domain.SCUMWorkflowStep, title string, message string) (domain.SCUMWorkflowStep, error) {
|
|
stamp := svc.now()
|
|
step.Status = domain.SCUMWorkflowStepBlocked
|
|
step.SafeSummary = domain.SCUMSafeSummary{Title: title, Message: message, Details: map[string]string{"stepKey": step.StepKey, "capability": step.Capability}}
|
|
step.UpdatedAt = stamp
|
|
workflow.Status = domain.SCUMWorkflowBlocked
|
|
workflow.CurrentStepKey = step.StepKey
|
|
workflow.BlockerReason = title
|
|
workflow.SafeSummary = step.SafeSummary
|
|
workflow.UpdatedAt = stamp
|
|
if err := svc.store.SCUMWorkflowSteps().Update(step); err != nil {
|
|
return domain.SCUMWorkflowStep{}, err
|
|
}
|
|
if err := svc.store.SCUMWorkflowInstances().Update(workflow); err != nil {
|
|
return domain.SCUMWorkflowStep{}, err
|
|
}
|
|
return domain.CopySCUMWorkflowStep(step), nil
|
|
}
|
|
|
|
func (svc *CoreService) refreshSCUMWorkflowStatus(workflow domain.SCUMWorkflowInstance) (domain.SCUMWorkflowInstance, error) {
|
|
steps, err := svc.sortedSCUMWorkflowSteps(workflow.ID)
|
|
if err != nil {
|
|
return domain.SCUMWorkflowInstance{}, err
|
|
}
|
|
allConfirmed := len(steps) > 0
|
|
stamp := svc.now()
|
|
for _, step := range steps {
|
|
switch step.Status {
|
|
case domain.SCUMWorkflowStepFailed:
|
|
workflow.Status = domain.SCUMWorkflowFailed
|
|
case domain.SCUMWorkflowStepUnknown:
|
|
workflow.Status = domain.SCUMWorkflowUnknown
|
|
case domain.SCUMWorkflowStepCancelled:
|
|
workflow.Status = domain.SCUMWorkflowCancelled
|
|
case domain.SCUMWorkflowStepConfirmed:
|
|
default:
|
|
allConfirmed = false
|
|
}
|
|
if workflow.Status == domain.SCUMWorkflowFailed || workflow.Status == domain.SCUMWorkflowUnknown || workflow.Status == domain.SCUMWorkflowCancelled {
|
|
workflow.CurrentStepKey = step.StepKey
|
|
workflow.CompletedAt = stamp
|
|
workflow.UpdatedAt = stamp
|
|
return domain.CopySCUMWorkflowInstance(workflow), svc.store.SCUMWorkflowInstances().Update(workflow)
|
|
}
|
|
}
|
|
if allConfirmed {
|
|
workflow.Status = domain.SCUMWorkflowConfirmed
|
|
workflow.CurrentStepKey = ""
|
|
workflow.CompletedAt = stamp
|
|
} else {
|
|
workflow.Status = domain.SCUMWorkflowQueued
|
|
workflow.CurrentStepKey = ""
|
|
}
|
|
workflow.UpdatedAt = stamp
|
|
if err := svc.store.SCUMWorkflowInstances().Update(workflow); err != nil {
|
|
return domain.SCUMWorkflowInstance{}, err
|
|
}
|
|
return domain.CopySCUMWorkflowInstance(workflow), nil
|
|
}
|
|
|
|
func scumWorkflowDependenciesConfirmed(step domain.SCUMWorkflowStep, steps []domain.SCUMWorkflowStep) bool {
|
|
if len(step.DependsOn) == 0 {
|
|
return true
|
|
}
|
|
statuses := map[string]domain.SCUMWorkflowStepStatus{}
|
|
for _, candidate := range steps {
|
|
statuses[candidate.StepKey] = candidate.Status
|
|
}
|
|
for _, dependency := range step.DependsOn {
|
|
if statuses[dependency] != domain.SCUMWorkflowStepConfirmed {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func scumWorkflowRunnable(status domain.SCUMWorkflowStatus) bool {
|
|
switch status {
|
|
case domain.SCUMWorkflowQueued, domain.SCUMWorkflowRunning, domain.SCUMWorkflowWaiting:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func scumWorkflowStepRunnable(status domain.SCUMWorkflowStepStatus) bool {
|
|
switch status {
|
|
case domain.SCUMWorkflowStepQueued, domain.SCUMWorkflowStepWaiting:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func scumWorkflowStepTerminal(status domain.SCUMWorkflowStepStatus) bool {
|
|
switch status {
|
|
case domain.SCUMWorkflowStepConfirmed, domain.SCUMWorkflowStepFailed, domain.SCUMWorkflowStepUnknown, domain.SCUMWorkflowStepCancelled:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func scumWorkflowTemplates() map[string]scumWorkflowTemplateDefinition {
|
|
read := domain.JobCapabilityRemoteRunDBSQLiteQuery
|
|
logs := domain.JobCapabilityRemoteRunLogsTransfer
|
|
protectedSQL := domain.JobCapabilityRemoteRunProtectedSQL
|
|
rcon := domain.JobCapabilityRemoteRunRCONCommand
|
|
return map[string]scumWorkflowTemplateDefinition{
|
|
"scum.bootstrap-real-data": {Key: "scum.bootstrap-real-data", Title: "Bootstrap SCUM real data", Steps: []scumWorkflowStepDefinition{{Key: "verify-run-binding", Capability: read, TargetKey: "scum-database", Summary: "Verify run binding and SCUM.db query capability."}, {Key: "schema-probe", DependsOn: []string{"verify-run-binding"}, Capability: read, TargetKey: "scum-database", QueryTemplateKey: "scum.schema.probe", Summary: "Probe SCUM.db schema before projection refresh."}, {Key: "login-cursor", DependsOn: []string{"schema-probe"}, Capability: logs, TargetKey: "scum-login", Summary: "Initialize login log observation cursor."}}},
|
|
"scum.player-refresh": {Key: "scum.player-refresh", Title: "Refresh SCUM player", Steps: []scumWorkflowStepDefinition{{Key: "login-evidence", Capability: logs, TargetKey: "scum-login", Summary: "Sync login/logout evidence."}, {Key: "player-profile", DependsOn: []string{"login-evidence"}, Capability: read, TargetKey: "scum-database", QueryTemplateKey: "scum.player.profile", Summary: "Read player profile/economy facts."}, {Key: "position-read", DependsOn: []string{"player-profile"}, Capability: read, TargetKey: "scum-database", QueryTemplateKey: "scum.positions", Summary: "Read current player coordinates."}}},
|
|
"scum.world-refresh": {Key: "scum.world-refresh", Title: "Refresh SCUM world", Steps: []scumWorkflowStepDefinition{{Key: "squad-read", Capability: read, TargetKey: "scum-database", QueryTemplateKey: "scum.squads", MaxAttempts: 2, Summary: "Refresh squads."}, {Key: "vehicle-read", Capability: read, TargetKey: "scum-database", QueryTemplateKey: "scum.vehicles", MaxAttempts: 2, Summary: "Refresh vehicles."}, {Key: "flag-read", Capability: read, TargetKey: "scum-database", QueryTemplateKey: "scum.flags", MaxAttempts: 2, Summary: "Refresh flags."}, {Key: "position-read", Capability: read, TargetKey: "scum-database", QueryTemplateKey: "scum.positions", MaxAttempts: 2, Summary: "Refresh map positions."}}},
|
|
"scum.player-correction": {Key: "scum.player-correction", Title: "SCUM player correction", Steps: []scumWorkflowStepDefinition{{Key: "safety-check", Capability: read, TargetKey: "scum-database", QueryTemplateKey: "scum.player.profile", Summary: "Verify current projection, before value, offline state, and backup evidence."}, {Key: "apply-operation", DependsOn: []string{"safety-check"}, Capability: protectedSQL, TargetKey: "scum-database", OperationKey: "player.attribute.855.set", MutatesState: true, Summary: "Apply the approved typed operation through Run."}, {Key: "confirmation-read", DependsOn: []string{"apply-operation"}, Capability: read, TargetKey: "scum-database", QueryTemplateKey: "scum.player.profile", Summary: "Confirm the requested value by readback."}}},
|
|
"scum.gift-delivery": {Key: "scum.gift-delivery", Title: "SCUM gift delivery", Steps: []scumWorkflowStepDefinition{{Key: "eligibility-check", Summary: "Evaluate gift eligibility and idempotency."}, {Key: "deliver-reward", DependsOn: []string{"eligibility-check"}, Capability: rcon, TargetKey: "scum-management", OperationKey: "reward.deliver", MutatesState: true, MaxAttempts: 2, Summary: "Deliver approved reward through typed operation."}, {Key: "notify-player", DependsOn: []string{"deliver-reward"}, Capability: rcon, TargetKey: "scum-management", OperationKey: "player.notify", MutatesState: true, Summary: "Notify the player after delivery."}, {Key: "confirmation-read", DependsOn: []string{"notify-player"}, Capability: read, TargetKey: "scum-database", QueryTemplateKey: "scum.player.profile", Summary: "Confirm grant state/readback before marking delivered."}}},
|
|
"scum.territory-audit": {Key: "scum.territory-audit", Title: "SCUM territory audit", Steps: []scumWorkflowStepDefinition{{Key: "squad-roster", Capability: read, TargetKey: "scum-database", QueryTemplateKey: "scum.squad-members", Summary: "Refresh squad rosters."}, {Key: "flag-ownership", Capability: read, TargetKey: "scum-database", QueryTemplateKey: "scum.flags", Summary: "Refresh flag ownership."}, {Key: "risk-signal", DependsOn: []string{"squad-roster", "flag-ownership"}, Summary: "Project stale owner/member risk signals."}}},
|
|
"scum.vehicle-audit": {Key: "scum.vehicle-audit", Title: "SCUM vehicle audit", Steps: []scumWorkflowStepDefinition{{Key: "vehicle-read", Capability: read, TargetKey: "scum-database", QueryTemplateKey: "scum.vehicles", Summary: "Refresh vehicle inventory."}, {Key: "vehicle-map", DependsOn: []string{"vehicle-read"}, Capability: read, TargetKey: "scum-database", QueryTemplateKey: "scum.positions", Summary: "Refresh vehicle map overlays."}}},
|
|
"scum.ai-assist": {Key: "scum.ai-assist", Title: "SCUM AI assist", Steps: []scumWorkflowStepDefinition{{Key: "collect-allowed-fields", Summary: "Collect plugin-declared config fields and workflow inputs."}, {Key: "draft-review", DependsOn: []string{"collect-allowed-fields"}, Summary: "Create a reviewable typed diff or workflow draft."}, {Key: "approved-dispatch", DependsOn: []string{"draft-review"}, MutatesState: true, Summary: "Dispatch only after human approval through typed paths."}}},
|
|
"scum.product-cleanup": {Key: "scum.product-cleanup", Title: "SCUM product cleanup", Steps: []scumWorkflowStepDefinition{{Key: "remove-raw-routes", Summary: "Remove raw logs, terminal, config, and operation-history product routes."}, {Key: "publish-safe-status", DependsOn: []string{"remove-raw-routes"}, Summary: "Route users to safe workflow/status surfaces."}}},
|
|
}
|
|
}
|