package engine import ( "context" "errors" "fmt" "sort" "github.com/MDietermann/ArduinoArchSetup/internal/module" ) // RunOptions controls a single engine run. type RunOptions struct { Selected []string // module IDs to run; empty = all DryRun bool // Detect + Plan only, no Apply/Verify HaltOnError bool // stop at first failure instead of continuing siblings Reporter module.Reporter } // ModuleStatus is the terminal state of a module within a run. type ModuleStatus string const ( StatusSucceeded ModuleStatus = "succeeded" StatusFailed ModuleStatus = "failed" StatusSkipped ModuleStatus = "skipped" StatusPlanned ModuleStatus = "planned" // dry-run only ) // ModuleResult captures what happened for one module. type ModuleResult struct { ID string Status ModuleStatus Plan module.Plan Err error } // Summary aggregates results. type Summary struct { Results []ModuleResult } // Counts returns (succeeded, failed, skipped). func (s Summary) Counts() (int, int, int) { var ok, fail, skip int for _, r := range s.Results { switch r.Status { case StatusSucceeded, StatusPlanned: ok++ case StatusFailed: fail++ case StatusSkipped: skip++ } } return ok, fail, skip } // Run drives the selected modules through the four phases in DAG order. // Parallelism is intentionally omitted in this foundation; a later plan // will introduce it. Sequential execution is a correct baseline. func Run(ctx context.Context, reg *module.Registry, opts RunOptions) (Summary, error) { mods, err := resolveSelection(reg, opts.Selected) if err != nil { return Summary{}, err } metas := make([]module.Meta, 0, len(mods)) for _, m := range mods { metas = append(metas, m.Meta()) } order, err := TopoSort(metas) if err != nil { return Summary{}, fmt.Errorf("module graph: %w", err) } byID := make(map[string]module.Module, len(mods)) for _, m := range mods { byID[m.Meta().ID] = m } results := make(map[string]*ModuleResult, len(mods)) skipped := make(map[string]bool) for _, id := range order { if skipped[id] { results[id] = &ModuleResult{ID: id, Status: StatusSkipped, Err: errors.New("skipped: upstream failed")} continue } m := byID[id] res := &ModuleResult{ID: id} results[id] = res state, err := m.Detect(ctx) if err != nil { res.Status = StatusFailed res.Err = fmt.Errorf("detect: %w", err) markFailed(metas, id, skipped) if opts.HaltOnError { break } continue } plan, err := m.Plan(ctx, state) if err != nil { res.Status = StatusFailed res.Err = fmt.Errorf("plan: %w", err) markFailed(metas, id, skipped) if opts.HaltOnError { break } continue } res.Plan = plan if opts.DryRun { res.Status = StatusPlanned continue } if err := m.Apply(ctx, plan, opts.Reporter); err != nil { res.Status = StatusFailed res.Err = fmt.Errorf("apply: %w", err) markFailed(metas, id, skipped) if opts.HaltOnError { break } continue } if err := m.Verify(ctx); err != nil { res.Status = StatusFailed res.Err = fmt.Errorf("verify: %w", err) markFailed(metas, id, skipped) if opts.HaltOnError { break } continue } res.Status = StatusSucceeded } summary := Summary{} // Emit results in topo order for stable output. ids := make([]string, 0, len(results)) for id := range results { ids = append(ids, id) } sort.SliceStable(ids, func(i, j int) bool { return indexOf(order, ids[i]) < indexOf(order, ids[j]) }) for _, id := range ids { summary.Results = append(summary.Results, *results[id]) } return summary, nil } func resolveSelection(reg *module.Registry, selected []string) ([]module.Module, error) { all := reg.All() if len(selected) == 0 { return all, nil } out := make([]module.Module, 0, len(selected)) for _, id := range selected { m, ok := reg.Get(id) if !ok { return nil, fmt.Errorf("unknown module %q", id) } out = append(out, m) } return out, nil } func markFailed(metas []module.Meta, id string, skipped map[string]bool) { for d := range Descendants(metas, id) { skipped[d] = true } } func indexOf(s []string, v string) int { for i, x := range s { if x == v { return i } } return -1 }