You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Wire the npm-registry version-picking surface ported in #11755 into the install command so resolution actually flows through the Resolver chain instead of the legacy Package::fetch_from_registry + Package::pinned_version path.
Yes — currently the only resolution call in install
The legacy path is a plain max-satisfying lookup. It has no latest-tag short-circuit, no preferred-versions bias, no deprecated-fallback, no minimumReleaseAge filter, no in-memory metaCache, no offline / preferOffline support, no conditional GET, and no policy-violation surfacing.
Plan
Phase A — npm resolver bridge
Port parseBareSpecifier (upstream) into pacquet-resolving-npm-resolver. Produces RegistryPackageSpec from (bare_specifier, alias, default_tag, registry). Handles the npm:-alias form, tarball-URL parse, and tag/version/range discrimination via version-selector-type.
Add NpmResolver struct in pacquet-resolving-npm-resolver. Owns:
Surface policy_violation: after the pick, if opts.published_by is set and the picked version's published_at is past the cutoff, populate ResolveResult.policy_violation with the MinimumReleaseAge variant (mirroring upstream's pickRespectingMinReleaseAge fall-back-to-lowest reporting flow).
Phase B — chain wiring
Construct NpmResolver at install entry (config, http_client, auth_headers, meta_cache all available there).
Build DefaultResolver::new(vec![Box::new(NpmResolver { … })]). Other protocols (git, tarball, workspace, etc.) are separate Tier 2 items and stay out of this chain until their crates land.
Hand the DefaultResolver (as a boxed dyn Resolver) into the install path.
Phase C — install refactor (faithful resolveDependencyTree port)
Port pnpm's two-pass approach: build the full resolved tree first (resolve-only, no fetch), then run the fetch+install pass over that tree. Larger refactor than a per-package swap but lines up with eventual lockfile generation, enables sibling dedup, and gives preferred-versions sharing a place to live.
Port resolveDependencyTree (or the minimum slice needed to drive npm-registry resolution end-to-end) into a new pacquet crate, e.g. pacquet-resolving-deps-resolver. Outputs a resolved tree keyed by (name, version) with parent/child edges + per-node ResolveResult.
Refactor install_package_from_registry:
take a pre-resolved ResolveResult (name, version, integrity, tarball URL, manifest) instead of (name, version_range).
drop the Package::fetch_from_registry + pinned_version calls.
use ResolveResult.resolution for the tarball fetch step.
Refactor install_without_lockfile.rs (both call sites at L145 and L299) so the top-level call runs resolveDependencyTree first, then walks the resulting tree calling install_package_from_registry on each node. The recursion in install becomes tree-traversal, not interleaved resolve+install.
Decide where the cross-call PackageMetaCache lives: probably owned by the resolveDependencyTree driver and dropped after the resolve pass completes.
Phase D — preferred-versions construction
Bootstrap preferred_versions from pnpm-lock.yaml snapshots: in --frozen-lockfile mode the lockfile is already loaded for verification, so harvest the existing pins as EXISTING_VERSION_SELECTOR_WEIGHT entries before the resolve pass begins.
Plumb prev_specifier on WantedDependency from the lockfile entry so the resolver can prefer the previously-pinned version when no update was requested.
Phase E — policy-violation surfacing
Add a collector on the install side that aggregates ResolveResult.policy_violation across all resolves, modeled after upstream's handleResolutionPolicyViolations and the existing lockfile-verifier's violation flow.
On strict-mode minimumReleaseAge, abort install with the collected set; on non-strict, surface as warnings (matching the verifier's split).
Phase F — tests
Unit tests on NpmResolver:
parseBareSpecifier parity across the upstream test corpus (resolving/npm-resolver/test/parseBareSpecifier.test.ts is already in TEST_PORTING.md)
scope-based registry picking
ResolveResult shape verification
policy_violation population on immature picks
Unit tests on the resolveDependencyTree driver: dedup across siblings, preferred-versions propagation, error aggregation.
Integration test driving install through the chain via the mocked registry — byte-identical node_modules/ to a pnpm install of the same lockfile.
Update pacquet/plans/TEST_PORTING.md checkboxes.
Phase G — docs
Update crates/resolving-npm-resolver/src/lib.rs module-doc to reflect that the resolver is now active in install.
Note the wiring in pacquet/AGENTS.md if any agent-facing guidance needs to change.
Out of scope
Other-protocol resolvers (git, tarball, local, jsr, named-registry, workspace). Each is a separate Tier 2 item under Rust Roadmap #11633 and adds an entry to the DefaultResolver chain when it lands.
Abbreviated metadata fetcher. Pacquet currently fetches full metadata only — documented in feat(pacquet): resolver scaffold + npm version picking #11755. Adding an abbreviated fetcher is a follow-up: Accept-header swap, second mirror dir at v11/metadata, fullMetadata: bool flag through the orchestrator, and resurrection of the abbreviated→full upgrade-for-releaseAge path that's already structurally in place.
dry_run no-disk-side-effects. Today's fetcher still warms the on-disk mirror under dry_run; restoring upstream parity needs a write-bypass flag on the fetcher.
Lockfile generation. Pacquet's install today is --frozen-lockfile only; writable-lockfile installs are a separate roadmap item, though Phase C's two-pass shape is a prerequisite.
Acceptance criteria
install_package_from_registry no longer calls Package::fetch_from_registry or Package::pinned_version. Resolution flows entirely through the Resolver chain.
DefaultResolver has at least one resolver registered (NpmResolver).
A pacquet install --frozen-lockfile of a package whose lockfile entry was resolved by pnpm produces byte-identical node_modules/ (modulo timestamps), verifying parity through the chain.
minimumReleaseAge violations on resolution (not just lockfile verification) surface through the install layer with the same exit code / reporter events as the verifier path.
All existing pacquet-package-manager tests continue to pass.
Goal
Wire the npm-registry version-picking surface ported in #11755 into the install command so resolution actually flows through the
Resolverchain instead of the legacyPackage::fetch_from_registry+Package::pinned_versionpath.Part of #11633 Stage 2.
Current state (after #11755)
Library code exists; nothing in
package-managerorclicalls it.Resolvertrait + dispatcher contractpacquet-resolving-resolver-base::resolveDefaultResolverchainpacquet-resolving-default-resolverparse_wanted_dependencypacquet-resolving-parse-wanted-dependency(alias, bareSpecifier)split; no protocol routing yet)pick_package/pick_package_from_metapacquet-resolving-npm-resolverNpmResolutionVerifierpacquet-resolving-npm-resolverPackage::pinned_versionpathpacquet/crates/package-manager/src/install_package_from_registry.rs:97-108The legacy path is a plain max-satisfying lookup. It has no
latest-tag short-circuit, no preferred-versions bias, no deprecated-fallback, nominimumReleaseAgefilter, no in-memory metaCache, no offline / preferOffline support, no conditional GET, and no policy-violation surfacing.Plan
Phase A — npm resolver bridge
parseBareSpecifier(upstream) intopacquet-resolving-npm-resolver. ProducesRegistryPackageSpecfrom(bare_specifier, alias, default_tag, registry). Handles thenpm:-alias form, tarball-URL parse, and tag/version/range discrimination viaversion-selector-type.NpmResolverstruct inpacquet-resolving-npm-resolver. Owns:registryURL + per-scoperegistriesmap (pacquet_config::Registries)named_registriesArc<ThrottledClient>+Arc<AuthHeaders>Arc<dyn PackageMetaCache>(shared across resolves so one install warms one cache)cache_diroffline,prefer_offline,ignore_missing_time_fieldResolver::resolveonNpmResolver:parseBareSpecifieronwanted_dependency.bare_specifier(usewanted_dependency.alias+opts.default_tag)None→Ok(None)(decline, chain continues to next resolver)pick_registry_for_package(already exists innamed_registry.rs)PickPackageOptionsfromResolveOptions(preferred-versions, pick-lowest, etc.)pick_packagePickPackageResult→ResolveResult:id=PkgNameVer,latest=meta.dist_tag("latest"),published_at=meta.published_at(version),manifest= JSON,resolution=LockfileResolution::TarballResolution { integrity, tarball },resolved_via = "npm-registry",normalized_bare_specifier,aliasResolver::resolve_latestonNpmResolver(forpnpm outdated/pnpm update --latest).policy_violation: after the pick, ifopts.published_byis set and the picked version'spublished_atis past the cutoff, populateResolveResult.policy_violationwith theMinimumReleaseAgevariant (mirroring upstream'spickRespectingMinReleaseAgefall-back-to-lowest reporting flow).Phase B — chain wiring
NpmResolverat install entry (config, http_client, auth_headers, meta_cache all available there).DefaultResolver::new(vec![Box::new(NpmResolver { … })]). Other protocols (git, tarball, workspace, etc.) are separate Tier 2 items and stay out of this chain until their crates land.DefaultResolver(as a boxeddyn Resolver) into the install path.Phase C — install refactor (faithful
resolveDependencyTreeport)Port pnpm's two-pass approach: build the full resolved tree first (resolve-only, no fetch), then run the fetch+install pass over that tree. Larger refactor than a per-package swap but lines up with eventual lockfile generation, enables sibling dedup, and gives preferred-versions sharing a place to live.
resolveDependencyTree(or the minimum slice needed to drive npm-registry resolution end-to-end) into a new pacquet crate, e.g.pacquet-resolving-deps-resolver. Outputs a resolved tree keyed by(name, version)with parent/child edges + per-nodeResolveResult.install_package_from_registry:ResolveResult(name, version, integrity, tarball URL, manifest) instead of(name, version_range).Package::fetch_from_registry+pinned_versioncalls.ResolveResult.resolutionfor the tarball fetch step.install_without_lockfile.rs(both call sites at L145 and L299) so the top-level call runsresolveDependencyTreefirst, then walks the resulting tree callinginstall_package_from_registryon each node. The recursion in install becomes tree-traversal, not interleaved resolve+install.PackageMetaCachelives: probably owned by theresolveDependencyTreedriver and dropped after the resolve pass completes.Phase D — preferred-versions construction
preferred_versionsfrompnpm-lock.yamlsnapshots: in--frozen-lockfilemode the lockfile is already loaded for verification, so harvest the existing pins asEXISTING_VERSION_SELECTOR_WEIGHTentries before the resolve pass begins.prev_specifieronWantedDependencyfrom the lockfile entry so the resolver can prefer the previously-pinned version when no update was requested.Phase E — policy-violation surfacing
ResolveResult.policy_violationacross all resolves, modeled after upstream'shandleResolutionPolicyViolationsand the existing lockfile-verifier's violation flow.minimumReleaseAge, abort install with the collected set; on non-strict, surface as warnings (matching the verifier's split).Phase F — tests
NpmResolver:parseBareSpecifierparity across the upstream test corpus (resolving/npm-resolver/test/parseBareSpecifier.test.tsis already in TEST_PORTING.md)ResolveResultshape verificationpolicy_violationpopulation on immature picksresolveDependencyTreedriver: dedup across siblings, preferred-versions propagation, error aggregation.node_modules/to a pnpm install of the same lockfile.pacquet/plans/TEST_PORTING.mdcheckboxes.Phase G — docs
crates/resolving-npm-resolver/src/lib.rsmodule-doc to reflect that the resolver is now active in install.pacquet/AGENTS.mdif any agent-facing guidance needs to change.Out of scope
DefaultResolverchain when it lands.v11/metadata,fullMetadata: boolflag through the orchestrator, and resurrection of the abbreviated→full upgrade-for-releaseAge path that's already structurally in place.p-limit(1)per-mirror serialization. Atomic rename insave_metacovers write safety today.dry_runno-disk-side-effects. Today's fetcher still warms the on-disk mirror underdry_run; restoring upstream parity needs a write-bypass flag on the fetcher.--frozen-lockfileonly; writable-lockfile installs are a separate roadmap item, though Phase C's two-pass shape is a prerequisite.Acceptance criteria
install_package_from_registryno longer callsPackage::fetch_from_registryorPackage::pinned_version. Resolution flows entirely through theResolverchain.DefaultResolverhas at least one resolver registered (NpmResolver).pacquet install --frozen-lockfileof a package whose lockfile entry was resolved by pnpm produces byte-identicalnode_modules/(modulo timestamps), verifying parity through the chain.minimumReleaseAgeviolations on resolution (not just lockfile verification) surface through the install layer with the same exit code / reporter events as the verifier path.pacquet-package-managertests continue to pass.References
pickPackage.ts/pickPackageFromMeta.tsatf657b5cb44parseBareSpecifier.tsatf657b5cb44resolveDependencyTree.tsatf657b5cb44createResolverchain at3687b0e180Written by an agent (Claude Code, claude-opus-4-7).