Dependency resolution and flog.lock
Flog resolves npm registry and workspace dependencies into a versioned strict JSON lockfile. Resolution is deterministic: package names, versions, dependency edges, peer checks, diagnostics, and JSON map keys are processed in stable order. The canonical encoding uses two-space indentation and one trailing newline.
Schema#
The root version identifies the lockfile schema and is currently 0. Flog is
not yet released, so this is the first and only supported schema. A lock
contains:
targets: explicitly represented npm-style OS, CPU, and optional libc targets. Each target references the root (.) and every workspace importer snapshot and records only package-snapshot overrides that differ by target;importers: shared importer snapshots with original dependency specifiers preserved by dependency field, exact package resolutions, and derivedpeerProvidersedges;workspaces: local package names, versions, and repository-relative link paths; andpackages: shared immutable registry snapshots containing version, tarball URL, integrity, regular, optional, and derived peer-provider edges, peer constraints and metadata, effective peer context, resolution context, and platform restrictions.
Identical importer and package snapshots are encoded once. Package membership is reconstructed from each target's importer roots and dependency edges. If the same importer path or logical package ID has target-specific edges, each distinct snapshot receives a deterministic content-hash suffix and only the affected target references that variant. This keeps common graph data shared while preserving optional selection and peer-provider differences exactly.
An importer or package edge stores both its original specifier and exact target.
Aliases therefore retain values such as npm:typescript@^7.0.2 while resolving
to the actual typescript snapshot. Workspace edges use
workspace:<relative-path> and never query the registry.
Registry snapshot IDs include a deterministic SHA-256 context suffix only when
an ancestor peer provider can affect that package or one of its descendants.
This prevents two otherwise identical package versions from sharing incorrect
child edges under conflicting peer environments, while packages without peer
context retain the readable name@version identity.
Tarball URLs are required to be HTTP(S), may not contain user information, and are recorded without query strings or fragments. Tokens and machine-specific cache paths are never written to the lock.
Selection behavior#
For each registry range, flog chooses the highest valid npm SemVer version that
satisfies the range. npm prerelease range semantics come from the shared SemVer
implementation. A registry dist-tag such as latest or next selects the exact
version named by that package's dist-tags metadata; the lock records that
version, so locked and offline installs do not silently follow later tag
changes. npm: aliases select ranges or tags from their actual package name.
workspace: dependencies must target a discovered local package and explicit
ranges must accept its local version.
Production dependencies take precedence over duplicate development
dependencies, and optional dependencies override regular dependencies with the
same name. Overridden importer entries retain their original specifier with a
shadowed marker, while only the effective field carries a resolution.
Development dependencies are resolved for every importer. Registry package
development dependencies are not installed.
Peer dependencies are validated against deterministic parent/ancestor provider
sets. Explicit providers take precedence. When a required provider is absent,
flog selects the highest registry version satisfying every peer range in that
environment, or the matching local workspace for a workspace: peer, and
records its exact placement in peerProviders. Incompatible explicit providers
and unsatisfiable shared constraints fail with stable importer, consumer, range,
and dependency-chain diagnostics. Peers marked optional in
peerDependenciesMeta may be absent and are not automatically installed, but
an installed provider must still satisfy their range. Peer-sensitive transitive
graphs get distinct package snapshots.
Locked validation reconstructs each environment's required-peer closure from its resolved declared consumers. Every derived provider must be reached by that closure, with providers allowed to introduce further required peers; orphan providers and self-supporting orphan peer cycles are rejected rather than materialized.
os, cpu, and libc metadata supports positive entries, negated entries, and
any. An incompatible required package fails resolution. An incompatible or
unavailable optional package remains in its dependency field with a stable
skipped reason and no package snapshot. Optional providers are stabilized
before required peer decisions; if an optional subtree is skipped, it cannot
satisfy a peer and every dependent peer check is recomputed without it.
Locked workflow#
Commit the generated lock and use plain flog install in development and CI.
An existing lock is parsed strictly, checked against every workspace manifest
and the current target, and installed without registry metadata requests or
lockfile writes. A missing lock, stale lock, or unsupported target fails rather
than being silently created or reconciled.
Dependency changes are deliberate: flog add, flog remove, and
flog upgrade update manifests and the exact lock together. After a manual
manifest edit, use flog lock to preserve compatible locked versions and write
the reconciled graph without installing it.
Target matrices#
Declare the complete portable target matrix while explicitly generating the
lock. Repeat --target in npm-style os/cpu[/libc] form:
flog lock \
--target linux/x64/glibc \
--target linux/x64/musl \
--target linux/arm64/glibc \
--target linux/arm64/musl \
--target darwin/x64 \
--target darwin/arm64 \
--target win32/x64 \
--target win32/arm64
amd64 is accepted as an alias for x64, and windows as an alias for
win32. Linux targets must declare glibc or musl; other operating systems
must omit libc. An explicit matrix replaces the previous declaration, so the
same command converges to byte-identical output on every host. Without explicit
targets, lock reconciliation preserves and refreshes the existing matrix.
Named dependency installs and flog upgrade also resolve every represented
target before replacing manifests or the lock; they never collapse the matrix
to the invoking host. A failure in any target leaves all project files
unchanged.
Plain flog install selects only the current target graph. A represented target
does not access registry metadata or rewrite the lock; an unsupported target
fails and lists those represented. Downloads, offline cache checks, bins, and
lifecycle ordering are limited to packages reachable from the selected graph.
flog lock may generate any complete matrix from any host.
Only an explicit integer "version": 0 is accepted. A missing or null
version is not interpreted as zero, and no earlier development schema is read
or rewritten by install or lock.
Before the version-zero decision, development builds briefly wrote target-matrix
locks labeled version 2. That matrix shape is the current version-zero shape, so
the coordinated migration is an operator-reviewed one-line relabel: retain a
backup, change only the root version from 2 to 0, verify that this is the only
diff, and then run plain flog install for each represented target. Do not use
Flog to rewrite the file. Earlier development locks labeled version 1 used a
different single-target shape and must instead be backed up, removed, and
regenerated with an explicit complete --target matrix; review every resolution
against the backup before committing it.
Unknown lockfile fields, unsupported versions, incomplete records, dangling
edges, and additional JSON values are rejected. Default mode reports the exact
importer field that differs from package.json and directs the operator to
flog lock.