= npm → pnpm migration audit
== Executive result
The repository is not yet migration-ready: there is no tracked pnpm-lock.yaml, pnpm-workspace.yaml, or packageManager declaration. The active contract is npm + Lerna, with the root package.json and package-lock.json symlinked to a selected variants/<solution>/ tree.
A safe migration should begin with one explicitly defined workspace boundary and package-manager bootstrap. Do not replace every npm occurrence at once: the repository contains a root variant workspace, nested workspaces, independent applications, Docker packaging contracts, and a Solana/Anchor project that explicitly requires npm.
The audit was read-only. No files, builds, installs, locks, or dirty worktree files were changed.
== Inventory
package.json files and 32 tracked package-lock.json files.
variants/{bpx,carbon,cix,crypto,da,imperium,prediction}/.
package.json is a symlink to variants/da/package.json; the root lock is likewise variant-backed in the working tree.
lerna.json remains the build runner’s package graph tool.
^24.13.1 and npm ^11.8.0; .npmrc sets engine-strict=true.
The variant workspace lists are broad and variant-specific. scripts/build.sh:145-174 changes root symlinks when the solution variant changes. .github/workflows/per-commit-build.yml:70-89 instead copies each variant manifest and lock to the root for validation, then deletes them. This is a central design constraint, not incidental setup.
== Findings
=== High: workspace and lockfile model is undefined
scripts/package_lock_updater.sh:3-26 copies a variant lock to the root, runs npm i, optionally runs npx lerna, then exports the generated lock back to variants/<variant>/. custom-update_package_lockfiles.yml:23-60 runs this for all solution variants and commits the result. per-commit-build.yml:70-89 validates every variant with npm’s npm ls --all --package-lock-only through scripts/validate_lockfile.sh.
A pnpm migration must decide between:
Simply adding a root pnpm-workspace.yaml will not preserve the current seven-lock workflow. It may also accidentally include packages that the current variant does not build.
=== High: npm workspace semantics are embedded in scripts
The main build path uses npm ci, npm ls, npm pkg get, npm run ... -w, and npx lerna in scripts/build.sh:20-35,262-299 and p8-ui/scripts/variant_prebuild.sh:27-45. Deployment helpers use npm clean-install -w, npm run build -w, and --omit=dev in p8-deployments/scripts/build_utils.sh:342-359.
The E2E tree is especially sensitive. p8-e2e/package.json declares nested workspaces including api, fix, gui, both frameworks, and two workspaces outside the directory. Its scripts run separate installs in p8-e2e/api/scripts/update-and-run.sh:75-109 and p8-e2e/fix/scripts/update-and-run.sh:74-83. The API script deliberately avoids npm ci --ws because strict validation of every member exposes an unresolvable dev dependency; that behavior must be reproduced or simplified deliberately, not translated mechanically.
Other workspace-shaped areas include p8-refdata, p8-common-extension, p8-trader-ui, and p8-ui. p8-ui/e2e/package.json has a file:../../p8-e2e/p8-e2e-api-framework dependency. p8-e2e/nft/package.json also uses local file: dependencies. These need explicit pnpm linking and packaging tests.
=== High: deployment and release artifacts assume npm files and layout
p8-deployments/scripts/build_utils.sh, common.sh, release_manager.sh, lambda_env_change.sh, account/environment provisioning scripts, and p8-starter-pack/app_build_commands.sh invoke npm installs directly. The release path copies npm metadata in p8-chain-engine/deployments/pre_publish.sh:22-45, including the EVM root package-lock.json and the subgraph package-lock.json.
The EVM subgraph has an intentional exception: p8-chain-engine/p8-chain-engine-evm/subgraph/common/deploy.sh:46-63 says npm install, rather than npm ci, is needed because Graph CLI peer dependencies resolve differently under npm 11 CI. This is a migration blocker requiring an isolated proof with pnpm, or a documented decision to keep that project on npm.
Production installs also need a layout decision. pnpm’s symlinked/virtual-store node_modules must not leave release tarballs or Docker build contexts referring to files outside the copied package. Prefer validating an isolated deployable directory (or a deliberately used pnpm deploy --prod flow) before changing packaging.
=== High: Docker files expose explicit lockfile contracts
p8-fix-gateway/p8-test-server/Dockerfile:7-13 explicitly copies package.json package-lock.json, runs npm install, and starts with npm. p8-events-listener/Dockerfile:7-8 documents the package-lock wildcard convention. p8-refdata/p8-refdata-engine/Dockerfile, p8-starter-pack/Dockerfile, and p8-system-manager/p8-system-manager-agent/Dockerfile copy prebuilt application trees, so their behavior depends on how deployment packaging materializes node_modules.
If these images stay npm-based, retain their locks and treat them as standalone projects. If they move to pnpm, copy the pnpm lock/config and install with frozen mode, or package a self-contained production tree before Docker build. Do not assume a pnpm workspace symlink survives a source-pack or image boundary.
=== High: CI has npm cache and lockfile assumptions
per-commit-build.yml:70-89,190-201, custom-deploy-gui.yml:44-91, custom-test.yml:124-126, custom-long-run-test.yml:46-48, and custom-trading-bot.yml:35-37 key caches from package-lock.json and cache ~/.npm. These need pnpm store paths and the correct root/variant/nested lock inputs.
The build and CI runner Dockerfiles (p8-deployments/utils/docker-images/build-runner/Dockerfile:70 and cicd-runner/Dockerfile:72) install and verify Node/npm but do not provision a pinned pnpm executable. The deployment prerequisite script p8-deployments/scripts/setup-pre-requisites.sh:212-217 likewise checks npm only. A workflow-side assumption that pnpm happens to be present would make clean runners non-reproducible.
Pin pnpm in package.json using packageManager, and provision that exact version in runner images or via an explicit, pinned setup action. Do not rely on an unpinned global install or implicit Corepack availability.
=== Medium: npm/npx are used as tool resolvers, not just script runners
The repository uses npx for Lerna (scripts/build.sh, p8-ui/scripts/variant_prebuild.sh, p8-e2e/scripts/deploy_gui.sh), Playwright (p8-e2e/gui, p8-ui/e2e), Hardhat (p8-chain-engine/p8-chain-engine-evm/run_tests.sh and compile scripts), Graph CLI (subgraph/common/deploy.sh), Allure, dotenv-cli, Rspack, and Sonar. npx --yes can download tools at execution time, so replacing it with pnpm dlx would preserve non-reproducibility and may change registry/auth behavior. Prefer declared package binaries via pnpm exec; use pnpm dlx only for intentionally pinned, one-shot tools.
Many package scripts recursively invoke npm run, including API prebuilds, common JS, E2E, and legacy UI packages. p8-digital-asset-module/dependency_fence.sh:12-30 is more fragile: it requires npm’s npm_config_local_prefix and executes ./node_modules/.bin/depcruise from the workspace root. It is wired as postbuild in every DAM package. Validate the environment variables and binary location under pnpm, or simplify the script to use the invoking package manager’s exec path.
p8-chain-engine/p8-chain-engine-solana/Anchor.toml runs npm exec -- ts-mocha, and p8-chain-engine/p8-chain-engine-solana/per_commit.sh:20-25 explicitly checks for npm. This should remain an npm boundary unless Anchor’s package-manager behavior is separately proven and intentionally changed.
=== Medium: registry and config compatibility needs a policy
Private registry templates such as p8-e2e/p8-e2e-api-framework/.npmrc.template and the many .npmrc.template files under common packages provide JFrog scope/auth settings. p8-ui/.npmrc and p8-ui/e2e/.npmrc contain npm-specific behavior (audit, save-exact, install-links). pnpm can consume npm registry/auth settings, but the generated CI config, scoped registry resolution, auth masking, and local file: links must be tested. Package publishConfig entries also point to JFrog and must remain unchanged unless publishing itself migrates.
== Recommended staged plan
=== Stage 0 — define boundaries without changing installs
=== Stage 1 — prove one non-production slice
Use one variant, preferably da, and the root packages plus one small p8-ui target. Generate a pnpm lock in an isolated worktree/temp checkout, test private package resolution, lifecycle scripts, Lerna replacement, DAM dependency fence, TypeScript builds, and --filter equivalents for current -w calls. Compare produced package contents and dependency versions with npm output.
=== Stage 2 — migrate CI and lock validation
Update runner images/setup, cache store paths, variant matrix handling, lockfile updater, and validation. Make lock generation explicit and non-mutating to the checkout. Run all seven variant lock/graph checks, then per-commit JavaScript builds for at least da, crypto, and cix/carbon solution mapping. Keep npm validation in parallel until parity is established.
=== Stage 3 — deployment packaging and E2E
Test Lambda/service release directories, the EVM/subgraph publish tree, p8-fix-gateway Docker build context, UI deploys, API/FIX/GUI E2E setup, trading bot, and standalone caches. Verify that every artifact is self-contained and that runtime images do not need a workspace root or pnpm store.
=== Stage 4 — expand or retain boundaries
Migrate standalone projects only where pnpm adds value and has a reproducible lock/install path. Keep Solana/Anchor on npm initially, and keep Graph CLI on npm if the peer-resolution issue remains. Remove obsolete npm updater/validator logic only after a full variant and release cycle has passed.
== Validation gates
node_modules.
file: dependencies resolve without network fallback surprises.