fix(cli): sync alias data loss, remove --dry-run side effects, cache clean guard - #725
Merged
Conversation
…clean guard mops sync compared imports against alias-stripped manifest keys, so a pinned alias like "map@8.1.0" was reported as both missing and unused. Adding it overwrote the base package's version and a single run could remove the dependency entirely. Aliases are now matched verbatim and written under their own key. sync also gains --dry-run, classifies test/bench-only imports as dev dependencies, removes an unused package from both sections when declared in both, and runs moc --print-deps once per file instead of twice. mops remove --dry-run deleted local cache directories and rewrote a stale mops.lock, and reported removals as done. mops cache clean compared a path.join result against a forward-slash suffix, so it failed with "Invalid cache directory" on every Windows run and for every network-scoped cache directory. The replacement guard is separator-agnostic and strictly narrower — it also requires containment in the global cache root. It no longer targets ./.mops when run outside a project, and gains --global. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This was referenced Aug 12, 2026
Contributor
Cursor AI review👍 APPROVE — looks safe to merge Verified the three command fixes against base behavior,
VerdictDecision: APPROVE Generated for commit f677966 |
automation-sa-sre
approved these changes
Aug 12, 2026
automation-sa-sre
left a comment
There was a problem hiding this comment.
Automated approval: the AI review verdict for f677966 is APPROVE. See the "Cursor AI review" comment for details.
Kamirus
added a commit
that referenced
this pull request
Aug 12, 2026
…cost (#726) A warm-cache `mops install && mops update` — the shape of a CI or agent session start — goes from ~3.8 s to ~2.3 s wall, or **2.2 s → 0.7 s** of actual work once Node's interpreter startup is excluded. A cold install of 8 root packages plus transitives goes **19.7 s → 13.2 s**. Almost all of the warm-path cost was fixed overhead rather than work: - **Install telemetry waited for consensus.** `notifyInstalls` is declared `oneway`, but `@dfinity/agent` only special-cases `query`/`composite_query`, so it took the full update path and was awaited. Submitting the ingress message and acknowledging on acceptance measured **1111 ms → 125 ms**. Simply dropping the `await` would not have helped: the CLI has no `process.exit()` on the success path, so a pending request keeps the event loop alive and the process waits anyway. - **The agent eagerly synchronised time on every invocation**, issuing three `read_state` requests against the ICP ledger canister `ryjl3-tyaaa-aaaaa-aaaba-cai` — a canister mops never otherwise talks to, on a different subnet. Clock skew already self-heals: `call`, `query` and `readState` each catch `IngressExpiryInvalidErrorCode`, re-sync against the mops canister, and retry once. Verified in the installed `@icp-sdk/core` 5.4.0 source before removing the eager sync. - **`mops install` serialised on the API compatibility query** rather than overlapping it. On the cold path, `installDeps` was a plain sequential `for … await`, so every package waited on the previous one's downloads *and* its integrity check. Packages now run through a bounded pool. ## Concurrency budget `installMopsDep` already parallelises files within a package, so a naive package pool would *multiply* with it. The budget is fixed and shared: 16 concurrent fetches (8 in CI), derived as packages × per-package threads, so a single dependency still gets the old 12 threads while four get four each. Transitive levels run at concurrency 1, with the top-level pool alone bounding in-flight work at any graph depth. A global semaphore was rejected because a parent holds its slot while awaiting its children, which deadlocks. ## Correctness of the parallel path The concern with parallelising an installer is dropping or duplicating a package. Two independent cold installs of a fixture graph — four root deps including an alias sharing a package id, plus two nested local levels — produce **byte-identical** `mops.lock` and `.mops` listings, and every locked package is asserted present on disk. A failing package fails the command with a clear message, no unhandled rejection, no leftover staging directories, and siblings still committed. ## Migration `downloadFile` and `downloadPackageFiles` return `Uint8Array` instead of `Array<number>`. This affects programmatic consumers of the `ic-mops` package only; no CLI surface changes. An incompatible CLI now runs the install to completion before the version error surfaces, rather than being gated by it. The message and exit code are unchanged. This is acceptable because the guard was already nominal — `build`, `check`, `check-stable`, `check-candid`, `test`, `bench`, `generate candid`, `sources` and `add` never checked at all; only `install` and `publish` did. `publish` is deliberately left serialised, since it writes immutable registry state. ## What is unchanged The per-package consensus call for download verification is still there — moving it is the next PR in the stack, and it is what takes cold installs the rest of the way down. The persisted "already up to date" stamp was evaluated and **not** implemented: warm install is already ~0.35 s of work on top of a ~0.70 s interpreter-startup floor, and a stale stamp silently skipping a needed install is a worse failure than being slow. ## Scope Second of three stacked PRs splitting the Cargo/pnpm audit work, originally opened as [#724](#724). Stacked on [#725](#725) — review that first. Remaining findings: [#723](#723). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
mops synccould silently delete a dependency. Given the pinned-alias layout the docs recommend:sync built its "used" set from imports (
mo:map@8.1.0/Map→map@8.1.0) but its "declared" set throughgetDepName, which collapses both keys tomap. The two sets lived in different namespaces, so the alias was reported as both missing and unused. Adding it split on@and wrotemap = "8.1.0", clobbering9.0.1; and because both sets are computed before any mutation, one run could add and then remove, leaving the project with nomapat all.It evaded detection because
mops synchas no test coverage at all —cli.test.tsnotes that add/remove/update/sync "all route through the same checkIntegrity code path tested above", which is true of the lock write but not of the set arithmetic that decides what to add and remove.Three smaller
syncdefects came out of covering it: packages imported only fromtest/benchsources were added to[dependencies]; a package declared in both sections was only ever removed from[dependencies], leaving a dangling entry the next run reported again; andmoc --print-depswas spawned twice per file because the used-set was recomputed for the missing and unused queries independently.mops remove --dry-runwas not a dry runIt deleted local cache directories, and ran
checkIntegrity, which rewritesmops.lockwhenever it was already stale. It also printedPackage removed …as though it had happened.mops cache cleanwas broken on Windows, alwaysThe safety guard compared a
path.joinresult against the literal suffixmops/cache. On Windowspath.joinyields backslashes, so the guard could never match and the command failed with "Invalid cache directory" on every run. Network-scoped directories (mops/cache/<network>) matched no clause either.The replacement is separator-agnostic and strictly narrower than what it replaces: it also requires the target to be contained in the global cache root, so a traversing
MOPS_NETWORKis rejected rather than accepted on a suffix match. Separately, when run outside a projectgetRootDir()returns empty andpath.join("", ".mops")resolved to./.mopsin whatever directory the user happened to be standing in — that local delete is now skipped.Migration
mops cache cleanstill removes the project's.mopsas documented; the new--globalopts out. No other behaviour changes for existing invocations.Scope
First of three stacked PRs splitting the Cargo/pnpm audit work, which was originally opened as #724. This one is the self-contained command fixes and touches no install, resolution or integrity code. Remaining audit findings are catalogued in #723.
🤖 Generated with Claude Code