package app import ( "fmt" "strings" "gitea.maximumdirect.net/eric/narratio/internal/stage" ) // BoundedPlan is one validated inclusive range of the canonical pipeline. // It owns effective endpoints and membership so command, runner, and // composition callers do not independently interpret range bounds. type BoundedPlan struct { stages []stage.Stage canonicalNames []string startIndex int endIndex int explicitFrom bool explicitThrough bool } // BuildBoundedPlan selects an inclusive contiguous range of the canonical // pipeline. Empty endpoints default to the beginning or end respectively. func BuildBoundedPlan(from, through string) (BoundedPlan, error) { registry := stage.All() names := make([]string, len(registry)) indices := make(map[string]int, len(registry)) for index, candidate := range registry { if candidate == nil { return BoundedPlan{}, fmt.Errorf("build bounded plan: canonical stage %d is nil", index) } name := candidate.Name() if _, duplicate := indices[name]; duplicate { return BoundedPlan{}, fmt.Errorf("build bounded plan: duplicate canonical stage %q", name) } names[index] = name indices[name] = index } if len(registry) == 0 { return BoundedPlan{}, fmt.Errorf("build bounded plan: canonical stage registry is empty") } start := 0 if from != "" { var ok bool start, ok = indices[from] if !ok { return BoundedPlan{}, fmt.Errorf("build bounded plan: unknown from stage %q; valid stages: %s", from, strings.Join(names, ", ")) } } end := len(registry) - 1 if through != "" { var ok bool end, ok = indices[through] if !ok { return BoundedPlan{}, fmt.Errorf("build bounded plan: unknown through stage %q; valid stages: %s", through, strings.Join(names, ", ")) } } if start > end { return BoundedPlan{}, fmt.Errorf("build bounded plan: from stage %q occurs after through stage %q; valid stages: %s", from, through, strings.Join(names, ", ")) } return BoundedPlan{ stages: append([]stage.Stage(nil), registry[start:end+1]...), canonicalNames: append([]string(nil), names...), startIndex: start, endIndex: end, explicitFrom: from != "", explicitThrough: through != "", }, nil } // Stages returns a copy of the selected canonical stages. func (p BoundedPlan) Stages() []stage.Stage { return append([]stage.Stage(nil), p.stages...) } // Names returns selected stage names in canonical order. func (p BoundedPlan) Names() []string { out := make([]string, 0, len(p.stages)) for _, candidate := range p.stages { out = append(out, candidate.Name()) } return out } // From returns the effective inclusive start stage. func (p BoundedPlan) From() string { if len(p.canonicalNames) == 0 || p.startIndex < 0 || p.startIndex >= len(p.canonicalNames) { return "" } return p.canonicalNames[p.startIndex] } // Through returns the effective inclusive end stage. func (p BoundedPlan) Through() string { if len(p.canonicalNames) == 0 || p.endIndex < 0 || p.endIndex >= len(p.canonicalNames) { return "" } return p.canonicalNames[p.endIndex] } // Contains reports whether a canonical stage is selected by the range. func (p BoundedPlan) Contains(name string) bool { for _, candidate := range p.stages { if candidate.Name() == name { return true } } return false } // PrefixNames returns canonical stages excluded before the selected start. func (p BoundedPlan) PrefixNames() []string { if p.startIndex <= 0 || p.startIndex > len(p.canonicalNames) { return nil } return append([]string(nil), p.canonicalNames[:p.startIndex]...) } // HasExplicitBounds reports whether either endpoint was supplied by the caller. func (p BoundedPlan) HasExplicitBounds() bool { return p.explicitFrom || p.explicitThrough } // BuildFullPlan returns the canonical full stage list in deterministic order. func BuildFullPlan() []stage.Stage { plan, err := BuildBoundedPlan("", "") if err != nil { panic(err) } return plan.Stages() } // BuildSingleStagePlan returns a one-stage plan for an exact stage name. func BuildSingleStagePlan(name string) ([]stage.Stage, error) { s, err := stage.Select(name) if err != nil { return nil, fmt.Errorf("build stage plan: %w", err) } return []stage.Stage{s}, nil }