AI coding

A File Manifest for Lockfile Diffs: Paths, Globs and Line Counts

Antonio Evans9 min read

A clean diagram on a light background: a 1,200-line lockfile diff shown as a tall stack of horizontal lines on the left, a funnel in the middle labeled with three rules (path glob, file type, line count).
Key points

Key points

  1. 1A lockfile diff is mostly generated resolution records, so reviewers should skim those and open only the entries that change source, integrity or direct version guarantees.
  2. 2Path allowlists with globs decide which files a reviewer may collapse, while a line-count rule per file type decides when collapsing happens automatically.
  3. 3Manifest edits deserve a human because they change the constraint set that regenerates the lockfile, and a mismatch between manifest and lockfile is the usual origin of lockfile drift.
  4. 4The strongest signal in any lockfile diff is a changed registry URL or integrity hash, which is exactly how a compromised package is swapped in behind an otherwise ordinary version bump.

Separate the two kinds of file in a dependency change

Every dependency change touches two different artifacts. The manifest says what you asked for, and the lockfile says what you got, including the exact resolved versions of direct and transitive packages. A lockfile records the resolved set so that builds are reproducible and packages can be verified as documented in the lockfile design study. The manifest holds version ranges such as ^1.2.0, which leaves the package manager free to resolve compatible versions as described in the lockfile drift writeup.

That split is what makes a single diff budget fail. A 900 line package-lock.json change and a 3 line package.json change need completely different reviewer attention, even though both arrive in the same pull request. The lockfile change is mostly mechanical output of a resolution pass. The manifest change is a human decision about what the project is allowed to consume.

The decision rule is simple enough to write on a card:

Skim any file whose content is regenerated by the package manager. Review any file whose content is written by a person. Open specific entries in a skimmed file when they change a source, a hash, or a direct version boundary that a manifest line can be traced to.

That rule survives changes in tooling because it depends on who authored the line, not on which tool produced the file.

A path allowlist with globs

An allowlist works better than a denylist here because generated file names multiply. A monorepo may contain package-lock.json, pnpm-lock.yaml, yarn.lock, poetry.lock, Cargo.lock, go.sum, Gemfile.lock and per-package lockfiles nested inside workspace directories. Enumerating every lockfile name by hand is maintenance work that nobody does. Globs handle the nesting, and a small explicit review list handles the manifests.

Two tiers cover most repositories:

TierMatching pathsReviewer actionDefault when large
Skim**/*.lock, **/package-lock.json, **/pnpm-lock.yaml, **/go.sum, **/composer.lockCollapse the diff, read the flagged entries onlyAuto-collapse above the threshold
Review**/package.json, **/pyproject.toml, **/Cargo.toml, **/go.mod, **/Gemfile, **/composer.jsonRead every line in contextNever collapse
Watch**/.github/workflows/*.yml, **/*.lock entries whose source field changesOpen the specific hunkNever collapse

Tier two matters more than it looks. A manifest edit that bumps a range from ^1.4.0 to ^1.5.0 may pull in a new major of a transitive dependency that nothing in the manifest mentions. The semver specification is explicit that a major version signals incompatible API changes, a minor signals backward compatible additions and a patch signals backward compatible fixes per the SemVer specification. Reviewer attention should go to the line that changes the range, then to the lockfile hunk showing what the new range resolved to.

One caution on the skim tier: a lockfile in a library is not the same artifact as a lockfile in an application. Library lockfiles are not used by consumers, and some maintainers decline contributions that add them as argued in this discussion of lockfile scope. The related Stack Overflow thread on whether to commit package-lock.json shows the same disagreement, with the consensus that it depends on whether the project is a top level application or a dependency of other projects per the Stack Overflow discussion. If your repository publishes a library, keep lockfiles out of the skim tier or leave them out of version control entirely.

The line-count rule per file type

A threshold makes collapsing deterministic. GitHub already collapses diffs once they pass a couple hundred lines of code by default, which is precisely why a malicious lockfile edit can hide in a folded diff as shown in the Snyk analysis of lockfile injection. Manual review of a folded diff is not review at all.

Set the threshold per file type instead of globally. A reasonable starting set:

File typeCollapse aboveReason
Lockfiles (*.lock, package-lock.json, go.sum)200 changed linesResolution churn is normal; 500 line changes are common in real projects per Snyk
Manifests (package.json, pyproject.toml, Cargo.toml)NeverEvery line is a human decision
CI workflow definitions0 lines, meaning never collapseSmall diffs carry high leverage
Everything elseWhatever policy your team already usesOut of scope for this rule

Why 200 and not 100? Because a routine transitive bump can add or remove a few dozen packages, each contributing a handful of lines, and a 100 line threshold would fire on nearly every dependency pull request. Why not 1,000? Because a 900 line lockfile change can still contain a swapped source field, and folding it hides exactly the line that matters. The 200 line number is a starting point, not a law. Measure your own repository: if half your lockfile pull requests exceed the threshold, the threshold stops meaning anything, and you should raise it or move to entry level filtering.

Which entries need a human

Inside a large lockfile diff, only a small set of fields deserves attention. The rest is generated bookkeeping: dependency edges, resolved version strings that follow from a range, and structural reshuffling from deduplication.

The first class is source and integrity. A lockfile entry normally points at the official registry and pins a content hash. If either changes, treat it as a security event until proven otherwise. Snyk demonstrated an attack where a perfectly ordinary looking version bump in yarn.lock replaced the registry URL for a package with a personal GitHub tarball, which then ran a postinstall script on install per the Snyk article. The version stayed constant. Only the source moved.

Scanner tooling has the same problem in the opposite direction: a rule that matches == in source flagged base64 padding inside lockfile integrity hashes and produced 183 critical findings on one real package-lock.json as reported in this scanner issue. That is a good reminder that lockfiles are data, not code, and both your scanner and your reviewer should treat them that way.

The second class is direct dependency version changes. If package.json names a package and the lockfile shows its resolved version moving across a major boundary, a human should confirm the code still works. Cognate discussions on when a dependency change justifies a version bump in the consuming project show how unsettled this remains, with no clear consensus on whether a dependency only change counts as a patch or a minor for the consumer as seen in this semver issue thread and this related Stack Overflow thread. If the internal debate is unresolved, automation is not the answer; a reviewer is.

The third class is overrides and resolutions. An override in the manifest forces a specific version of a transitive package everywhere in the tree, either globally or scoped inside one parent as documented in this npm overrides guide. Overrides show up in the lockfile as surprising version pins that contradict the surrounding package's own range. They are safe when deliberate and alarming when mysterious. Any diff that adds or edits an override deserves a human, because the lockfile will look wrong on purpose.

Two smaller classes round out the list. Deduplication reshuffling is skimmable: when npm folds several copies of a package into one installed copy, the tree output marks them as deduped, and in one documented example 532 of 1,337 modules were deduplicated per this walkthrough of npm ls. You can see the same effect in your own tree with npm ls --all, which prints transitive dependencies rather than direct only per the npm documentation referenced in this answer. A tie breaking re-resolution that moves a package between resolution paths without changing its version or hash is not worth reading line by line.

Finally, check that the lockfile was regenerated at all. If the manifest changed and the lockfile did not, you have drift: a manifest that declares constraints the lockfile does not satisfy, which is the most common way dependency changes go wrong unnoticed per sbomify. The fix is mechanical: use npm ci, yarn install --frozen-lockfile or poetry check --lock in CI so the build fails on mismatch instead of silently regenerating per the same writeup.

A worked example from a sample repo

Take a small application repository with a root package.json, a committed package-lock.json, a Python service with pyproject.toml and poetry.lock, and a CI workflow under .github/workflows/. A Dependabot style pull request changes the following:

FileChanged linesTierAction
package-lock.json640SkimAbove 200, auto-collapse to flagged entries
package.json2ReviewRead fully
services/worker/pyproject.toml1ReviewRead fully
services/worker/poetry.lock118SkimBelow 200, read in normal diff view
.github/workflows/test.yml6WatchRead fully

With the allowlist applied, the reviewer sees three small files in full and two collapsed lockfiles, of which only one collapse is triggered automatically. On the Python side at 118 lines, there is no reason to collapse; the whole diff is readable and may contain a source change.

Inside the 640 line npm lockfile change, the flagged entries are the ones to read:

  1. The root packages[""].dependencies block, which mirrors package.json and should agree with it.
  2. Any entry whose resolved URL differs from https://registry.npmjs.org/..., because a source change is the highest risk event in the diff per Snyk.
  3. Any entry whose integrity hash changed without a corresponding version change.
  4. Any entry for a package named in package.json, since that is a direct dependency by definition per the npm ls behavior described here.

Everything else, which in this example is several hundred lines of version strings for transitive packages, can be skimmed. If the reviewer wants a structural sanity check, running npm ls --all on the branch and diffing the tree against the base branch gives a compact summary of what actually moved per the same discussion.

To encode this in CI, the allowlist can live as data and drive a small check. Nothing here is specific to any one linter:

lockfile_policy:
  skim:
    - "**/*.lock"
    - "**/package-lock.json"
    - "**/pnpm-lock.yaml"
    - "**/go.sum"
  review:
    - "**/package.json"
    - "**/pyproject.toml"
    - "**/Cargo.toml"
    - "**/go.mod"
  collapse_threshold:
    skim: 200
    review: 0
  flagged_fields:
    - resolved
    - integrity
    - version

The version field being flagged does not mean every version change is a human review requirement. It means the diff extractor should surface version changes for packages that appear in the manifest, then drop version changes for everything else. That filtering step is the part that turns a 640 line wall into four or five lines worth reading.

Applying it to newer ecosystems

The same policy generalizes to ecosystems outside npm. Poetry and pip tools record hashes alongside pinned versions in their lockfiles, and tooling like Pex verifies downloaded artifacts against the expected hashes in the lockfile at build time as described in the Pants documentation on Python lockfiles. That means a Python lockfile diff with a changed hash but an unchanged version deserves the same scrutiny as its npm equivalent.

Cargo and Go follow the same shape. Their ecosystems' strict CI commands fail when the lockfile and manifest disagree, which is the cheapest possible drift detector per sbomify. Add them to the skim tier and their manifests to the review tier, and the policy carries over without modification.

Workflow files are the odd case. GitHub Actions now supports workflow lockfiles that pin dependencies to specific commits, so tags and branches moving cannot silently change what runs, along with a CLI extension for generating the lockfile and a repository policy to require it per the GitHub changelog entry. That means a workflow file plus its lockfile should be a single review unit. Never collapse one without the other, and never treat the .yml as skimmable just because its lockfile partner is.

If you would rather not maintain the diff filtering logic in-house, an expert can take the policy as written and wire it into your review process. Quanta Experts reads your repository context and returns the diff plan and the CI check as a deliverable.

FAQ

FAQ

What exactly counts as a lockfile, as opposed to a manifest?

A manifest is written by a person and declares version ranges or requirements, for example package.json, pyproject.toml, Cargo.toml or go.mod. A lockfile is generated by the package manager and records the exact resolved versions of direct and transitive dependencies, for example package-lock.json, poetry.lock, Cargo.lock or go.sum. Both files are usually committed for applications; for libraries, the convention is more contested per this dev.to argument.

If the lockfile is generated, why review it at all?

Because the generated file can point at files the package manager did not fetch from the registry you expect. Snyk showed a case where a version bump in yarn.lock swapped the resolved URL to a personal repository, and a postinstall script then ran on install per the Snyk article. The version stayed the same, and only the source changed, which is exactly what auto-collapsing hides.

How do I choose the line threshold for my repository?

Start at 200 changed lines for lockfiles and 0 for manifests, then measure. Count how many dependency pull requests exceed the threshold in a normal month. If more than half do, the threshold is too low and will be ignored; raise it until collapsing is a signal rather than a routine. For manifests and CI workflow files, never collapse.

What if the manifest changed but the lockfile did not?

That is lockfile drift, and it means the lockfile no longer reflects the constraints in the manifest. The build is not reproducible, and any SBOM generated from the lockfile will list the wrong versions per sbomify. Catch it in CI with strict install commands such as npm ci for npm, yarn install --frozen-lockfile for Yarn, poetry check --lock for Poetry, and cargo check --locked for Cargo.

Do dependency updates in the lockfile mean the project's own version should bump?

There is no settled answer. The semver specification defines major, minor and patch in terms of the public API of the package itself, not its dependencies per the SemVer specification, and long running discussions disagree on whether a dependency only change counts as patch or minor for the consumer as seen in this semver issue thread and a related exchange. Pick a convention, document it, and treat it as an internal policy rather than a specification requirement.

Sources

Sources

  1. The Design Space of Lockfiles Across Package Managers
  2. What Is Lock File Drift? A Hidden Risk in Dependency ...
  3. Semantic Versioning 2.0.0 | Semantic Versioning
  4. When not to use package-lock.json
  5. Do I commit the package-lock.json file created by npm 5?
  6. Why npm lockfiles can be a security blindspot for injecting ...
  7. js.type-coercion.loose-equality reports lockfile integrity ...
  8. Version after dependency changes? · Issue #148 - semver
  9. Should I increase major version on dependency update
  10. A Guide to NPM Overrides: Take Control of Your ...
  11. Using npm's ls command for Fun and Insight
  12. How to view the dependency tree of a given npm module?
  13. Lockfiles
  14. GitHub Actions workflow lockfiles in public preview
About the author

About the author

Antonio Evans
Antonio Evans

Antonio Evans is the founder of Quanta Intelligence and a fractional CTO who builds HIPAA-ready AI systems for health and life-science companies. Before that he was technical co-founder of FriendsEAT for twenty years, CEO of Socialdraft, and a data engineer at IBM. Antonio lives with his dog Santino in Lisbon, Portugal.

All articles