sorcery-go/pkg/dag/solver.go

75 lines
3.1 KiB
Go
Executable File

// 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
}