Skip to content

toolchain(pnpm): add non-frozen fresh-resolution canary for registry, override and lockfile integrity #570

Description

@qnbs

Context

PR #567 exposed a latent repository-wide dependency-resolution defect that ordinary WorldScript development and CI had not detected:

# pnpm-workspace.yaml — previous state
typescript: "npm:@typescript/typescript6@~6.0.3"

Routine repository operations used --frozen-lockfile, so pnpm trusted the already-existing lockfile and never had to resolve that impossible registry target. A genuine non-frozen pnpm install finally proved that @typescript/typescript6@6.0.3 had never existed.

The same fresh resolution also removed a stale vulnerable browserslist@4.28.2 graph that had begun failing the enforced OSV Security Audit after new advisories were published.

This incident demonstrates a real gap:

Frozen-lockfile correctness proves reproducibility of the current lockfile, but does not prove that current dependency policy can still produce that lockfile from registry metadata.

WorldScript needs both guarantees.

Existing related owners:

This issue owns fresh dependency resolution integrity, not package provenance or automated dependency upgrades.

Goal

Add a deterministic, low-risk fresh-resolution canary that periodically and on dependency-policy changes proves:

package.json
pnpm-workspace.yaml
.npmrc
workspace manifests
registry metadata under current policy
        ↓
can still produce a valid, internally consistent dependency graph

without mutating main, executing untrusted lifecycle scripts, or turning normal PR CI into an uncontrolled dependency-upgrade generator.


Required distinction

The repository should deliberately maintain two different dependency checks:

Frozen authority check

pnpm install --frozen-lockfile

Answers:

Can this exact committed lockfile be installed reproducibly?

This remains the normal merge/release dependency authority.

Fresh-resolution canary

A disposable, non-frozen resolution answers:

If the lockfile had to be regenerated today under the committed dependency policy, is the policy still valid and resolvable?

These checks are complementary.

Do not replace the frozen install with an always-floating install in ordinary CI.


Canary execution model

Run in a clean disposable environment/worktree/container with:

  • exact committed package manifests/configuration;
  • repository-pinned Node version;
  • repository-pinned pnpm version;
  • no persisted node_modules;
  • no pre-existing lockfile resolution state influencing the resolver beyond the explicit test mode;
  • no credentials beyond public registry access;
  • no publish/deploy/write permissions.

Prefer a mode that can resolve dependencies without executing package lifecycle scripts.

At minimum evaluate:

--ignore-scripts

and ensure .pnpmfile.cjs / other repository-controlled resolver hooks are either intentionally included as part of the policy being verified or intentionally disabled with a second dedicated check. Do not accidentally create a canary that tests a different resolver policy than production.


What the canary must detect

A. Unresolvable dependency policy

Examples:

  • nonexistent package/version;
  • impossible npm alias;
  • incompatible override;
  • invalid peer/engine policy where configured to enforce it;
  • package removed/unavailable from the configured registry.

Classification:

UNRESOLVABLE_CONFIG

This is a real repository-toolchain defect.

B. Lockfile drift

Fresh resolution succeeds but produces a graph different from the committed lockfile.

Classification:

LOCKFILE_DRIFT

This is not automatically a bug: semver ranges, registry metadata and minimum-release-age policy can legitimately select newer compatible versions.

The canary must report the diff in a bounded artifact/summary instead of automatically committing it.

C. Temporary infrastructure failure

Examples:

  • npm registry outage;
  • DNS/network failure;
  • rate limiting;
  • transient CDN corruption.

Classification:

REGISTRY_OR_NETWORK_FAILURE

Do not misclassify network unavailability as repository dependency corruption.

D. Security-policy interaction

If fresh resolution selects a package rejected by current security policy (minimumReleaseAge, blocked exotic dependency, lifecycle build policy, OSV/dependency review), surface the exact policy conflict.

Do not automatically weaken the security policy to make resolution succeed.


Trigger policy

Use a layered cadence.

Dependency-policy PR trigger

For PRs that change any resolver-authoritative input, run a fresh-resolution sanity check.

At minimum include:

package.json
pnpm-workspace.yaml
pnpm-lock.yaml
.npmrc
workspace package manifests
patches / pnpm patch metadata

The exact path set must be re-derived from current repository configuration.

A PR directly modifying resolver policy should not be allowed to merge with UNRESOLVABLE_CONFIG merely because its hand-edited/frozen lockfile still installs.

Scheduled/main canary

Run periodically on unchanged main to catch registry ecosystem drift.

Prefer daily or weekly according to measured noise/cost.

Coordinate cadence with #448/#550 rather than attaching the full expensive CI matrix.

Manual diagnostic

Provide a documented command/workflow-dispatch for maintainers investigating dependency resolution.


Lockfile handling

The canary MUST NOT push or commit a regenerated lockfile automatically.

In its disposable environment it may create a candidate lockfile and compare it to committed pnpm-lock.yaml.

Report:

candidate resolution succeeded / failed
package count delta
added package/version nodes
removed package/version nodes
changed peer-resolution keys
changed override outcomes
security-relevant changes

Large peer-key normalization such as the one observed after #567 should be distinguishable from intentional direct dependency upgrades.

Do not reduce the result to a raw multi-thousand-line diff if a structured summary can explain the semantic graph changes.


Minimum-release-age semantics

WorldScript intentionally uses a release-age quarantine policy.

The fresh-resolution canary must use the same policy unless running an explicitly named diagnostic comparison.

A package newer than the quarantine threshold should not become evidence that the committed graph is broken merely because it is not yet eligible.

If the resolver supports different results depending on current wall-clock time, record:

run timestamp
pnpm version
registry
minimumReleaseAge configuration

for reproducibility.


Lifecycle/build-script safety

This canary exists to ask the resolver whether a graph is valid. It should not unnecessarily execute third-party install scripts.

Coordinate with #529's allowBuilds/lifecycle-script enforcement work.

If a later stage intentionally validates build-script allowlisting, make that a separately named step with explicit sandbox/trust assumptions; do not blur it into the resolver check.


Regression tests

Add deterministic tests/fixtures around the repository tooling that prove the canary detects at least:

  • nonexistent npm alias target like the fix(toolchain): restore reproducible TypeScript 6 dependency resolution #567 incident;
  • impossible version override;
  • valid fresh resolution;
  • frozen lockfile still installable after a valid regeneration;
  • lockfile drift without treating it automatically as repository corruption;
  • temporary command/network failure classification where practical to simulate;
  • dependency-policy path detection.

Do not make tests depend on a future registry package remaining absent forever; use local/mock fixtures where practical for the classification logic.


Security and permissions

Scheduled/PR fresh resolution should run with:

contents: read

and no release/package/deployment write scopes.

Do not expose repository secrets to third-party lifecycle scripts.

Do not allow a pull request to modify the canary and then use that modified code as privileged authority without respecting existing workflow-policy/trust-boundary rules (#506/#510 as applicable).


Relationship to automation / dependency bots

This issue does not require adopting another dependency bot.

If Renovate/Dependabot already operates, the canary complements it:

bot/update tooling
= proposes newer graph

fresh-resolution canary
= proves committed resolver policy remains constructible

Do not create automated update PR churn merely because a fresh candidate graph differs.


Acceptance criteria

Non-goals

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions