Semantic Versioning ties each bump to one exact test: incompatible change for major, backward compatible addition for minor, backward compatible fix for patch — and a number chosen by feel is a promise to consumers I had no right to make.
I used to pick a version bump the way I picked which reaction to leave on a message: whichever one felt roughly right for how big the change seemed to me. A rewrite of the internals felt like a major. A new optional field felt like a minor. A one-line fix felt like a patch. That system worked fine until the day a consumer of one of my own packages, pinned to a minor-version range and trusting the promise implicit in it, broke anyway, because the change I had shipped as a minor bump was not backward compatible at all. It only felt that way to me.
Semantic Versioning 2.0.0, the current version of the specification, does not leave the three categories to feel. A MAJOR bump is for when you make incompatible API changes. A MINOR bump is for when you add functionality in a backward compatible manner. A PATCH bump is for when you make backward compatible bug fixes. Each definition turns on one distinction, incompatible versus backward compatible, and that distinction is the entire test. It is not a test of how much code changed or how long the diff came out.
What I had actually shipped as a minor bump was the removal of a parameter nobody was supposed to be using any more, which felt small because the change itself was small to write. Removing a parameter is an incompatible API change under that definition regardless of how minor it feels to the person doing the removing, because somewhere a caller is passing it and that call now breaks. The consumer who pinned to a minor range was not wrong to trust the number. I was wrong to have put that number on that change.
The version number is not a note to myself about how the release felt while I was writing it. It is a promise made to anyone who has not read the diff and is relying entirely on the number to say whether an automatic update is safe. A range like a caret-pinned minor is a bet the consumer makes, based entirely on my adherence to those three definitions, that anything landing inside it will not break their code. Every time I have bumped a version based on feel rather than on the actual test, I have been spending trust on their behalf that was never mine to spend.
The same test now runs in the other direction too, when I am the one consuming someone else’s bump rather than issuing my own. A changelog that says a release is minor, sitting next to a diff that also renames a public method, is not describing a minor release by the specification’s own definition, whatever label the maintainer chose to attach to it. I have started actually reading the diff behind an incoming minor or patch bump on anything my own build depends on, rather than trusting the label on its own. The label is still useful information. It is just not sufficient by itself, in either direction.
What changed in how I tag a release now is small and mechanical. Before choosing the bump, I ask which of the three definitions the change actually satisfies, not how big it seems while I am writing it. Did I make an incompatible change, anything removed, renamed, or altered in a way an existing caller could break on. Did I add functionality without touching anything existing, which is minor. Is it a fix that does not touch the API surface at all, which is patch. It is a duller process than picking by feel, and it is the only version of the process where the number still means what the person reading it assumes it means.