feat: tighten minimumReleaseAge — auto-exclude, lockfile verification, and interactive prompt#11705
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThis PR implements end-to-end collection, confirmation, and enforcement of immature package picks through the pnpm install pipeline. Resolution policy violations (minimum release age, trust downgrade) are detected inline during npm package selection, collected during dependency tree resolution, surfaced to an optional handler hook for approval or rejection, persisted to workspace manifest auto-excludes in loose mode, and verified against the lockfile on subsequent installs when strict mode or frozen-lockfile is enabled. ChangesImmature picks + verifier
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related issues
Possibly related PRs
Poem
✨ Finishing Touches🧪 Generate unit tests (beta)
|
When `minimumReleaseAgeStrict` is off, loose-mode picks (the lowest-version fallback and the peekManifestFromStore fast path that reuses lockfile-pinned versions) are now captured and persisted to `minimumReleaseAgeExclude` in pnpm-workspace.yaml. A single info message lists what was added so users see the bypass becoming explicit on disk instead of staying silent.
The verifier no longer skips when `minimumReleaseAgeStrict` is false. Now that loose-mode auto-collect persists every accepted-immature pin to `minimumReleaseAgeExclude`, the verifier can require every lockfile entry to be either mature or on the exclude list — same contract strict mode enforces. Lockfiles produced under a weaker or absent policy that still hold immature entries are rejected the same way they would be in strict mode. Also tightened the resolver-side `onImmaturePick` callbacks so an entry whose specific version is already on the exclude list isn't re-announced as immature on every install.
testDefaults<T>'s generic inference loses the InstallOptions intersection when T contains a callback field that's not in its explicit allowlist. Spread the testDefaults result and add onImmaturePick afterward — typecheck sees the full InstallOptions shape and the test exercises the same path.
In strict mode + TTY, instead of throwing ERR_PNPM_NO_MATURE_MATCHING_VERSION on the first immature pick (which forces the discover-by-loop dance described in #10488), the resolver now collects every immature direct and transitive in one pass and pnpm prompts the user with the full list before peer-dep resolution runs. Approve → install proceeds, the new entries are persisted to minimumReleaseAgeExclude at the end (same path loose mode uses). Decline → resolution aborts before the lockfile, package.json, or modules dir are touched. Strict mode without a TTY (CI) keeps today's fail-fast behavior. Implementation: - New \`deferImmatureDecision\` flag threaded through the resolver chain. When set, pickRespectingMinReleaseAge falls back to the lowest version (instead of throwing in strict mode) so every immature pick fires onImmaturePick. - New \`confirmImmaturePicks\` checkpoint in resolveDependencies, invoked between resolveDependencyTree and resolvePeers. Throws to abort. - setupImmaturePicks in installing/commands wires the prompt: TTY detection, enquirer confirm with default=No, ERR_PNPM_MINIMUM_RELEASE_AGE_DENIED on decline.
`(pkg) => { picks.push(pkg) }` triggered @stylistic/brace-style
(single-line block body) and `(pkg) => picks.push(pkg)` infers
`=> number` which collides with the void return type elsewhere
in the type intersection. `(pkg) => void picks.push(pkg)` avoids
both — the `void` operator pins the expression to void without a
block.
The prepareFixtures step runs \`pnpm install -rf --frozen-lockfile\` across every fixture. Some fixtures (e.g. workspace-external-depends-deep, has-outdated-deps) have lockfiles pinned to \`@pnpm.e2e/*\` packages from the local registry-mock, which aren't on npmjs.org. The v11 default \`minimumReleaseAge: 1440\` spins up the lockfile verifier — and with verification now running in loose mode, the verifier fail-closes on the un-checkable entries. The actual minimumReleaseAge code paths have their own dedicated unit and e2e tests, so disabling the policy on the fixture scaffolding keeps the build green without losing coverage.
Four review fixes: 1. --no-save preserves the workspace manifest. drainImmaturePicks no longer logs and clears the collector when opts.save === false; the next install resurfaces the picks as expected. 2. pickFromSimpleRegistry forwards onImmaturePick and deferImmatureDecision. JSR + named-registry resolutions now feed the same auto-collect / prompt path as plain npm. 3. setupImmaturePicks uses ci-info's isCI in addition to the TTY check (overridable per-call), so CI runners that allocate a TTY still get strict-mode fail-fast behavior. 4. Verifier policy snapshots minimumReleaseAgeExclude and canTrustPastCheck rejects when today's list isn't a superset of the cached one. Removing an exclude entry forces re-verification so the now-uncovered immature pin is flagged. Plus: targeted tests for each (--no-save e2e, JSR onImmaturePick unit, ci=true + TTY unit, cache-invalidation e2e), and the pacquet-parity note in the changeset.
The previous superset-based canTrustPastCheck was correct for the hole the review described (removing an entry forces re-verify), but the layered reasoning — "trust the cache when today's list is a superset of the cached one" — invites future bugs and was unnecessary for the security-sensitive cache contract. Switch to a byte-for-byte JSON.stringify match on a sorted+deduped snapshot. Cache identity now equals policy identity: any change to \`minimumReleaseAgeExclude\` invalidates the cached run, including additions. The extra re-verification on additions is rare (users mostly add entries after a failure, not as a routine), and removes the class of bypasses where a previously-approved version stays trusted after its exclude entry has been pulled.
The CI-detection check in setupImmaturePicks imports \`ci-info\` from src/, so it needs to be a runtime dependency rather than a devDependency. Lint flagged this with import-x/no-extraneous-dependencies.
The verifier already re-checks minimumReleaseAge on every lockfile entry to catch policy violations that bypassed fresh resolution (peek path, --frozen-lockfile, restored CI cache). The trust check had the same blind spot: failIfTrustDowngraded was only running at resolver time, so a downgraded entry that was committed before the policy was enabled (or that resolved through the peek path on a later install) would never trip the gate. createNpmResolutionVerifier now activates when either minimumReleaseAge OR trustPolicy='no-downgrade' is set, fetches the full packument once per package, and runs both checks against the same metadata. Failures map to MINIMUM_RELEASE_AGE_VIOLATION or TRUST_DOWNGRADE codes respectively. trustPolicyExclude / trustPolicyIgnoreAfter pass through with the same semantics they have at resolution time. The cache policy snapshot records the trust fields too — any change to trustPolicy / trustPolicyExclude / trustPolicyIgnoreAfter invalidates the cached run (consistent with the exact-match rule already used for minimumReleaseAgeExclude). Plumbing: ClientOptions / ResolutionVerifierFactoryOptions / CreateNewStoreControllerOptions all forward the three trust fields to the verifier factory. Two e2e tests cover the lockfile-bypass case and the exclude-list short-circuit.
f7cb61c to
f59fab0
Compare
|
@zkochan thanks for this, looks great for local users! What about non-interactive mode also discovering all immature transitive dependencies for Renovate and similar? (non-interactive environments) Otherwise, with strict projects Renovate will still be stuck in the loop of |
|
This PR also updates the non-strict install (with minimumReleaseAgeStrict=false) to automatically add all exclusions to pnpm-workspace.yaml. So they could use that. |
Four review issues addressed: 1. Recursive --no-save no longer drops the auto-excludes silently — the drain runs inside the opts.save !== false guard like installDeps does, and the unconditional fallback write at the recursive workspace-level path is gone. 2. The trust verifier no longer fast-paths on "any attestation." That shortcut would have let through a downgrade from trustedPublisher (rank 2) to provenance (rank 1) — a case resolver-time failIfTrustDowngraded correctly rejects (see trustChecks.test.ts:261). The check now always fetches the full packument and runs failIfTrustDowngraded — same fidelity the resolver enforces. The per-version attestation cache is removed along with the unsafe path. 3. The abbreviated-modified shortcut for the maturity gate now requires the pinned version to be present in the abbreviated metadata's `versions` map. Without that check an unpublished or never-published pin could pass on a stale package-level `modified` timestamp. 4. pnpm-lock.yaml refreshed for the ci-info dep move (devDependencies → dependencies of installing/commands). Plus tests: - New `resolving/npm-resolver/test/createNpmResolutionVerifier.test.ts` with focused unit coverage: trustedPublisher → provenance downgrade is rejected, same-evidence provenance passes, abbreviated shortcut bails on missing version, canTrustPastCheck invalidates on exclude-list shrinkage. - Recursive --no-save e2e test mirroring the single-project one.
✅ Actions performedReview triggered.
|
CodeRabbit-found gap + safety net for the policy-handler contract:
* The lockfile verifier (`createNpmResolutionVerifier`) didn't accept
`ignoreMissingTimeField`, so on registries that strip per-version
`time` the verifier failed closed while the resolver picker (via
`pickMatchingVersionFinal`) warn-and-skipped. Same install could
resolve successfully and then fail verification. The verifier now
takes the flag, warns once via the shared `warnMissingTimeFieldOnce`,
and passes the entry when set. Threaded through
`ResolutionVerifierFactoryOptions` and `ClientOptions` so
`minimumReleaseAgeIgnoreMissingTime` flows from the user config
(via createNewStoreController) all the way to the verifier.
* `resolveDependencies` in deps-resolver now throws
ERR_PNPM_RESOLUTION_POLICY_VIOLATIONS_UNHANDLED if violations fire
but no `handleResolutionPolicyViolations` callback was wired. The
policy contract is "every pick that trips a check produces a
violation that gets handled"; a missing handler silently let
policy-rejected versions land in the lockfile. Tests that
inspected `result.resolutionPolicyViolations` without a handler
now wire an explicit no-op (the contract is "acknowledge or
abort", not "ignore").
Tests added:
- createNpmResolutionVerifier passes the entry under
ignoreMissingTimeField when no source surfaces a publish
timestamp.
- resolveDependencies throws when violations fire and no handler
is wired.
optimisticRepeatInstall short-circuits via checkDepsStatus, which compares the current config to the WorkspaceStateSettings snapshot persisted in node_modules/.pnpm-workspace-state-v1.json. The list of tracked keys didn't include minimumReleaseAge / trustPolicy / their exclude lists / ignore-after / ignoreMissingTime — so turning a policy on, shrinking an exclude list, etc. left the workspace state "clean", the optimistic short-circuit fired, and the verifier fan-out never got a chance to re-check the lockfile against the new policy. Add the policy keys to WORKSPACE_STATE_SETTING_KEYS so checkDepsStatus correctly reports "not up to date" when any of them changes; the verifier's own per-lockfile cache keeps the cheap- repeat case cheap once the install actually runs. Revert the diagnostic in pnpm/test/install/misc.ts now that the "Already up to date" output has been surfaced and the underlying issue is fixed.
The lockfile-resolution verifier writes its per-content-hash result cache to <cacheDir>/lockfile-verified.jsonl. `pnpm store prune` is documented as "prune unreferenced packages and clean the cache" but only ran the store's own prune + the dlx-cache cleaner — the verifier cache file was left behind, which `test/store/storePrune.ts` caught by asserting cacheDir is empty after prune. Add a `cleanLockfileVerifiedCache(cacheDir)` helper alongside `cleanExpiredDlxCache`. Silent on a missing file so prune stays idempotent. The cache file name is duplicated by hand on both sides (here and in verifyLockfileResolutionsCache.ts); a shared package would be heavier than tracking one stable string in two places.
… in new test fixtures origin/main advanced with four PRs touching files this branch also changes: - #11679 (fix: silence verify-deps auto-install output) — TS only, no conflict with pacquet/. - #11705 (feat: tighten minimumReleaseAge) — TS only, no conflict. - #11708 (refactor(pacquet): optional DI pattern + documentation) — renames the test-DI seam (`Api` generic → `Sys`, `RealApi` → `Host`, `FsReadString` → `FsReadToString`, fakes drop `*Api` suffix) across modules-yaml / cmd-shim / config / reporter / package-manager / cli, and adds a "Dependency injection for tests" section to `pacquet/CODE_STYLE_GUIDE.md` plus a corresponding rule 7 in `pacquet/AGENTS.md`. Most files auto-merged cleanly with this branch's single-letter renames; only `pacquet/crates/patching/Cargo.toml` needed a manual resolve where this branch added `[lints] workspace = true` and main added `text-block-macros` to dev-dependencies — combined. - #11710 (test(pacquet): coverage) — adds 44 unit + integration tests. Two of the new closures (`|v|` in `registry/package/tests.rs:84` and `|e|` in `lockfile/save_lockfile/tests.rs:337`) tripped `perfectionist::single_letter_closure_param` because they predate this branch's enablement of the rule; renamed to `|version|` / `|entry|` to keep dylint at zero warnings post-merge. Verified: `cargo check --workspace --tests --locked` clean, `cargo dylint --all -- --all-targets --workspace` clean, `cargo fmt --check` clean.
* feat: report lockfile verification progress The lockfile resolution verifier introduced in #11705 runs an unbounded registry round-trip on cache miss and was previously silent — on a cold registry cache users saw nothing for several seconds. Emit pnpm:lockfile-verification log events (started/done) around the actual verification pass and render them in the default reporter as a transient progress line that collapses into a final "verified" summary with entry count and elapsed time. The cached short-circuit stays silent. * feat: include lockfile path in verification log and render when non-standard Add `lockfilePath` to the `pnpm:lockfile-verification` event payload so consumers always know which lockfile a `started`/`done` pair refers to. In the default reporter, render the path in the message only when the lockfile lives outside the workspace root (or, for non-workspace installs, outside cwd) — the common case stays uncluttered, while custom `lockfileDir` setups now surface in the verification line. * feat: name what the lockfile verification actually checks in the rendered message "Verifying lockfile" was opaque about *what* was being verified. Reword the rendered messages to explicitly name the check ("supply-chain policies"), so users on a cold-cache pause understand what's happening instead of just seeing the pause. * fix: skip lockfile verification emission for empty candidate set A non-empty lockfile.packages whose snapshots all fail name/version extraction would still emit a "Verifying lockfile (0 entries)" line even though no verifier work runs. Bail before emission when the candidate map is empty so the no-op branch stays silent, matching the contract for the other no-op branches (empty verifiers, no lockfile.packages). * fix(reporter): always close out the verifying-lockfile frame Address two Copilot review points on #11712: 1. The verifier emitted `started` but no terminal event when violations were found or when the registry fan-out threw, leaving "Verifying lockfile…" as the last frame for that block in ansi-diff mode (and an unmatched line in CI logs). Add a `failed` status to the logger, wrap the fan-out in try/finally so a terminal event is emitted on every exit path that emitted `started`, and render a brief failure line so the spinner-style frame is replaced before the PnpmError block prints. 2. The path-suppression heuristic used strict `===` between path.dirname(lockfilePath) and expectedDir, which broke on trailing separators and slash-direction differences. Switch to a path.relative-based check so a workspaceDir like `/repo/` or a Windows path with mixed slashes still correctly suppresses the redundant "at <path>" suffix. * docs: update lockfile verification logging behavior The lockfile verifier now emits log events during the registry round-trip pass, improving user visibility into the process.
@zkochan with "you", I guess you mean the end user? and I guess "the whole list of such packages" are the Git additions in the PR to so an end user would need to review each $ pnpm list '@next/env' --depth Infinity --lockfile-only --json
[
{
"name": "security-vulnerability-examples-next-js-postgres",
"dependencies": {
"next": {
"version": "16.2.6",
"dependencies": {
"@next/env": {
"version": "16.2.6"
}
}
}
}
}
]
# Passing example - `@next/env` is a transitive dep of `next`
# -> it should be allowed to be added to minimumReleaseAgeExclude
$ pnpm list '@next/env' --depth Infinity --lockfile-only --json \
| jq -e '
def contains_package($name):
(.dependencies // {}) as $deps
| ($deps | has($name)) or any($deps[]; contains_package($name));
any(.[].dependencies["next"]?; contains_package("@next/env"))
'
true
# Failing example - `css-tree` is NOT a transitive dep of `next`
# -> it NOT should be allowed to be added to minimumReleaseAgeExclude
$ pnpm list css-tree --depth Infinity --lockfile-only --json \
| jq -e '
def contains_package($name):
(.dependencies // {}) as $deps
| ($deps | has($name)) or any($deps[]; contains_package($name));
any(.[].dependencies["next"]?; contains_package("css-tree"))
'
falsethis assumes a hypothetical security PR similar to upleveled/security-vulnerability-examples-next-js-postgres#395 (but with the full and instead of the end user, I guess way better would be if Renovate could run that command / tool instead, as part of the PR creation... 🤔 |
yes |
Adds a regression test verifying that `pnpm outdated` honors `minimumReleaseAge` by not surfacing immature newer versions as available upgrades. The underlying fix already shipped as part of pnpm#11705 (which removed `strictPublishedByCheck` entirely and routed maturity decisions through `policyViolation`); this lands the dedicated test from the original repro PR (pnpm#11699).
Adds a regression test for #11698. The underlying fix already shipped as part of #11705 (which removed `strictPublishedByCheck` entirely and routed maturity decisions through `policyViolation`), so this PR now lands only the dedicated test that locks in the behavior. ## What the test covers `deps/inspection/commands/test/outdated/minimumReleaseAge.test.ts`: - **Baseline** — without an age policy, `pnpm outdated` reports `is-negative@2.1.0` as an available upgrade (sanity check that the fixture actually has outdated deps). - **Regression** — with `minimumReleaseAge` set to a cutoff so far in the past that every published version is immature, `pnpm outdated` reports nothing: `exitCode === 0` and `2.1.0` does not appear. Before #11705 this test went red because the non-strict resolver fallback re-picked the immature `latest` ignoring `publishedBy`. The `allImmatureMinimumReleaseAge = Date.now() / (60 * 1000)` trick (cutoff = epoch in minutes) is date-independent and matches the technique already used in the install-side `minimumReleaseAge` suite. ## Why a test-only PR The original PR proposed flipping `strictPublishedByCheck` in `createManifestGetter`, but #11705 deleted that option entirely and replaced it with an always-defer model (`policyViolation` flows through `ResolveResult` → `getManifest` returns `null` on `MINIMUM_RELEASE_AGE_VIOLATION`). The test was the durable contribution; preserving it as a regression gate is worth keeping.
Summary
Three coordinated changes that close the silent-bypass gap in loose
minimumReleaseAgemode AND the discover-by-loop UX problem in strict mode (#10488), plus a parallel hardening of the lockfile verifier:Auto-collect into
minimumReleaseAgeExclude(loose mode) — fresh resolutions that fall back to a version newer than the cutoff are auto-recorded into the workspace manifest'sminimumReleaseAgeExclude. A single info message lists what was persisted. The workspace manifest writer dedupes against existing entries.Lockfile verifier runs in loose mode too —
createNpmResolutionVerifierno longer gates onminimumReleaseAgeStrict. With auto-collect keeping the exclude list explicit, every accepted-immature pin must be on the list — same contract strict mode enforces. Lockfiles produced under a weaker (or absent) policy that still hold immature entries are rejected the same way strict mode would.Strict mode prompts on the aggregate set instead of throwing on the first — the resolver always collects every immature direct and transitive in one pass; the install command's
handleResolutionPolicyViolationscheckpoint decides what to do with the set. Interactive (TTY) prompts the user once with the full list (default = No) and asks whether to add them all tominimumReleaseAgeExcludeand proceed. Approve → install continues, persisted at the end. Decline → resolution aborts before the lockfile, package.json, or modules dir is touched. Non-interactive (CI) keepsERR_PNPM_NO_MATURE_MATCHING_VERSIONas the exit code but lists every offending entry instead of just the first one the resolver happened to hit.The lockfile verifier now also covers
trustPolicy: 'no-downgrade'. The same post-resolution gate that re-checksminimumReleaseAgeon lockfile entries now re-runsfailIfTrustDowngradedfor every npm-registry entry whose name isn't ontrustPolicyExclude. The two checks share a single full-metadata fetch per package, so the extra coverage doesn't cost an extra round trip when both policies are active. Resolver-time trust checks still run as before — this just closes the gap when an entry bypasses resolution (peek path,--frozen-lockfile, restored CI cache).The steady-state flows:
pnpm add foo@immature: lockfile clean, verifier no-op, resolver picks via lowest-version fallback,foo@immaturelands inminimumReleaseAgeExclude, install succeeds. Subsequentpnpm install --frozen-lockfilein CI verifies against the populated list and succeeds.next@15.5.9: resolver collectsnext@15.5.9AND every immature@next/swc-*@15.5.9shim. pnpm prompts once with the full list. User approves → install completes, all entries persisted inpnpm-workspace.yaml. CI then runs the populated config cleanly.ERR_PNPM_NO_MATURE_MATCHING_VERSIONlisting every immature entry'sname@versionand publish time — no more discover-by-loop dance.ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION(orERR_PNPM_TRUST_DOWNGRADE); a batch that trips both policies escalates to the genericERR_PNPM_LOCKFILE_RESOLUTION_VERIFICATIONand lists each entry's per-policy code in the breakdown.Implementation
ResolveResult.policyViolationinstead of throwingNO_MATURE_MATCHING_VERSION.deferImmatureDecisionandstrictPublishedByCheckare gone — every caller (install, dlx, outdated, self-update) inspects the violation and decides what to do.policyViolationflows fromResolveResult→PackageResponse.body.policyViolation→ a shared accumulator inResolutionContext→ theresolutionPolicyViolationsfield onresolveDependencyTree's return → out throughmutateModules/addDependenciesToPackageto the install command.@pnpm/resolving.resolver-baseasResolutionPolicyViolation; the npm resolver exports the two built-in codes (MINIMUM_RELEASE_AGE_VIOLATION_CODE,TRUST_DOWNGRADE_VIOLATION_CODE) as constants so consumers reference one source of truth.handleResolutionPolicyViolationsruns betweenresolveDependencyTreeandresolvePeers— the resolver-agnostic checkpoint where the install command's plan prompts (TTY) or aborts (no-TTY) with the full violation list.setupPolicyHandlers(ininstalling/commands/src/policyHandlers.ts) composes per-policy handlers behind a uniform plan interface: each handler has its ownhandleResolutionPolicyViolations(filter by code, decide what to do) andpickManifestUpdates(return a typedWorkspaceManifestPolicyUpdatespatch the install command spreads intoupdateWorkspaceManifest). Today the only registered handler iscreateMinimumReleaseAgeHandler— strict + TTY prompts viaenquirer, strict no-TTY throwsERR_PNPM_NO_MATURE_MATCHING_VERSIONwith every entry listed, loose mode auto-persists at the tail. Strict +--no-saveis rejected up-front viaERR_PNPM_STRICT_MIN_RELEASE_AGE_REQUIRES_SAVE. Future policies plug in via a sibling factory + push into the handlers list, with no changes toinstallDeps.ts/recursive.ts.installDeps/recursivedrainpickManifestUpdatesafter install and spread the patch intoupdateWorkspaceManifest. Plainpnpm install(no--update, no params) now still updates the workspace manifest when any handler contributes a patch. Theinstallcommand's CLI schema gainedsave: Booleanso--no-saveactually flows through toopts.save = falseinstead of being silently dropped by nopt.makeResolutionStrict(ininstalling/client) wraps aResolveFunctionand rethrows anypolicyViolationas aPnpmError. Used bydlxandself-updateunder strictminimumReleaseAgeORtrustPolicy: 'no-downgrade', since one-shot callers have nowhere to defer a violation to. Violation-code → error-code mapping lives in one place so future violation kinds get consistent UX.createNpmResolutionVerifierextends its check totrustPolicy: 'no-downgrade'— same per-entry fan-out, same cache key, sharing the full-metadata fetch with the maturity check. Trust-fetch errors now propagate up so the violation reason carries the underlying message (network code, 404 detail) instead of a generic "metadata is unavailable".verifyLockfileResolutions's aggregate throw uses the per-policy code when every violation in the batch shares it, and escalates to a genericLOCKFILE_RESOLUTION_VERIFICATION(with per-entry codes in the breakdown) for mixed batches.trustPolicy: 'no-downgrade'(ERR_PNPM_TRUST_POLICY_INCOMPATIBLE_WITH_AGENT) — the agent has no server-side counterpart to that check yet, so silently allowing it would land a lockfile the local verifier would later reject.minimumReleaseAgeis forwarded to the agent and enforced server-side, so that combination is fine.Pacquet parity
Pacquet only carries a stub reference to
minimumReleaseAgeExclude(seepacquet/crates/package-manager/src/version_policy.rs); the broaderminimumReleaseAgeandtrustPolicypolicies aren't ported yet, so this feature is outside pacquet's current surface area. It'll come along when pacquet ports the policies.Closes
withTransitivesoption #10488 (resolves the discover-by-loop dance for security bumps without needingwithTransitives).Test plan
installing/deps-installer/test/install/minimumReleaseAge.ts): fresh immature pick surfacesresolutionPolicyViolations, lockfile verifier rejects unlisted entries in loose mode, loose-mode entries already on the exclude list pass, exclude list short-circuits the violation report,handleResolutionPolicyViolationsdecline aborts before lockfile write, approval lets install proceed.installing/commands/test/policyHandlers.ts): TTY detection, strict-no-TTY plan throws with the full violation list, loose-mode noop on the hook, strict +--no-saverefusal, manifest-patch shape, dedup.makeResolutionStrict(installing/client): violation →NO_MATURE_MATCHING_VERSIONmapping, no-throw when the resolver returns no violation.createNpmResolutionVerifier: trustedPublisher → provenance downgrade detection, same-evidence-level pass, abbreviated meta shortcut requires the pinned version to exist, exclude-list canTrustPastCheck rejects shrinkage. Trust-fetch errors surface the underlying message in the violation reason.verifyLockfileResolutions: single-code batch keeps the per-policy code, mixed-code batch escalates toLOCKFILE_RESOLUTION_VERIFICATIONwith per-entry codes in the breakdown.pnpm/test/install/minimumReleaseAge.ts,pnpm/test/dlx.ts): verifier rejects unlisted entries in loose mode, fresh-resolution auto-add (incl. transitives), dlx error message format on strict immature pick, round-trip clean install once exclude list is populated, recursive + non-recursive--no-saveleaves the workspace manifest untouched.Written by an agent (Claude Code, claude-opus-4-7).
Summary by CodeRabbit
Release Notes
New Features
Improvements