LPM CLI

Lockfile

Why LPM CLI ships two lockfile files, when each is read, and how the warm-install fast path works.

LPM CLI's lockfile starts with lpm.lock: the TOML, git-diffable, authoritative graph. When the graph fits the current binary subset, LPM CLI also writes lpm.lockb, a generated binary companion. Commit lpm.lock always; commit lpm.lockb when LPM CLI writes it.

This page is the conceptual overview — when each file gets read, what the dual format buys you, and how the warm-install fast paths work. For the on-disk schema, see lpm.lock format and lpm.lockb format.

Why two files

FileOptimized forWhen it's read
lpm.lockHumans, git diffs, authoritative install inputRead by install, audit, graph, and review workflows
lpm.lockbGenerated binary companion for the representable subsetUsed for binary validation/writeback and cheap generated-cache inspection when present

The TOML lockfile is the canonical source — sorted, deterministic, git-friendly. The binary companion is intentionally smaller: its v3 wire format represents fixed-width package records plus sparse verified-provenance records. If the TOML lockfile contains importer snapshots, patch records, aliases, peer pinning, dependency engine constraints, platform metadata, optional-reachability state, catalogs, or registry signatures, LPM CLI skips lpm.lockb and removes any stale binary file.

You don't choose the mode manually. Edit lpm.lock by hand if you ever need to (humans do that sometimes); the next install either regenerates a compatible lpm.lockb or leaves the project TOML-only.

What's pinned

Each entry in the lockfile records:

  • Exact resolved version — never a range
  • Source registry (e.g. registry+https://registry.npmjs.org)
  • SRI integrity hash (sha512-…) — verified on every install
  • Platform metadata (os, cpu, libc) and optional reachability
  • Registry signature evidence (registry-signatures, registry-published-at) when npm metadata provides it
  • Verified provenance evidence — publisher/workflow identity, exact npm package URL, tarball SHA-512 binding, transparency-log coordinates, certificate digest, and original bundle digest
  • Patch records (path, patch-file sha256, original-integrity) when lpm.patchedDependencies changes installed bytes
  • Direct dep names + versions — for transitive walking
  • Tarball URL hint — cached resolved URL that lets warm installs skip the per-package metadata round-trip
  • Importer snapshot — the package.json dependency sections and resolver-affecting settings that frozen installs compare before replaying the lockfile
  • (if any) npm-alias edges — local_name → target_canonical_name pairs

After lpm install, two further files exist:

  • lpm.lock — commit it
  • lpm.lockb — commit it when present (mark it as binary in .gitattributeslpm init does this automatically)

Install fast paths

The install pipeline has a tiered fast-path hierarchy. Each tier skips more work than the next.

Tier 1: Up-to-date install (~14 ms)

If the install-hash file (.lpm/install-hash) matches the current state of package.json, the authoritative lockfile, and the resolved linker mode, install exits immediately. Nothing to do.

This is the "I just ran lpm install and now I'm running it again" case. Hits in CI between consecutive lpm install calls in a script. The hash check is sync — file stat + timestamp comparison — so the entire lpm install invocation finishes in a few milliseconds on small projects and around 14 ms on the VitePress docs benchmark fixture.

Tier 2: Warm install (~387 ms)

If lpm.lock matches package.json (no drift), but node_modules/ is missing or stale, the install rebuilds node_modules/ directly from the lockfile + the global content-addressable store.

No metadata fetches. No tarball downloads. Project node_modules/<pkg> is created as a symlink into the matching link entry under ~/.lpm/store/v2/links/<graph-key>/. The link entries live in the global store, not in the project, so rm -rf node_modules between iterations only loses the cheap project-side symlinks — the canonical extracted bytes and the per-graph wrappers stay put. Warm install is a symlink rebuild over already-materialized link entries.

lpm fetch is the lockfile-only way to create that warm store state ahead of time. It reads lpm.lock, downloads compatible remote tarballs into the store, and leaves package.json, node_modules, and the lockfile untouched.

Tier 3: Lockfile-only install (offline-able)

lpm install --offline forces this path. The resolver is skipped entirely; every dep is resolved from lpm.lock's pinned versions, fetched from the global store (or errored if missing). No network. No re-resolution.

This is the CI-recommended path:

lpm install --offline --strict-integrity

--strict-integrity tightens the contract further: tarball-URL deps must declare their SRI inline. Trust-on-first-use is disabled.

Tier 4: Resolve-and-install (cold)

If the lockfile drifts from package.json (a new dep was added, a range was bumped), the resolver runs. The streaming dispatcher fetches metadata, picks versions, downloads tarballs, populates the store, links into node_modules/, writes a fresh lockfile.

This is the slow path, but "slow" is relative — cold install on the 535-package VitePress docs fixture is 2,945 ms in our benchmarks (vs npm 17,354 ms / pnpm 6,125 ms).

Lockfile-version

The TOML lockfile is schema-versioned (current: 7), not tool-versioned. A version bump only happens when the on-disk shape changes. Older clients reading a lockfile with a higher version refuse to install rather than silently misinterpreting fields — fail-fast over silent breakage. Version 5 added importer snapshots and patch checksum records, version 6 added per-package engines.node constraints, and version 7 added artifact-bound Sigstore provenance evidence.

The binary lockfile has its own wire-format version (current: 3). Version 2 added the per-entry tarball_off/tarball_len pair; version 3 added a sparse provenance section and an 8-byte footer while preserving the 36-byte package entries. On version mismatch, the reader rejects the binary file and falls back to parsing the TOML; the next write rebuilds the binary lockfile in the current version, completing the migration transparently.

When the binary lockfile is skipped

lpm.lockb is intentionally smaller than lpm.lock. It doesn't carry every field — importer snapshots, patch records, npm-alias metadata, ambient peer installs, per-package peers, dependency engine constraints, platform metadata, optional reachability, catalogs, and registry signatures live only in TOML. Projects with any of those fields write only lpm.lock. Verified provenance is representable in v3, but importer snapshots normally keep install-produced lockfiles TOML-only.

Why: silently dropping metadata would produce a binary lockfile that disagrees with the reviewer-visible TOML lockfile. Skipping the write and removing stale binary state is the safe behavior.

If your project only has lpm.lock, that's expected when the graph needs TOML-only metadata.

Determinism

The lockfile is deterministic by construction:

  • Entries sorted by name
  • Per-entry dependencies arrays sorted
  • Optional fields omitted (never written as null)
  • Schema-versioned

This is what makes git diff lpm.lock actionable. A new dep adds entries. A version bump rewrites a single package's fields. Supply-chain surprises (a transitive integrity change without a version bump, a new package appearing without a corresponding package.json change) are visible without parsing.

Should I commit lpm.lockb?

Commit lpm.lock always. Commit lpm.lockb when LPM CLI wrote it. If the project is TOML-only because the graph carries metadata outside the binary subset, do not synthesize a binary file by hand.

The only special handling: mark lpm.lockb as binary in .gitattributes:

lpm.lockb binary

lpm init adds this line for you.

See also