@@ -1315,19 +1315,28 @@ func (s *RecipeMetadataSpec) TopologicalLevels() ([][]string, error) {
13151315 return ComponentRefsTopologicalLevels (s .ComponentRefs )
13161316}
13171317
1318- // ComponentRefsTopologicalLevels is the free-function form of
1319- // RecipeMetadataSpec.TopologicalLevels — operates on a bare
1320- // []ComponentRef slice. Callers that have refs but not a full
1321- // RecipeMetadataSpec (e.g., the bundler post-resolution) use this.
1322- func ComponentRefsTopologicalLevels (refs []ComponentRef ) ([][]string , error ) {
1318+ // buildDependencyGraph constructs the dependency graph shared by
1319+ // TopologicalSort and ComponentRefsTopologicalLevels. It centralizes the
1320+ // enabled-filtering and external-satisfaction semantics so the two traversals
1321+ // (flat Kahn sort vs. level-grouped BFS) stay in lock-step — the duplication
1322+ // this removes is exactly what caused the double-fix in #1465 (see #1466).
1323+ //
1324+ // Only enabled components are nodes. A dependency edge pointing at a declared-
1325+ // but-disabled component is treated as already satisfied (the dependency is
1326+ // assumed provided externally, e.g. a CSP-managed cert-manager) and excluded
1327+ // from the in-degree count. An edge to an undeclared component is retained so
1328+ // it still surfaces as a cycle/missing-dependency error. See componentSets and
1329+ // edgeSatisfiedExternally.
1330+ //
1331+ // Returns the per-node in-degree, the reverse adjacency (dependency name → the
1332+ // components that depend on it), and the number of enabled nodes. Callers
1333+ // compare their processed count against enabledCount to detect cycles/missing
1334+ // dependencies (a node whose in-degree never drains to zero is never emitted).
1335+ func buildDependencyGraph (refs []ComponentRef ) (inDegree map [string ]int , dependents map [string ][]string , enabledCount int ) {
13231336 declared , enabled := componentSets (refs )
13241337
1325- // Only enabled components are nodes; an edge pointing at a declared-but-
1326- // disabled (externally-provided) component is treated as satisfied, while
1327- // an edge to an undeclared component is retained so it still surfaces as a
1328- // cycle/missing-dependency error. See componentSets and TopologicalSort.
1329- inDegree := make (map [string ]int , len (enabled ))
1330- dependents := make (map [string ][]string , len (enabled ))
1338+ inDegree = make (map [string ]int , len (enabled ))
1339+ dependents = make (map [string ][]string , len (enabled ))
13311340 for _ , c := range refs {
13321341 if _ , ok := enabled [c .Name ]; ! ok {
13331342 continue
@@ -1342,6 +1351,15 @@ func ComponentRefsTopologicalLevels(refs []ComponentRef) ([][]string, error) {
13421351 }
13431352 inDegree [c .Name ] = degree
13441353 }
1354+ return inDegree , dependents , len (enabled )
1355+ }
1356+
1357+ // ComponentRefsTopologicalLevels is the free-function form of
1358+ // RecipeMetadataSpec.TopologicalLevels — operates on a bare
1359+ // []ComponentRef slice. Callers that have refs but not a full
1360+ // RecipeMetadataSpec (e.g., the bundler post-resolution) use this.
1361+ func ComponentRefsTopologicalLevels (refs []ComponentRef ) ([][]string , error ) {
1362+ inDegree , dependents , enabledCount := buildDependencyGraph (refs )
13451363
13461364 // Seed level 0: components with no incoming edges.
13471365 current := make ([]string , 0 , len (inDegree ))
@@ -1371,7 +1389,7 @@ func ComponentRefsTopologicalLevels(refs []ComponentRef) ([][]string, error) {
13711389 current = next
13721390 }
13731391
1374- if processed != len ( enabled ) {
1392+ if processed != enabledCount {
13751393 return nil , errors .New (errors .ErrCodeInvalidRequest ,
13761394 "cannot determine deployment levels: circular dependencies exist" )
13771395 }
@@ -1382,30 +1400,7 @@ func ComponentRefsTopologicalLevels(refs []ComponentRef) ([][]string, error) {
13821400// Components with no dependencies come first, then components that depend only
13831401// on already-listed components, etc.
13841402func (s * RecipeMetadataSpec ) TopologicalSort () ([]string , error ) {
1385- declared , enabled := componentSets (s .ComponentRefs )
1386-
1387- // Only enabled components are nodes. A dependency edge pointing at a
1388- // declared-but-disabled component is treated as already satisfied (the
1389- // dependency is assumed to be provided externally, e.g. a CSP-managed
1390- // cert-manager) and excluded from the in-degree count. An edge to an
1391- // undeclared component is still counted so it surfaces as a cycle/missing
1392- // dependency error, matching the prior behavior. See componentSets.
1393- inDegree := make (map [string ]int , len (enabled ))
1394- dependents := make (map [string ][]string ) // dep -> list of components that depend on it
1395- for _ , c := range s .ComponentRefs {
1396- if _ , ok := enabled [c .Name ]; ! ok {
1397- continue
1398- }
1399- degree := 0
1400- for _ , dep := range c .DependencyRefs {
1401- if edgeSatisfiedExternally (dep , declared , enabled ) {
1402- continue
1403- }
1404- degree ++
1405- dependents [dep ] = append (dependents [dep ], c .Name )
1406- }
1407- inDegree [c .Name ] = degree
1408- }
1403+ inDegree , dependents , enabledCount := buildDependencyGraph (s .ComponentRefs )
14091404
14101405 // Kahn's algorithm
14111406 // https://www.geeksforgeeks.org/dsa/topological-sorting-indegree-based-solution/
@@ -1418,7 +1413,7 @@ func (s *RecipeMetadataSpec) TopologicalSort() ([]string, error) {
14181413 // Sort queue for deterministic output
14191414 sort .Strings (queue )
14201415
1421- result := make ([]string , 0 , len ( enabled ) )
1416+ result := make ([]string , 0 , enabledCount )
14221417 for len (queue ) > 0 {
14231418 node := queue [0 ]
14241419 queue = queue [1 :]
@@ -1434,7 +1429,7 @@ func (s *RecipeMetadataSpec) TopologicalSort() ([]string, error) {
14341429 }
14351430
14361431 // Check if all enabled nodes were processed (no cycles)
1437- if len (result ) != len ( enabled ) {
1432+ if len (result ) != enabledCount {
14381433 return nil , errors .New (errors .ErrCodeInvalidRequest , "cannot determine deployment order: circular dependencies exist" )
14391434 }
14401435
0 commit comments