Breaking changes deferred past 3.0.0, now that it has shipped. Non-breaking work that can ship in a 3.x minor lives here or in TODO.md.
CLI (ic-mops) and the backend canister version independently — group accordingly.
Refs: GH = caffeinelabs/mops, LIN = Linear ticket title.
This file is forward-looking only. Shipped breaking changes are recorded in cli/CHANGELOG.md, not here.
Recorded so they are not re-litigated. Each was considered and deliberately not built.
- v3 keeps today's version model. Bare
1.2.3inmops.tomlmeans exact, conflicts resolve max-wins, freshness comes from caret-boundedmops update.=/range syntax in published packages needs backend validator changes plus an ecosystem forward-compat window (older CLIs cannot parse published spec strings), and caret-without-escape-hatch is a bad intermediate state. Motoko dep graphs are shallow, so exact pins plus one-command update is good enough — and the coherent time to adopt cargo's model is when moc--overridelands, enabling the full jump at once. See Cargo version model below. - No supported-range policy for PocketIC. Only a floor exists (
MIN_POCKET_IC_VERSION9.0.0 incli/commands/toolchain/pocket-ic-versions.ts), framed as a migration guard, not a compatibility policy:< 9.0.0pins genuinely worked in 2.x viapic-ic, so without it an upgrade turns into an inscrutableBinTimeoutError. There is no ceiling and no clamping oflatest. mops applies no version gating tomoc,wasmtimeorlintoko— the only version check in the CLI is[requirements], which surfaces a dependency-declared minimum — so a mops-maintained list of blessed pocket-ic versions would be a new and inconsistent policy, and would make our release cadence a gate on DFINITY's. A pin newer than anything we have tried is simply used; if the protocol has moved, the client's own error says so. - No silent default for
[toolchain] pocket-ic. A silent default is a frozen contract (bumping 14 → 15 would change every unpinned project's replica) and the one exception to "toolchain versions are pinned".RECOMMENDED_POCKET_IC_VERSIONis hint-only. The offline-runtime guarantee (Caffeine requirement) holds via an explicit pin plus the cached-check indownload()— warm the cache from the pin during a Docker image build. --lockeddoes not re-walk the dependency graph to byte-compare a freshly computed lock. It cannot: a lock-driven install skips the versions that lost a conflict, so their manifests are never cached andresolvePackages({skipLock: true})throws ENOENT on a fresh clone. Instead it checks that the lock is present, parseable, current-format, pins every dependency declared inmops.toml, and agrees with the registry on every file hash. Closing the gap needs a per-candidate manifest fetch — a new feature, not a lockfile change.mops sourcesdeliberately has no--locked. Its stdout is machine-parsed mid-build; enforce the lock with a precedingmops install --locked.mops updatekeeps its name. Under exact pins only one update operation can exist, so there is noupdate-vs-upgradeambiguity. If ranges land, that is whenmops upgradegets added alongside a redefinedmops update.v2is an archive, not a release line. The branch is kept so 2.x source stays reachable without digging through tags, but nothing releases from it:release.ymlrequires every tag to be onmain, so acli-v2.*tag aborts. That is intentional — no 2.x releases are planned, and a second release path is surface for nothing. If a 2.x patch ever becomes necessary, the guard has to be widened first (see the closed #777 for the shape of that change), and its docs would need porting tomainto publish, sincev2has no docs-deploy step.- Keep
cli/tests/build/no-dfx/andbuild-no-dfx.test.tsas a regression test that mops works with neitherdfxnoricpon PATH.
- GH #651 —
@dfinity/picis a devDependency pre-bundled intodist/vendor/pic.mjs, because itspostinstalldownloads a ~94 MB pocket-ic binary and throws when it cannot (a plaindependenciesentry makesnpm i -g ic-mopsfail with no network). If upstream makes that download lazy, the vendor entry, thevendor:picstep, thefix-distrewrite andcli/tests/vendor-pic.test.tsall get deleted, and dev/CI installs stop paying the 94 MB. - GH #655 — an upstream stderr /
onCanisterLoghook would delete theserverProcesscast incli/helpers/pocket-ic-client.ts.
Cargo version model (gated on moc --override — caffeinelabs/motoko#5124)
Adopt cargo's model end-to-end as one coherent jump once moc supports multiple versions of a package in a build:
- Flip bare
1.2.3to mean^1.2.3in resolution + sources + install.mops update/outdatedalready do this (cli/commands/available-updates.ts:53-56has the// Caret (cargo-style)comment). Purely client-side reinterpretation of existing registry data — no backend migration. - Resolver finds the intersection of all constraints, picks the highest satisfying version. Fail loudly on unsatisfiable. No warning on within-major skew — that's the point of caret.
=1.2.3escape hatch. Backend work: dep-version validation must accept spec strings (validateConfig.mo:248), the dep-exists check becomes "some version satisfies the spec" (PackagePublisher.mo:106-118), alias prefix check fix (validateConfig.mo:262), display paths likegetPackageSummary.mo:105. Non-breaking canister upgrade (accepts a superset), deployable ahead of time. Ecosystem forward-compat window needed: older CLIs can't parse published=strings — tolerate-parse (strip)=in 3.x CLIs early to shorten the window.- Different-major diamonds → multi-version coexistence via moc
--override. (GH #283) - Decide on
^/range syntax (and document the difference from npm). (LIN: Mops support ^versions) - Dev-dep / prod-dep separation: today root
[dev-dependencies]merge into the same flat resolved set (cli/resolve-packages.ts:86-91). Cargo separates them. Do alongside this resolver rewrite. - Workspaces (cargo
[workspace]/ npm workspaces). Shared lockfile root, member graph, single-resolve across in-repo packages. Significant design chunk; high payoff for monorepos. - Migration risk: library authors not committing lock will see within-major drift. Bounded because lock is default since 2.8, and v3's cross-major warning will have flagged conflicted graphs long before.
Both are blocked on the same thing — a cross-version conflict cannot actually be resolved today without moc --override (GH #283), so louder reporting gives noise without a proportional action. Full detail in GH #723.
0.xminor skew is never reported. Conflict reporting buckets by semver major, sobase 0.7.3vs0.16.0both bucket to0. Cargo treats0.MINORas the compatibility unit, and most of the Motoko ecosystem lives at0.x.- Conflict reporting is inert once
mops.lockis fresh. Resolution short-circuits tolock.depsbefore the reporting block runs, so warnings appear exactly once. Fix: re-derive version sightings fromlock.graphat the short-circuit. Related to GH #683.
- Install-as alias — table-stakes (cargo
package = "...", npm"foo": "npm:bar@1"). Schema +--packageplumbing change. (GH #266) - Local-path deps no longer require copy-pasting transitives into the parent's
mops.toml. (GH #289) - Expose the replica/PocketIC canister id to tests. (GH #274)
- Doc pages for
mops searchandmops template, which have never had one in either tree. (GH #205)
The Cargo/pnpm parity backlog — store/CAS, cache GC, --offline, mops why/tree/licenses, registry metadata cache, comment-preserving mops.toml writes — is tracked in GH #723. Nothing there is breaking.
Lock format versions independently of the CLI (cargo precedent: cargo is perpetually 1.x, yet shipped lock v1→v4 via read-early/write-later). Ship in two non-breaking-at-the-time phases whenever the work starts:
- Read support in any regular release — extend
supportedVersions(cli/integrity.ts:232), parse/verify the new fields, write nothing. - Flip the write default in a later release, once the installed base has absorbed read support.
Content: pin git deps to a resolved commit SHA. Today the deps map (cli/integrity.ts:288-303) stores the ref as written (repo#v1.0.0) — a moved tag goes unnoticed; only registry packages get file hashes (cli/integrity.ts:69-86). Resolve ref → SHA at lock (re)generation, verify at install. Path deps stay exempt by design (live-edited dirs; cargo precedent).
A version bump (not in-place v3 extension) is required regardless: older CLIs fail v3's exact-match deps check and hashes count check on any added fields, and the unsupported-version error (cli/integrity.ts:253) is a cleaner, recoverable failure. Note that no bump is needed for additive optional fields or sorted output on their own — validation never inspects key order or unknown fields.
- True Node-less binary distribution (single executable, no
node_modules). Todaynpm i -g ic-mopsand thecli-releasesinstall.shboth end up shelling tonpm add -g <tgz>, so any Node-runtime / native-module bug hits both. Node SEA,bun build --compile, or Rust rewrite (GH #237) eliminates this whole class of install failures. (LIN: investigate publishing standalone binary) - Rust CLI rewrite — defer or commit. (GH #237)
- Migrate
backend/main/main-canister.motopersistent actor(EOP). Storage canister already is. Define migration for ~20stable var *Stable : [(K, V)]arrays. - Migrate
mo:base→mo:coreacross allbackend/**/*.mo(21 files). (GH #354) - Store package file blobs in stable memory in storage canisters. (GH #18)
- Drop legacy
PackageConfigV2fields:documentation,homepage,donation,scripts,dfx,moc. (backend/main/types.mo:73-80) The CLI already rejectsdfxin[package]at publish but still sendsdfx: ""for wire compatibility. - Drop legacy
PackageSummary.owner/ownerInfo(useowners[]). (types.mo:92-93,getPackageSummary.mo:46-47) - Drop legacy
packageOwnersmap; keep onlyownersByPackage. (main-canister.mo:68) - Drop legacy
hasDocumentationflag. (types.mo:182) - Collapse
PackageConfigV2/V3/V3_Publishinginto a single current type. (types.mo:62-89)
- Resolve dependency tree on the backend. (GH #19)
- A certified hash endpoint for
getFileHashesByPackageIds, which ispublic sharedtoday and pays consensus. Do not simply make it aquery: a query reply is signed by one node, package bytes are already query-served, and that combination lets a single malicious node forge both. The legitimate version is a certified-data tree overhashByFileId, verified client-side. (getFileHashesQueryexists and is intentionally unused; a test fails if anything calls it.) getHighestSemverBatchtraps at 100+ direct dependencies (assert(list.size() < 100)), taking downmops outdatedandmops update. Paginate or return an error instead of trapping.- Store packages as compressed tarballs with a single integrity hash; cuts storage, the per-file fan-out and per-install round-trips. (GH #291, GH #716, LIN: optimize package storage)
- Provide an incremental package index for offline/
mops verify. (GH #291) - Reject Git/path deps in published packages (allow for dev-deps only). (GH #291)
- Publisher signatures. Hashes are computed by the registry canister itself, so registry compromise is total — there is no trust anchor independent of it. Sigstore-style signing needs new canister state and API.
yank/deprecate/unpublish(with npm-style time/dependent restrictions). (GH #291) Priority raised by the no-=decision — yank is the systemic answer to "patch release X is broken" without exact pins.
- Add canister upgrade test. (GH #169)
semver.mo: support pre-release tags1.2.3-pre.1(backend/main/utils/semver.mo:70) — bundle with the spec-validator work from the cargo-model track sosemver.mois touched once.PackagePublication.user→userId. (backend/main/types.mo:47)
mops promote— vendor-into-source workflow. (GH #281)- Publishing non-
.mofiles. (GH #217) - Coverage reports. (GH #45)