Skip to content

fix(cli): sync alias data loss, remove --dry-run side effects, cache clean guard - #725

Merged
Kamirus merged 1 commit into
v3from
kamil-claude/v3-cmd-fixes
Aug 12, 2026
Merged

fix(cli): sync alias data loss, remove --dry-run side effects, cache clean guard#725
Kamirus merged 1 commit into
v3from
kamil-claude/v3-cmd-fixes

Conversation

@Kamirus

@Kamirus Kamirus commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

mops sync could silently delete a dependency. Given the pinned-alias layout the docs recommend:

map = "9.0.1"
"map@8.1.0" = "8.1.0"

sync built its "used" set from imports (mo:map@8.1.0/Mapmap@8.1.0) but its "declared" set through getDepName, which collapses both keys to map. The two sets lived in different namespaces, so the alias was reported as both missing and unused. Adding it split on @ and wrote map = "8.1.0", clobbering 9.0.1; and because both sets are computed before any mutation, one run could add and then remove, leaving the project with no map at all.

$ mops sync
Before: map = "8.1.0"        # 9.0.1 overwritten, alias key gone
After:  map = "9.0.1"        # unchanged
        "map@8.1.0" = "8.1.0"

It evaded detection because mops sync has no test coverage at all — cli.test.ts notes 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 sync defects came out of covering it: packages imported only from test/bench sources 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; and moc --print-deps was spawned twice per file because the used-set was recomputed for the missing and unused queries independently.

mops remove --dry-run was not a dry run

It deleted local cache directories, and ran checkIntegrity, which rewrites mops.lock whenever it was already stale. It also printed Package removed … as though it had happened.

mops cache clean was broken on Windows, always

The safety guard compared a path.join result against the literal suffix mops/cache. On Windows path.join yields 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_NETWORK is rejected rather than accepted on a suffix match. Separately, when run outside a project getRootDir() returns empty and path.join("", ".mops") resolved to ./.mops in whatever directory the user happened to be standing in — that local delete is now skipped.

Migration

mops cache clean still removes the project's .mops as documented; the new --global opts 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

…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>
@github-actions

Copy link
Copy Markdown
Contributor

Cursor AI review

👍 APPROVE — looks safe to merge

Verified the three command fixes against base behavior, add(..., asName) (same path as update.ts), installMopsDep/getDepName for aliases, and the new tests/docs/changelog/skills sync.

Category Assessment Details
Summary Fixes mops sync pinned-alias set arithmetic (and related prod/dev classification), makes mops remove --dry-run side-effect free, and hardens mops cache clean path guards; adds --dry-run / --global plus docs, changelog, skills, and tests.
Code Quality Reuses add's existing asName (same pattern as update.ts); extracts computeSyncPlan / parseImportedPackage / assertGlobalCacheDir; drops unused addCache (no remaining callers).
Consistency Dry-run wording matches other CLI dry-runs; sibling docs (05-mops-sync.md, 03-mops-cache.md), cli/CHANGELOG.md ## Next, --help options in cli/cli.ts, and .agents/skills/mops-cli/SKILL.md updated together.
Security Traced assertGlobalCacheDir containment + network-segment stripping vs old suffix endsWith; dry-run now returns before writeConfig / syncLocalCache / checkIntegrity in remove.ts; local .mops delete skipped when getRootDir() is empty.
Tests Alias/dev plan covered in sync-plan.test.ts; cache guard + --global in cache-clean.test.ts; remove dry-run disk/lock assertions in remove-dry-run.test.ts (targeted toMatch/toBe, appropriate for these paths).
Maintainability Planning logic is pure and unit-tested; cache safety is named and separator-agnostic; changelog documents the user-visible fixes clearly.

Verdict

Decision: APPROVE
Risk: Low
Reason: Contained CLI bugfixes outside install/resolve/integrity/backend; alias and dry-run/cache guards match intended behavior and are backed by focused tests, docs, and changelog.


Generated for commit f677966

@automation-sa-sre automation-sa-sre left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated approval: the AI review verdict for f677966 is APPROVE. See the "Cursor AI review" comment for details.

@Kamirus
Kamirus merged commit d4850f4 into v3 Aug 12, 2026
26 checks passed
@Kamirus
Kamirus deleted the kamil-claude/v3-cmd-fixes branch August 12, 2026 11:49
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants