npm did not always do it right, and IMO still does not do it completely right (nor does pnpm, my preferred replacement for npm -- but it has `--frozen-lockfile` at least that forces it to do the right thing) because transitive dependencies can still be updated.
cargo can also update transitive dependencies (you need `--locked` to prevent that).
Ruby's Bundler does not, which is preferred and is the only correct default behaviour. Elixir's mix does not.
I don't know whether uv handles transitive dependencies correctly, but lockfiles should be absolute and strict for reproducible builds. Regardless, uv is an absolute breath of fresh air for this frequent Python tourist.
npm will not upgrade transient dependencies if you have a lockfile. All the `forzen-lockfile` or `npm ci` commands does is prevent upgrades if you have incompatible versions specified inside of `package.json`, which should never happen unless you have manually edited the `package.json` dependencies by hand.
(It also removed all untracked dependencies in node_modules, which you should also never have unless you've done something weird.)
I'm not sure when that behaviour might have changed, but I have seen it do so. Same with yarn when not specifying a frozen lockfile.
I switched to pnpm as my preferred package manager a couple of years ago because of this, and even that still requires explicit specification.
It was an unpleasant surprise, to say the least.