I don't actually see the problem in producing a hash of the API you use.
It is simply the hash of the list of your imports.
Is this overspecific? In the sense that it is more restrictive than it could be?
Yes, but at least you get reproducibility and a permanently working build.
Semantic versioning sounds good in theory and often works in practice, but you are relying on the goodwill and the precision of the library maintainer when both of these qualities are in short supply.
The advantage of using module-level hashes is that you:
* Avoid mistakes due to incorrect semantic versioning
* Get maximum flexibility, as you are exploding the 'package cage' and accessing every file individually
* Avoid a lot of 'false incompatibilities', when for example you would like to access a new version of a module in an upgraded package but you can't upgrade because you depend on another package that declares to be compatible only with the previous version of the package you want to upgrade to even when it's not actually using the new module you are interested in at all.
The inconvenience of hashes is that they are cumbersome to manage by hand, but this can be automated.
In fact, the question is not really semantic versioning vs hash-versioning. Both can be used at the module level as well as to the package level.
The question is if we can simplify the dependency management problem by "going smaller", moving from a bunch of related modules (a package) as a unit of code exchange to a single module.
In principle one could even go smaller, considering every function or data type [1] as an independently sharable entity.
So, what is the optimal 'size' for code sharing: package, module, or entity?
Best,
titto
[1]
This actually make a lot of sense if you are interested in cross-language interoperability, see
https://github.com/Quid2/zm for an example.