A dependency version is useful when its publisher and consumers agree on the public contract. Semantic Versioning gives changes a vocabulary: from version 1.0.0 onward, incompatible public API changes require a major increment, compatible additions require a minor increment, and compatible fixes require a patch increment. The number summarizes a claim about a release. It does not inspect its implementation or guarantee that an upgrade will work in your application.

Declare what compatibility covers

A public API can include exported functions, documented command options, configuration formats, response fields, and specified error behavior. Write that boundary clearly enough to decide whether a change violates it. An internal refactor that preserves the contract differs from removing a documented option. A correction to behavior that contradicted the contract can be a compatible bug fix even though somebody observed the old behavior. Explain that change in release notes and test affected consumers.

SemVer requires a minor increment when public functionality is marked deprecated. Removal later requires the appropriate incompatible release. Once a version has been released, its contents must not change; publish another version for a correction. Replacing the bytes behind the same version defeats traceability and can produce different installations from identical dependency declarations.

Major version zero signals initial development, with an API that should not be considered stable. A publisher may describe additional conventions for that phase, but do not assume every zero-major update is compatible. Prereleases have lower precedence than the corresponding normal release. Build metadata after + is ignored when determining precedence, so equal precedence does not imply identical packages or hashes.

When it breaks

A dishonest minor release can break a documented contract despite satisfying a consumer’s version range. A correctly labeled release can also expose an application that relied on undocumented internals. Read the changelog, run meaningful integration checks, and retain a lockfile when the build needs repeatable dependency selection. A lockfile records selected versions; it does not certify their behavior or authenticity.

Package manager ranges are another boundary. SemVer defines versions and precedence, while tools define how constraints select candidates. In node-semver, a caret generally permits changes to the right of the leftmost nonzero component. Consequently, ^0.2.3 excludes 0.3.0, and ^0.0.3 excludes 0.0.4. A tilde with all three components permits patch changes within that minor line.

Prerelease selection has additional rules. A broad stable range does not automatically accept a newer beta. Explicitly allowing a prerelease tuple can admit subsequent prereleases for that tuple. Verify the resolver’s actual behavior instead of applying ordinary string sorting or treating every numerically larger version as an eligible update.

Check it quickly

Save this as semver.py and run python3 semver.py with Node and npm available. Internet access to the public npm registry is required. The script installs semver@7.7.2 into a temporary directory with isolated configuration and cache, disables package scripts, and runs the library’s actual comparison functions. It does not edit the current project’s dependencies.

from pathlib import Path
from tempfile import TemporaryDirectory
import json
import os
import subprocess

with TemporaryDirectory() as directory:
    root = Path(directory)
    user_config = root / 'user.npmrc'
    global_config = root / 'global.npmrc'
    user_config.write_text('')
    global_config.write_text('')
    env = {'PATH': os.environ['PATH'], 'LC_ALL': 'C',
           'NPM_CONFIG_USERCONFIG': str(user_config),
           'NPM_CONFIG_GLOBALCONFIG': str(global_config),
           'NPM_CONFIG_CACHE': str(root / 'cache')}
    subprocess.run(['npm', 'install', '--ignore-scripts', '--no-audit', '--no-fund',
                    '--package-lock=false', '--registry=https://registry.npmjs.org',
                    'semver@7.7.2'], cwd=root, env=env, check=True, capture_output=True)
    script = r'''
const s = require('semver');
const assert = require('node:assert/strict');
const cases = [
  ['1.9.0', '^1.2.3', true], ['2.0.0', '^1.2.3', false],
  ['1.2.9', '~1.2.3', true], ['1.3.0', '~1.2.3', false],
  ['0.2.9', '^0.2.3', true], ['0.3.0', '^0.2.3', false],
  ['0.0.4', '^0.0.3', false], ['1.3.0-beta.1', '^1.2.3', false],
  ['1.3.0-beta.2', '^1.3.0-beta.1', true]
].map(([version, range, expected]) => {
  const accepted = s.satisfies(version, range);
  assert.equal(accepted, expected);
  return {version, range, accepted};
});
assert.equal(s.compare('1.2.3+build.1', '1.2.3+build.2'), 0);
assert.equal(s.lt('1.3.0-beta.2', '1.3.0'), true);
assert.equal(s.valid('1.02.3'), null);
console.log(JSON.stringify({library: require('semver/package.json').version, cases,
  metadata_precedence_equal: true, prerelease_before_release: true, leading_zero_rejected: true}));
'''
    result = subprocess.run(['node', '-e', script], cwd=root, env=env,
                            capture_output=True, text=True, check=True)
    print(json.dumps(json.loads(result.stdout)))

The nine range cases must match their recorded expectations: caret accepts 1.9.0 under ^1.2.3 but rejects 2.0.0; tilde rejects the next minor; the zero-major cases remain narrower. The stable range rejects 1.3.0-beta.1, while the explicitly prerelease range accepts 1.3.0-beta.2.

The remaining assertions establish that two build metadata variants compare equally, a beta precedes its normal release, and a leading zero in 1.02.3 is invalid. These results verify parsing and selection, not the compatibility of any real library update. Pair a proposed version bump with evidence about the changed API and the consumers affected. The version communicates that judgment; release notes and tests make the judgment assessable.

Further reading

Sources