// Sub-Dependency Solver. // // In the original Source Mage, SUB_DEPENDS expresses the idea that // "spell A needs spell B built with feature X". Sorcery-Go encodes that // requirement on the dag.Edge.Features field and the Solver checks whether the // currently-active Essence for the dependency satisfies the requested feature set. // // If not, the Solver returns a ReForge recommendation that the Cast pipeline // uses to enqueue a fresh variant of the dependency (with the new flag) before // continuing the parent build. package dag import "fmt" // ReForge is a recommendation produced by the Solver when an existing // Essence does not satisfy the requested sub-depends features. type ReForge struct { Spell string NeededBy string MissingFlags []string } // Solver is the feature-aware dependency negotiator. // // It relies on a FeatureLookup callback to ask the Tomb whether a given // spell's current Essence variant exposes a feature. This keeps the dag // package free of import cycles with pkg/tomb. type Solver struct { Lookup func(spell, feature string) (present bool, err error) } // Solve inspects every required edge of `spell` and returns the list of // re-forge recommendations. An empty list means the existing variants are // already compatible and the cast can proceed. func (s *Solver) Solve(spell string, g *Graph) ([]ReForge, error) { return s.solveVisited(spell, g, make(map[string]struct{})) } // solveVisited is the recursive implementation with cycle detection. // The visited map prevents infinite recursion on circular dependency graphs. func (s *Solver) solveVisited(spell string, g *Graph, visited map[string]struct{}) ([]ReForge, error) { if _, seen := visited[spell]; seen { return nil, nil // cycle detected — skip, don't recurse further } visited[spell] = struct{}{} root, ok := g.Nodes[spell] if !ok { return nil, ErrSpellNotFound } var out []ReForge for _, edge := range root.Edges { if edge.Type == OptionalDep && !edge.Enabled { continue } for _, feature := range edge.Features { present, err := s.Lookup(edge.Target.Name, feature) if err != nil { return nil, fmt.Errorf("solver: lookup %s/%s: %w", edge.Target.Name, feature, err) } if !present { out = append(out, ReForge{ Spell: edge.Target.Name, NeededBy: spell, MissingFlags: []string{feature}, }) } } // Recurse into children so a transitive missing flag also triggers. if more, err := s.solveVisited(edge.Target.Name, g, visited); err == nil { out = append(out, more...) } } return out, nil }