The PLaneT network package manager did it even simpler: APIBREAKING.NONAPIBREAKING. Which might be interpreted as NEEDSENGINEERINGLOOKBEFOREUPGRADE.SAFEAUTOUPGRADE.
This was used for PLT Scheme, and was a rather nice 90%+ very lightweight solution.
If you were doing a package where backward-compatibility was a very high priority, you might be at PLaneT version 1.x forever.
If you were doing rapidly evolving research, and wanting to share code, while not surprise-breaking people who might use your stuff, you could end up with a large major version. For example, a package by Dave Herman (of Mozilla and Rust fame) got up to 9.x in only 2 years. http://planet.racket-lang.org/display.ss?package=javascript....
I don't recall whether anyone ever encountered unexpected breaking changes in practice that the version number didn't indicate. I'd guess probably. It wasn't a 100% solution alone.
PLaneT also supported specifying in a code file import which versions of the package you were known to work with (like meaning "major version 4, with minor version 2 or later; or major version 5"), with automatic installation of those.
You could also mix multiple versions of a package in a system, in the normal case (though maybe not if you had something like singleton state or resource that multiple versions of the package wanted to control). This seemed very valuable for pulling in lots of small open source dependencies, directly and indirectly, from different developers.
One downside of this simple approach is that it's bad as a "marketing version number". Like if you wanted to convey the value of large new features (or maybe talk about them in an easy way, like "version 4 foo features"), but you've been sticking to version 1.x backward compatibility the whole time. So there was a separate marketing version number, which seemed confusing enough that there must be a better way. But it was at least 90% there.