The version numbering should specifically speak of contracts between components, or else it's not robust enough to help with automating things.
Roughly speaking: the contract is the header files exported (or IDL files), and an implementation of the contract imports those header files. The header files should contain all documentation that pertains to the contract, and even documentation changes to the contract should bump the contract version up, while the implementation imports the contract just like the client does.
The contracts "5.0" and "5.1" are not necessarily compatible in every way, because the interface expanded; which will cause things like structures having incompatible sizes in C, or unimplemented interface members in Java. For this reason, 5.0.20 and 5.1.20 might be the same implementation exposing two different interfaces. Both being version "5" basically means compatibility at the level of source code, and both being "5.1" should mean that they both work with the same contract. That means that there is agreement on: structure members, sizes, which functions exist, their preconditions and postconditions, data layout in buffers passed around, etc.
Assuming that "5.7" can use "5.2" should be expressed in an explicit and computable way. A "5.7" component might export a "5.2" interface, but it should be done explicitly. In the real world, there are compatibility outages in places where theoretically things should be compatible. It's almost easier to dispense with version numbers, and simply declare that if A talks to B, then they both import C of the same git commit, or a C in a list of supported interfaces.
Otherwise, the versioning is just a bunch of marketing stuff that has little to say about what is actually connected to what.