ic-mops 2.15.1 → 2.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/bundle/cli.tgz +0 -0
  3. package/check-requirements.ts +53 -31
  4. package/cli.ts +37 -10
  5. package/commands/bench.ts +23 -5
  6. package/commands/check-stable.ts +20 -3
  7. package/commands/check.ts +6 -1
  8. package/commands/toolchain/index.ts +1 -3
  9. package/dist/check-requirements.js +31 -24
  10. package/dist/cli.js +19 -9
  11. package/dist/commands/bench.d.ts +2 -0
  12. package/dist/commands/bench.js +16 -3
  13. package/dist/commands/check-stable.d.ts +2 -1
  14. package/dist/commands/check-stable.js +8 -2
  15. package/dist/commands/check.js +6 -1
  16. package/dist/commands/toolchain/index.js +1 -3
  17. package/dist/helpers/get-lintoko-version.d.ts +3 -0
  18. package/dist/helpers/get-lintoko-version.js +30 -0
  19. package/dist/helpers/migrations.d.ts +16 -0
  20. package/dist/helpers/migrations.js +71 -1
  21. package/dist/helpers/parse-most.d.ts +5 -0
  22. package/dist/helpers/parse-most.js +28 -0
  23. package/dist/mops.js +2 -2
  24. package/dist/package.json +1 -1
  25. package/dist/tests/check-stable.test.js +15 -0
  26. package/dist/tests/check.test.js +5 -0
  27. package/dist/tests/lint.test.js +1 -1
  28. package/dist/tests/migrations-most.test.d.ts +1 -0
  29. package/dist/tests/migrations-most.test.js +47 -0
  30. package/dist/tests/requirements.test.d.ts +1 -0
  31. package/dist/tests/requirements.test.js +46 -0
  32. package/dist/types.d.ts +1 -0
  33. package/helpers/get-lintoko-version.ts +32 -0
  34. package/helpers/migrations.ts +117 -0
  35. package/helpers/parse-most.ts +32 -0
  36. package/mops.ts +2 -2
  37. package/package.json +1 -1
  38. package/tests/__snapshots__/check-stable.test.ts.snap +11 -0
  39. package/tests/__snapshots__/lint.test.ts.snap +29 -1
  40. package/tests/check-stable/check-limit-warning/deployed.most +8 -0
  41. package/tests/check-stable/check-limit-warning/migrations/20250101_000000_Init.mo +5 -0
  42. package/tests/check-stable/check-limit-warning/migrations/20250201_000000_AddField.mo +9 -0
  43. package/tests/check-stable/check-limit-warning/migrations/20250301_000000_AddD.mo +10 -0
  44. package/tests/check-stable/check-limit-warning/mops.toml +15 -0
  45. package/tests/check-stable/check-limit-warning/src/main.mo +12 -0
  46. package/tests/check-stable.test.ts +24 -0
  47. package/tests/check.test.ts +9 -0
  48. package/tests/lint-extra-example-rules/lint/migration-self-contained/migration-self-contained.toml +7 -0
  49. package/tests/lint-extra-example-rules/migrations/20250101_000000_Init.mo +11 -0
  50. package/tests/lint-extra-example-rules/mops.toml +1 -0
  51. package/tests/lint.test.ts +1 -1
  52. package/tests/migrations-most.test.ts +64 -0
  53. package/tests/requirements-lintoko/mops.toml +5 -0
  54. package/tests/requirements.test.ts +66 -0
  55. package/types.ts +1 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  ## Next
4
4
 
5
+ ## 2.16.0
6
+
7
+ - `mops bench` now compiles benchmark canisters under **enhanced orthogonal persistence** (moc's default) instead of forcing `--legacy-persistence` — measuring the persistence mode real canisters run. Pass `--legacy-persistence` to opt back into legacy persistence.
8
+ - `mops bench --query` measures each cell in a **query** call instead of an update call. Queries run no GC on the IC, so the instruction counts exclude GC work an update would incur — for benchmarking `query`/read-only workloads realistically. Only for synchronous benchmark runners (no inter-canister `await`).
9
+ - `[requirements].lintoko` declares a minimum lintoko version for package consumers. `mops install` (and `mops add`, `mops toolchain use`) warn when the project's lintoko is below a dependency's requirement, same as `moc` (#597).
10
+
11
+ ## 2.15.2
12
+
13
+ - `mops check-stable` (and the stable check inside `mops check`) reports when `[canisters.<name>.migrations].check-limit` is set but more migrations are pending than the limit allows. If the compatibility check failed, the check-limit diagnostic replaces the misleading `moc` error; if it passed anyway, a warning is shown. Compares the deployed `.most` baseline against the local chain; use `--no-check-limit` to suppress.
14
+
5
15
  ## 2.15.1
6
16
 
7
17
  - `mops check --fix` no longer aborts when a fixable file is read-only (e.g. a frozen migration chain file deliberately `chmod`'d to remove write access). The autofixer now skips such files with a warning and continues fixing the rest, instead of crashing the whole run on `EACCES`/`EPERM`.
package/bundle/cli.tgz CHANGED
Binary file
@@ -5,47 +5,69 @@ import chalk from "chalk";
5
5
  import { getDependencyType, getRootDir, readConfig } from "./mops.js";
6
6
  import { resolvePackages } from "./resolve-packages.js";
7
7
  import { getMocSemVer } from "./helpers/get-moc-version.js";
8
+ import { getLintokoSemVer } from "./helpers/get-lintoko-version.js";
8
9
  import { getPackageId } from "./helpers/get-package-id.js";
10
+ import type { Requirements } from "./types.js";
11
+
12
+ type ToolRequirement = keyof Requirements;
13
+
14
+ const TOOL_REQUIREMENTS: {
15
+ tool: ToolRequirement;
16
+ getInstalled: () => SemVer | null;
17
+ }[] = [
18
+ { tool: "moc", getInstalled: getMocSemVer },
19
+ { tool: "lintoko", getInstalled: getLintokoSemVer },
20
+ ];
9
21
 
10
22
  export async function checkRequirements({ verbose = false } = {}) {
11
- let installedMoc = getMocSemVer();
12
- if (!installedMoc) {
13
- return;
14
- }
15
- let highestRequiredMoc = new SemVer("0.0.0");
16
- let highestRequiredMocPkgId = "";
17
23
  let rootDir = getRootDir();
18
-
19
24
  let resolvedPackages = await resolvePackages();
20
- for (let [name, version] of Object.entries(resolvedPackages)) {
21
- if (getDependencyType(version) === "mops") {
22
- let pkgId = getPackageId(name, version);
23
- let depConfig = readConfig(
24
- path.join(rootDir, ".mops", pkgId, "mops.toml"),
25
- );
26
- let moc = depConfig.requirements?.moc;
27
-
28
- if (moc) {
29
- let requiredMoc = new SemVer(moc);
30
- if (highestRequiredMoc.compare(requiredMoc) < 0) {
31
- highestRequiredMoc = requiredMoc;
32
- highestRequiredMocPkgId = pkgId;
25
+
26
+ for (let { tool, getInstalled } of TOOL_REQUIREMENTS) {
27
+ let installed = getInstalled();
28
+ if (!installed) {
29
+ continue;
30
+ }
31
+
32
+ let highestRequired = new SemVer("0.0.0");
33
+ let highestRequiredPkgId = "";
34
+
35
+ for (let [name, version] of Object.entries(resolvedPackages)) {
36
+ if (getDependencyType(version) === "mops") {
37
+ let pkgId = getPackageId(name, version);
38
+ let depConfig = readConfig(
39
+ path.join(rootDir, ".mops", pkgId, "mops.toml"),
40
+ );
41
+ let required = depConfig.requirements?.[tool];
42
+
43
+ if (required) {
44
+ let requiredVersion = new SemVer(required);
45
+ if (highestRequired.compare(requiredVersion) < 0) {
46
+ highestRequired = requiredVersion;
47
+ highestRequiredPkgId = pkgId;
48
+ }
49
+ verbose && _check(tool, pkgId, installed, requiredVersion);
33
50
  }
34
- verbose && _check(pkgId, installedMoc, requiredMoc);
35
51
  }
36
52
  }
37
- }
38
53
 
39
- verbose || _check(highestRequiredMocPkgId, installedMoc, highestRequiredMoc);
54
+ verbose || _check(tool, highestRequiredPkgId, installed, highestRequired);
55
+ }
40
56
  }
41
57
 
42
- function _check(pkgId: string, installedMoc: SemVer, requiredMoc: SemVer) {
43
- let comp = installedMoc.compare(requiredMoc);
44
- if (comp < 0) {
45
- console.log(
46
- chalk.yellow(`moc version does not meet the requirements of ${pkgId}`),
47
- );
48
- console.log(chalk.yellow(` Required: >= ${requiredMoc.format()}`));
49
- console.log(chalk.yellow(` Installed: ${installedMoc.format()}`));
58
+ function _check(
59
+ tool: ToolRequirement,
60
+ pkgId: string,
61
+ installed: SemVer,
62
+ required: SemVer,
63
+ ) {
64
+ if (!pkgId || installed.compare(required) >= 0) {
65
+ return;
50
66
  }
67
+
68
+ console.log(
69
+ chalk.yellow(`${tool} version does not meet the requirements of ${pkgId}`),
70
+ );
71
+ console.log(chalk.yellow(` Required: >= ${required.format()}`));
72
+ console.log(chalk.yellow(` Installed: ${installed.format()}`));
51
73
  }
package/cli.ts CHANGED
@@ -102,14 +102,26 @@ function parseExtraArgs(variadicArgs?: string[]): {
102
102
  // Shared `--help` section describing the enhanced migration `check-limit`
103
103
  // trimming and its override flag, so the limit behaviour is discoverable
104
104
  // from `--help`. `withFix` appends the `--fix` hint for commands that support it.
105
- function enhancedMigrationHelp(withFix: boolean): string {
106
- const example = withFix ? " (e.g. to --fix older migrations)" : "";
107
- return (
105
+ // `withPendingWarning` adds the stable-check pending-migration warning (check / check-stable only).
106
+ function enhancedMigrationHelp(
107
+ options: {
108
+ withFix?: boolean;
109
+ withPendingWarning?: boolean;
110
+ } = {},
111
+ ): string {
112
+ const example = options.withFix ? " (e.g. to --fix older migrations)" : "";
113
+ let text =
108
114
  "\nEnhanced migration ([canisters.<name>.migrations]):\n" +
109
115
  " The canister is checked against its migration chain. [migrations].check-limit\n" +
110
116
  " trims it to the last N migrations (older ones are skipped). Pass --no-check-limit\n" +
111
- ` to use the full chain${example}.`
112
- );
117
+ ` to use the full chain${example}.`;
118
+ if (options.withPendingWarning) {
119
+ text +=
120
+ "\n When check-limit is set, the stable check reports if more migrations are pending\n" +
121
+ " (relative to the deployed .most baseline) than the limit allows — as an error if\n" +
122
+ " compat failed, otherwise a warning.";
123
+ }
124
+ return text;
113
125
  }
114
126
 
115
127
  program.name("mops");
@@ -372,14 +384,17 @@ program
372
384
  .addOption(
373
385
  new Option(
374
386
  "--no-check-limit",
375
- "Check the full migration chain, ignoring [migrations].check-limit",
387
+ "Use the full migration chain, ignoring [migrations].check-limit; also suppresses the pending-migration warning",
376
388
  ),
377
389
  )
378
390
  .addHelpText(
379
391
  "after",
380
392
  "\nArguments after -- are forwarded directly to moc, e.g.:\n $ mops check -- -Werror",
381
393
  )
382
- .addHelpText("after", enhancedMigrationHelp(true))
394
+ .addHelpText(
395
+ "after",
396
+ enhancedMigrationHelp({ withFix: true, withPendingWarning: true }),
397
+ )
383
398
  .allowUnknownOption(true)
384
399
  .action(async (args, options) => {
385
400
  checkConfigFile(true);
@@ -419,14 +434,14 @@ program
419
434
  .addOption(
420
435
  new Option(
421
436
  "--no-check-limit",
422
- "Check the full migration chain, ignoring [migrations].check-limit",
437
+ "Use the full migration chain, ignoring [migrations].check-limit; also suppresses the pending-migration warning",
423
438
  ),
424
439
  )
425
440
  .addHelpText(
426
441
  "after",
427
442
  "\nArguments after -- are forwarded directly to moc, e.g.:\n $ mops check-stable -- -Werror",
428
443
  )
429
- .addHelpText("after", enhancedMigrationHelp(false))
444
+ .addHelpText("after", enhancedMigrationHelp({ withPendingWarning: true }))
430
445
  .allowUnknownOption(true)
431
446
  .action(async (args, options) => {
432
447
  checkConfigFile(true);
@@ -543,6 +558,18 @@ program
543
558
  ),
544
559
  )
545
560
  // .addOption(new Option('--force-gc', 'Force GC'))
561
+ .addOption(
562
+ new Option(
563
+ "--query",
564
+ "Measure each cell in a query call (how `query` methods run on the IC: no GC). Only for benchmarks whose runner is synchronous (no inter-canister calls)",
565
+ ),
566
+ )
567
+ .addOption(
568
+ new Option(
569
+ "--legacy-persistence",
570
+ "Compile benchmark canisters under legacy persistence instead of enhanced orthogonal persistence (the default)",
571
+ ),
572
+ )
546
573
  .addOption(
547
574
  new Option(
548
575
  "--verbose",
@@ -979,7 +1006,7 @@ program
979
1006
  "after",
980
1007
  "\nArguments after -- are forwarded directly to lintoko, e.g.:\n $ mops lint -- --severity warning",
981
1008
  )
982
- .addHelpText("after", enhancedMigrationHelp(true))
1009
+ .addHelpText("after", enhancedMigrationHelp({ withFix: true }))
983
1010
  .allowUnknownOption(true)
984
1011
  .action(async (filter, options) => {
985
1012
  checkConfigFile(true);
package/commands/bench.ts CHANGED
@@ -40,6 +40,8 @@ type BenchOptions = {
40
40
  compilerVersion: string;
41
41
  gc: "copying" | "compacting" | "generational" | "incremental";
42
42
  forceGc: boolean;
43
+ query: boolean;
44
+ legacyPersistence: boolean;
43
45
  save: boolean;
44
46
  compare: boolean;
45
47
  verbose: boolean;
@@ -61,6 +63,8 @@ export async function bench(
61
63
  compilerVersion: getMocVersion(true),
62
64
  gc: "copying",
63
65
  forceGc: true,
66
+ query: false,
67
+ legacyPersistence: false,
64
68
  save: false,
65
69
  compare: false,
66
70
  verbose: false,
@@ -120,6 +124,14 @@ export async function bench(
120
124
  ` gc: ${options.gc}${options.forceGc ? " (forced)" : ""}`,
121
125
  ),
122
126
  );
127
+ console.log(
128
+ chalk.gray(` context: ${options.query ? "query" : "update"}`),
129
+ );
130
+ console.log(
131
+ chalk.gray(
132
+ ` persistence: ${options.legacyPersistence ? "legacy" : "enhanced"}`,
133
+ ),
134
+ );
123
135
  console.log(chalk.gray(` profile: ${options.profile}`));
124
136
  console.log(chalk.gray(` optimize: ${optimize}`));
125
137
  }
@@ -270,7 +282,12 @@ function computeDiffAll(
270
282
  function getMocArgs(options: BenchOptions): string {
271
283
  let args = "";
272
284
 
285
+ // Benchmarks compile under enhanced orthogonal persistence (moc's default
286
+ // since 0.15) — the mode real canisters run. Pass `--legacy-persistence`
287
+ // only when the user opts in, and only where moc supports the flag (>= 0.15;
288
+ // legacy is already the default below it).
273
289
  if (
290
+ options.legacyPersistence &&
274
291
  options.compilerVersion &&
275
292
  new SemVer(options.compilerVersion).compare("0.15.0") >= 0
276
293
  ) {
@@ -533,13 +550,14 @@ async function runBenchFile(
533
550
  log();
534
551
  }
535
552
 
536
- // run all cells
553
+ // run all cells. `--query` measures in query context (how `query` methods
554
+ // actually run on the IC: no GC), at the cost of not supporting benches whose
555
+ // runner performs async calls. Otherwise use the update path.
537
556
  for (let [rowIndex, row] of schema.rows.entries()) {
538
557
  for (let [colIndex, col] of schema.cols.entries()) {
539
- let res = await actor.runCellUpdateAwait(
540
- BigInt(rowIndex),
541
- BigInt(colIndex),
542
- );
558
+ let res = options.query
559
+ ? await actor.runCellQuery(BigInt(rowIndex), BigInt(colIndex))
560
+ : await actor.runCellUpdateAwait(BigInt(rowIndex), BigInt(colIndex));
543
561
  results.set(`${row}:${col}`, res);
544
562
 
545
563
  // @ts-ignore
@@ -4,9 +4,13 @@ import { rm } from "node:fs/promises";
4
4
  import chalk from "chalk";
5
5
  import { execa } from "execa";
6
6
  import { cliError } from "../error.js";
7
- import { prepareMigrationArgs } from "../helpers/migrations.js";
7
+ import {
8
+ getCheckLimitPendingIssue,
9
+ prepareMigrationArgs,
10
+ reportCheckLimitPendingIssue,
11
+ } from "../helpers/migrations.js";
8
12
  import { getGlobalMocArgs, readConfig, resolveConfigPath } from "../mops.js";
9
- import { CanisterConfig } from "../types.js";
13
+ import { CanisterConfig, MigrationsConfig } from "../types.js";
10
14
  import {
11
15
  filterCanisters,
12
16
  looksLikeFile,
@@ -103,6 +107,7 @@ export async function checkStable(
103
107
  mocPath,
104
108
  globalMocArgs,
105
109
  canisterArgs: [...migration.migrationArgs, ...(canister.args ?? [])],
110
+ migrations: canister.migrations,
106
111
  options,
107
112
  });
108
113
  } finally {
@@ -146,6 +151,7 @@ export async function checkStable(
146
151
  globalMocArgs,
147
152
  canisterArgs: [...migration.migrationArgs, ...(canister.args ?? [])],
148
153
  sources,
154
+ migrations: canister.migrations,
149
155
  options,
150
156
  });
151
157
  } finally {
@@ -173,6 +179,7 @@ export interface RunStableCheckParams {
173
179
  globalMocArgs: string[];
174
180
  canisterArgs: string[];
175
181
  sources?: string[];
182
+ migrations?: MigrationsConfig;
176
183
  options?: Partial<CheckStableOptions>;
177
184
  }
178
185
 
@@ -240,7 +247,17 @@ export async function runStableCheck(
240
247
  reject: false,
241
248
  });
242
249
 
243
- if (result.exitCode !== 0) {
250
+ const issue = getCheckLimitPendingIssue(
251
+ params.migrations,
252
+ canisterName,
253
+ oldMostPath,
254
+ options.checkLimit === false,
255
+ isOldMostFile,
256
+ );
257
+
258
+ if (issue) {
259
+ reportCheckLimitPendingIssue(issue, result.exitCode !== 0);
260
+ } else if (result.exitCode !== 0) {
244
261
  if (result.stderr) {
245
262
  console.error(result.stderr);
246
263
  }
package/commands/check.ts CHANGED
@@ -230,7 +230,12 @@ async function checkCanisters(
230
230
  globalMocArgs,
231
231
  canisterArgs: [...migration.migrationArgs, ...(canister.args ?? [])],
232
232
  sources,
233
- options: { verbose: options.verbose, extraArgs: options.extraArgs },
233
+ migrations: canister.migrations,
234
+ options: {
235
+ verbose: options.verbose,
236
+ extraArgs: options.extraArgs,
237
+ checkLimit: options.checkLimit,
238
+ },
234
239
  });
235
240
  }
236
241
  } finally {
@@ -337,9 +337,7 @@ async function update(tool?: Tool) {
337
337
  config.toolchain[tool] = version;
338
338
  writeConfig(config);
339
339
 
340
- if (tool === "moc") {
341
- await checkRequirements();
342
- }
340
+ await checkRequirements();
343
341
 
344
342
  if (oldVersion === version) {
345
343
  console.log(`Latest ${tool} ${version} is already installed`);
@@ -4,38 +4,45 @@ import chalk from "chalk";
4
4
  import { getDependencyType, getRootDir, readConfig } from "./mops.js";
5
5
  import { resolvePackages } from "./resolve-packages.js";
6
6
  import { getMocSemVer } from "./helpers/get-moc-version.js";
7
+ import { getLintokoSemVer } from "./helpers/get-lintoko-version.js";
7
8
  import { getPackageId } from "./helpers/get-package-id.js";
9
+ const TOOL_REQUIREMENTS = [
10
+ { tool: "moc", getInstalled: getMocSemVer },
11
+ { tool: "lintoko", getInstalled: getLintokoSemVer },
12
+ ];
8
13
  export async function checkRequirements({ verbose = false } = {}) {
9
- let installedMoc = getMocSemVer();
10
- if (!installedMoc) {
11
- return;
12
- }
13
- let highestRequiredMoc = new SemVer("0.0.0");
14
- let highestRequiredMocPkgId = "";
15
14
  let rootDir = getRootDir();
16
15
  let resolvedPackages = await resolvePackages();
17
- for (let [name, version] of Object.entries(resolvedPackages)) {
18
- if (getDependencyType(version) === "mops") {
19
- let pkgId = getPackageId(name, version);
20
- let depConfig = readConfig(path.join(rootDir, ".mops", pkgId, "mops.toml"));
21
- let moc = depConfig.requirements?.moc;
22
- if (moc) {
23
- let requiredMoc = new SemVer(moc);
24
- if (highestRequiredMoc.compare(requiredMoc) < 0) {
25
- highestRequiredMoc = requiredMoc;
26
- highestRequiredMocPkgId = pkgId;
16
+ for (let { tool, getInstalled } of TOOL_REQUIREMENTS) {
17
+ let installed = getInstalled();
18
+ if (!installed) {
19
+ continue;
20
+ }
21
+ let highestRequired = new SemVer("0.0.0");
22
+ let highestRequiredPkgId = "";
23
+ for (let [name, version] of Object.entries(resolvedPackages)) {
24
+ if (getDependencyType(version) === "mops") {
25
+ let pkgId = getPackageId(name, version);
26
+ let depConfig = readConfig(path.join(rootDir, ".mops", pkgId, "mops.toml"));
27
+ let required = depConfig.requirements?.[tool];
28
+ if (required) {
29
+ let requiredVersion = new SemVer(required);
30
+ if (highestRequired.compare(requiredVersion) < 0) {
31
+ highestRequired = requiredVersion;
32
+ highestRequiredPkgId = pkgId;
33
+ }
34
+ verbose && _check(tool, pkgId, installed, requiredVersion);
27
35
  }
28
- verbose && _check(pkgId, installedMoc, requiredMoc);
29
36
  }
30
37
  }
38
+ verbose || _check(tool, highestRequiredPkgId, installed, highestRequired);
31
39
  }
32
- verbose || _check(highestRequiredMocPkgId, installedMoc, highestRequiredMoc);
33
40
  }
34
- function _check(pkgId, installedMoc, requiredMoc) {
35
- let comp = installedMoc.compare(requiredMoc);
36
- if (comp < 0) {
37
- console.log(chalk.yellow(`moc version does not meet the requirements of ${pkgId}`));
38
- console.log(chalk.yellow(` Required: >= ${requiredMoc.format()}`));
39
- console.log(chalk.yellow(` Installed: ${installedMoc.format()}`));
41
+ function _check(tool, pkgId, installed, required) {
42
+ if (!pkgId || installed.compare(required) >= 0) {
43
+ return;
40
44
  }
45
+ console.log(chalk.yellow(`${tool} version does not meet the requirements of ${pkgId}`));
46
+ console.log(chalk.yellow(` Required: >= ${required.format()}`));
47
+ console.log(chalk.yellow(` Installed: ${installed.format()}`));
41
48
  }
package/dist/cli.js CHANGED
@@ -65,12 +65,20 @@ function parseExtraArgs(variadicArgs) {
65
65
  // Shared `--help` section describing the enhanced migration `check-limit`
66
66
  // trimming and its override flag, so the limit behaviour is discoverable
67
67
  // from `--help`. `withFix` appends the `--fix` hint for commands that support it.
68
- function enhancedMigrationHelp(withFix) {
69
- const example = withFix ? " (e.g. to --fix older migrations)" : "";
70
- return ("\nEnhanced migration ([canisters.<name>.migrations]):\n" +
68
+ // `withPendingWarning` adds the stable-check pending-migration warning (check / check-stable only).
69
+ function enhancedMigrationHelp(options = {}) {
70
+ const example = options.withFix ? " (e.g. to --fix older migrations)" : "";
71
+ let text = "\nEnhanced migration ([canisters.<name>.migrations]):\n" +
71
72
  " The canister is checked against its migration chain. [migrations].check-limit\n" +
72
73
  " trims it to the last N migrations (older ones are skipped). Pass --no-check-limit\n" +
73
- ` to use the full chain${example}.`);
74
+ ` to use the full chain${example}.`;
75
+ if (options.withPendingWarning) {
76
+ text +=
77
+ "\n When check-limit is set, the stable check reports if more migrations are pending\n" +
78
+ " (relative to the deployed .most baseline) than the limit allows — as an error if\n" +
79
+ " compat failed, otherwise a warning.";
80
+ }
81
+ return text;
74
82
  }
75
83
  program.name("mops");
76
84
  // --version
@@ -286,9 +294,9 @@ program
286
294
  .description("Check Motoko canisters or files for syntax errors and type issues. Arguments can be canister names or file paths. If no arguments are given, checks all canisters from mops.toml. Also runs stable compatibility checks for canisters with [check-stable] configured, and runs linting if lintoko is configured in [toolchain]")
287
295
  .option("--verbose", "Verbose console output")
288
296
  .addOption(new Option("--fix", "Apply autofixes to all files, including transitively imported ones"))
289
- .addOption(new Option("--no-check-limit", "Check the full migration chain, ignoring [migrations].check-limit"))
297
+ .addOption(new Option("--no-check-limit", "Use the full migration chain, ignoring [migrations].check-limit; also suppresses the pending-migration warning"))
290
298
  .addHelpText("after", "\nArguments after -- are forwarded directly to moc, e.g.:\n $ mops check -- -Werror")
291
- .addHelpText("after", enhancedMigrationHelp(true))
299
+ .addHelpText("after", enhancedMigrationHelp({ withFix: true, withPendingWarning: true }))
292
300
  .allowUnknownOption(true)
293
301
  .action(async (args, options) => {
294
302
  checkConfigFile(true);
@@ -321,9 +329,9 @@ program
321
329
  .command("check-stable [args...]")
322
330
  .description("Check stable variable compatibility. With no arguments, checks all canisters with [check-stable] configured. Arguments can be canister names or an old file path followed by an optional canister name")
323
331
  .option("--verbose", "Verbose console output")
324
- .addOption(new Option("--no-check-limit", "Check the full migration chain, ignoring [migrations].check-limit"))
332
+ .addOption(new Option("--no-check-limit", "Use the full migration chain, ignoring [migrations].check-limit; also suppresses the pending-migration warning"))
325
333
  .addHelpText("after", "\nArguments after -- are forwarded directly to moc, e.g.:\n $ mops check-stable -- -Werror")
326
- .addHelpText("after", enhancedMigrationHelp(false))
334
+ .addHelpText("after", enhancedMigrationHelp({ withPendingWarning: true }))
327
335
  .allowUnknownOption(true)
328
336
  .action(async (args, options) => {
329
337
  checkConfigFile(true);
@@ -393,6 +401,8 @@ program
393
401
  .addOption(new Option("--save", "Save benchmark results to .bench/<filename>.json"))
394
402
  .addOption(new Option("--compare", "Run benchmark and compare results with .bench/<filename>.json"))
395
403
  // .addOption(new Option('--force-gc', 'Force GC'))
404
+ .addOption(new Option("--query", "Measure each cell in a query call (how `query` methods run on the IC: no GC). Only for benchmarks whose runner is synchronous (no inter-canister calls)"))
405
+ .addOption(new Option("--legacy-persistence", "Compile benchmark canisters under legacy persistence instead of enhanced orthogonal persistence (the default)"))
396
406
  .addOption(new Option("--verbose", "Print the benchmark pipeline (compiler, replica, GC, optimization) and stream compiler/replica output, including dfx optimization warnings"))
397
407
  .action(async (filter, options) => {
398
408
  checkConfigFile(true);
@@ -693,7 +703,7 @@ program
693
703
  .addOption(new Option("-r, --rules <directory...>", "Directories containing rules (can be used multiple times)"))
694
704
  .addOption(new Option("--no-check-limit", "Lint the full migration chain, ignoring [migrations].check-limit"))
695
705
  .addHelpText("after", "\nArguments after -- are forwarded directly to lintoko, e.g.:\n $ mops lint -- --severity warning")
696
- .addHelpText("after", enhancedMigrationHelp(true))
706
+ .addHelpText("after", enhancedMigrationHelp({ withFix: true }))
697
707
  .allowUnknownOption(true)
698
708
  .action(async (filter, options) => {
699
709
  checkConfigFile(true);
@@ -7,6 +7,8 @@ type BenchOptions = {
7
7
  compilerVersion: string;
8
8
  gc: "copying" | "compacting" | "generational" | "incremental";
9
9
  forceGc: boolean;
10
+ query: boolean;
11
+ legacyPersistence: boolean;
10
12
  save: boolean;
11
13
  compare: boolean;
12
14
  verbose: boolean;
@@ -31,6 +31,8 @@ export async function bench(filter = "", optionsArg = {}) {
31
31
  compilerVersion: getMocVersion(true),
32
32
  gc: "copying",
33
33
  forceGc: true,
34
+ query: false,
35
+ legacyPersistence: false,
34
36
  save: false,
35
37
  compare: false,
36
38
  verbose: false,
@@ -71,6 +73,8 @@ export async function bench(filter = "", optionsArg = {}) {
71
73
  console.log(chalk.gray(` compiler: moc ${options.compilerVersion}`));
72
74
  console.log(chalk.gray(` replica: ${replicaType}${options.replicaVersion ? ` ${options.replicaVersion}` : ""}`));
73
75
  console.log(chalk.gray(` gc: ${options.gc}${options.forceGc ? " (forced)" : ""}`));
76
+ console.log(chalk.gray(` context: ${options.query ? "query" : "update"}`));
77
+ console.log(chalk.gray(` persistence: ${options.legacyPersistence ? "legacy" : "enhanced"}`));
74
78
  console.log(chalk.gray(` profile: ${options.profile}`));
75
79
  console.log(chalk.gray(` optimize: ${optimize}`));
76
80
  }
@@ -188,7 +192,12 @@ function computeDiffAll(currentResults, prevResults, rows, cols) {
188
192
  }
189
193
  function getMocArgs(options) {
190
194
  let args = "";
191
- if (options.compilerVersion &&
195
+ // Benchmarks compile under enhanced orthogonal persistence (moc's default
196
+ // since 0.15) — the mode real canisters run. Pass `--legacy-persistence`
197
+ // only when the user opts in, and only where moc supports the flag (>= 0.15;
198
+ // legacy is already the default below it).
199
+ if (options.legacyPersistence &&
200
+ options.compilerVersion &&
192
201
  new SemVer(options.compilerVersion).compare("0.15.0") >= 0) {
193
202
  args += " --legacy-persistence";
194
203
  }
@@ -386,10 +395,14 @@ async function runBenchFile(file, options, replica) {
386
395
  if (canUpdateLog) {
387
396
  log();
388
397
  }
389
- // run all cells
398
+ // run all cells. `--query` measures in query context (how `query` methods
399
+ // actually run on the IC: no GC), at the cost of not supporting benches whose
400
+ // runner performs async calls. Otherwise use the update path.
390
401
  for (let [rowIndex, row] of schema.rows.entries()) {
391
402
  for (let [colIndex, col] of schema.cols.entries()) {
392
- let res = await actor.runCellUpdateAwait(BigInt(rowIndex), BigInt(colIndex));
403
+ let res = options.query
404
+ ? await actor.runCellQuery(BigInt(rowIndex), BigInt(colIndex))
405
+ : await actor.runCellUpdateAwait(BigInt(rowIndex), BigInt(colIndex));
393
406
  results.set(`${row}:${col}`, res);
394
407
  // @ts-ignore
395
408
  instructionsCells[rowIndex][colIndex] = res.instructions;
@@ -1,4 +1,4 @@
1
- import { CanisterConfig } from "../types.js";
1
+ import { CanisterConfig, MigrationsConfig } from "../types.js";
2
2
  export interface CheckStableOptions {
3
3
  verbose: boolean;
4
4
  extraArgs: string[];
@@ -17,6 +17,7 @@ export interface RunStableCheckParams {
17
17
  globalMocArgs: string[];
18
18
  canisterArgs: string[];
19
19
  sources?: string[];
20
+ migrations?: MigrationsConfig;
20
21
  options?: Partial<CheckStableOptions>;
21
22
  }
22
23
  export declare function runStableCheck(params: RunStableCheckParams): Promise<void>;
@@ -4,7 +4,7 @@ import { rm } from "node:fs/promises";
4
4
  import chalk from "chalk";
5
5
  import { execa } from "execa";
6
6
  import { cliError } from "../error.js";
7
- import { prepareMigrationArgs } from "../helpers/migrations.js";
7
+ import { getCheckLimitPendingIssue, prepareMigrationArgs, reportCheckLimitPendingIssue, } from "../helpers/migrations.js";
8
8
  import { getGlobalMocArgs, readConfig, resolveConfigPath } from "../mops.js";
9
9
  import { filterCanisters, looksLikeFile, resolveCanisterConfigs, resolveSingleCanister, validateCanisterArgs, } from "../helpers/resolve-canisters.js";
10
10
  import { sourcesArgs } from "./sources.js";
@@ -61,6 +61,7 @@ export async function checkStable(args, options = {}) {
61
61
  mocPath,
62
62
  globalMocArgs,
63
63
  canisterArgs: [...migration.migrationArgs, ...(canister.args ?? [])],
64
+ migrations: canister.migrations,
64
65
  options,
65
66
  });
66
67
  }
@@ -95,6 +96,7 @@ export async function checkStable(args, options = {}) {
95
96
  globalMocArgs,
96
97
  canisterArgs: [...migration.migrationArgs, ...(canister.args ?? [])],
97
98
  sources,
99
+ migrations: canister.migrations,
98
100
  options,
99
101
  });
100
102
  }
@@ -136,7 +138,11 @@ export async function runStableCheck(params) {
136
138
  stdio: "pipe",
137
139
  reject: false,
138
140
  });
139
- if (result.exitCode !== 0) {
141
+ const issue = getCheckLimitPendingIssue(params.migrations, canisterName, oldMostPath, options.checkLimit === false, isOldMostFile);
142
+ if (issue) {
143
+ reportCheckLimitPendingIssue(issue, result.exitCode !== 0);
144
+ }
145
+ else if (result.exitCode !== 0) {
140
146
  if (result.stderr) {
141
147
  console.error(result.stderr);
142
148
  }
@@ -144,7 +144,12 @@ async function checkCanisters(config, canisters, options) {
144
144
  globalMocArgs,
145
145
  canisterArgs: [...migration.migrationArgs, ...(canister.args ?? [])],
146
146
  sources,
147
- options: { verbose: options.verbose, extraArgs: options.extraArgs },
147
+ migrations: canister.migrations,
148
+ options: {
149
+ verbose: options.verbose,
150
+ extraArgs: options.extraArgs,
151
+ checkLimit: options.checkLimit,
152
+ },
148
153
  });
149
154
  }
150
155
  }
@@ -259,9 +259,7 @@ async function update(tool) {
259
259
  let oldVersion = config.toolchain[tool];
260
260
  config.toolchain[tool] = version;
261
261
  writeConfig(config);
262
- if (tool === "moc") {
263
- await checkRequirements();
264
- }
262
+ await checkRequirements();
265
263
  if (oldVersion === version) {
266
264
  console.log(`Latest ${tool} ${version} is already installed`);
267
265
  }
@@ -0,0 +1,3 @@
1
+ import { type SemVer } from "semver";
2
+ export declare function getLintokoSemVer(): SemVer | null;
3
+ export declare function getLintokoVersion(throwOnError?: boolean): string;