From e6470796c6dc23593137021de352d7d58740d3f2 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Tue, 5 May 2026 00:41:31 +0200 Subject: [PATCH 01/11] feat: add motoko, mops-cli, and migration skills with upstream sync strategy - Rewrites motoko skill to align with caffeinelabs/motoko 1.7.0: plain actor {} with --default-persistent-actors flag as the primary style, transient var for reset-on-upgrade semantics, dot notation for collection operations (mo:core), and moc >= 1.2.0 compatibility - Adds mops-cli skill synced from caffeinelabs/mops cli-v2.13.1: covers mops.toml configuration, mops check, mops toolchain, mops migrate, mops test, and lintoko integration - Adds migrating-motoko skill: inline (with migration = ...) syntax for single-step actor state transformations without --enhanced-migration - Adds migrating-motoko-enhanced skill: migrations/ directory approach with --enhanced-migration for multi-step verifiable migration chains - Creates Motoko category for the three language skills; mops-cli stays in Infrastructure - Adds upstream sync strategy to CLAUDE.md: commit-hash pinning, two-step annotated tag dereference, curl+diff workflow - Adds evaluation files for all four skills with adversarial cases - Adds motoko/references/ directory with api-reference, control-flow, examples, and type-conversions reference files --- .claude/CLAUDE.md | 54 +- evaluations/migrating-motoko-enhanced.json | 109 ++++ evaluations/migrating-motoko.json | 99 ++++ evaluations/mops-cli.json | 127 +++++ evaluations/motoko.json | 96 +++- skills/migrating-motoko-enhanced/SKILL.md | 292 ++++++++++ skills/migrating-motoko/SKILL.md | 197 +++++++ skills/mops-cli/SKILL.md | 242 ++++++++ skills/motoko/SKILL.md | 555 +++++++++---------- skills/motoko/references/api-reference.md | 511 +++++++++++++++++ skills/motoko/references/control-flow.md | 84 +++ skills/motoko/references/examples.md | 427 ++++++++++++++ skills/motoko/references/type-conversions.md | 59 ++ 13 files changed, 2544 insertions(+), 308 deletions(-) create mode 100644 evaluations/migrating-motoko-enhanced.json create mode 100644 evaluations/migrating-motoko.json create mode 100644 evaluations/mops-cli.json create mode 100644 skills/migrating-motoko-enhanced/SKILL.md create mode 100644 skills/migrating-motoko/SKILL.md create mode 100644 skills/mops-cli/SKILL.md create mode 100644 skills/motoko/references/api-reference.md create mode 100644 skills/motoko/references/control-flow.md create mode 100644 skills/motoko/references/examples.md create mode 100644 skills/motoko/references/type-conversions.md diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index c5f195a..902e44e 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -69,6 +69,58 @@ Results are saved to `evaluations/results/` (gitignored). See `evaluations/icp-c **Eval prompt guidelines:** Keep prompts focused to avoid the 120s timeout. Scope the response ("just the function, no deploy steps"), ask for one thing, and match expected behaviors to what the prompt actually asks. See CONTRIBUTING.md for details and examples. +## Upstream Sync Strategy + +Several skills track content from external upstream repositories. **Do not use git submodules** — we only need a small number of files per repo and submodules complicate every clone. + +### Upstream comment format + +Every skill that tracks upstream content must have this comment block at the top of the SKILL.md body (after frontmatter): + +```html + +``` + +Always use the **full commit SHA** of the tag, not just the tag name. Annotated tags require a two-step dereference (tag object → commit SHA). + +### Getting a commit SHA for a tag + +```bash +# Step 1: get tag object SHA and type +TAG_SHA=$(curl -s "https://api.github.com/repos///git/ref/tags/" | \ + python3 -c "import sys,json; d=json.load(sys.stdin); print(d['object']['sha'], d['object']['type'])") + +# Step 2: if type is "tag" (annotated), dereference to the commit +curl -s "https://api.github.com/repos///git/tags/" | \ + python3 -c "import sys,json; print(json.load(sys.stdin)['object']['sha'])" +``` + +### Checking for upstream changes + +```bash +# Fetch upstream skill file at a new tag/commit +curl -s "https://raw.githubusercontent.com////" > /tmp/upstream.md + +# Compare to find what changed +diff /tmp/upstream.md skills//SKILL.md +``` + +### Current upstream sources + +| Skill | Upstream repo | Tag | Commit | +|-------|--------------|-----|--------| +| `motoko` | [caffeinelabs/motoko](https://github.com/caffeinelabs/motoko) | 1.7.0 | `1e65e26346b35927869dda044bb76763627c2c57` | +| `migrating-motoko` | [caffeinelabs/motoko](https://github.com/caffeinelabs/motoko) | 1.7.0 | `1e65e26346b35927869dda044bb76763627c2c57` | +| `migrating-motoko-enhanced` | [caffeinelabs/motoko](https://github.com/caffeinelabs/motoko) | 1.7.0 | `1e65e26346b35927869dda044bb76763627c2c57` | +| `mops-cli` | [caffeinelabs/mops](https://github.com/caffeinelabs/mops) | cli-v2.13.1 | `c947a79fc68d2d4d5b0d3bad10e23370b8134364` | + +When a new version of an upstream repo is released: (1) get the new commit SHA, (2) diff the upstream skill file against what we have, (3) apply non-conflicting improvements, (4) update the upstream comment with the new tag and SHA, (5) update the table above. + ## Writing Guidelines - **Write for agents, not humans.** Be explicit with canister IDs, function signatures, and error messages. @@ -78,7 +130,7 @@ Results are saved to `evaluations/results/` (gitignored). See `evaluations/icp-c ## Categories -Known categories: Auth, Core, DeFi, Frontend, Governance, Infrastructure, Integration, Security. New categories are allowed — the validator warns but does not block. +Known categories: Auth, Core, DeFi, Frontend, Governance, Infrastructure, Integration, Motoko, Security. New categories are allowed — the validator warns but does not block. ## Tech Stack diff --git a/evaluations/migrating-motoko-enhanced.json b/evaluations/migrating-motoko-enhanced.json new file mode 100644 index 0000000..4c37da4 --- /dev/null +++ b/evaluations/migrating-motoko-enhanced.json @@ -0,0 +1,109 @@ +{ + "skill": "migrating-motoko-enhanced", + "description": "Evaluation cases for the migrating-motoko-enhanced skill. Tests whether agents generate correct migration chain files, configure mops.toml correctly, write actor bodies without initializers, handle field semantics, compose migrations correctly, and avoid common pitfalls.", + + "output_evals": [ + { + "name": "First migration (Init)", + "prompt": "I'm starting a new Motoko canister with enhanced migration. My actor has `var count : Nat` and `var name : Text`. Write the initial migration file.", + "expected_behaviors": [ + "Creates a migration file with a timestamp prefix (e.g. `20250101_000000_Init.mo`)", + "Migration function takes empty input `(_ : {})`", + "Migration function returns `{ count : Nat; name : Text }` with default values", + "Explains that the actor body must declare these variables WITHOUT initializers" + ] + }, + { + "name": "Add a field", + "prompt": "Write a migration file that adds an `email : Text` field (default empty string) to a Motoko canister that uses enhanced migration. The canister already has `name : Text` and `count : Nat` from a previous Init migration.", + "expected_behaviors": [ + "Creates a new migration file with a timestamp prefix", + "Migration function takes empty input `(_ : {})`", + "Migration function returns `{ email : Text }` with `{ email = \"\" }`", + "Notes that only the NEW field needs to appear in this migration (existing fields carry through)" + ] + }, + { + "name": "Actor body without initializers", + "prompt": "Show me what a Motoko actor looks like when using enhanced migration. It should have `var count : Nat`, `var name : Text`, and `var email : Text`.", + "expected_behaviors": [ + "Actor variables are declared WITHOUT initializers: `var count : Nat;` not `var count : Nat = 0;`", + "Does NOT use `stable` keyword", + "Does NOT use `(with migration = ...)` inline syntax", + "Explains that values come from the migration chain" + ] + }, + { + "name": "mops.toml configuration", + "prompt": "How do I configure mops.toml for enhanced migration on my `backend` canister with migrations in `src/backend/migrations`?", + "expected_behaviors": [ + "Shows `[canisters.backend.migrations]` section with `chain` path", + "Does NOT put `--enhanced-migration` in `[moc].args` — it must be per-canister", + "Notes that mops injects `--enhanced-migration` automatically when `[canisters..migrations]` is configured", + "Warns not to duplicate `--enhanced-migration` in `[canisters.backend].args`" + ] + }, + { + "name": "Migration chain composition", + "prompt": "Explain how migrations compose in the enhanced migration system. I have Init (adds `name : Text`), then AddEmail (adds `email : Text`), then RenameField (renames `name` to `displayName`). What is the final actor state?", + "expected_behaviors": [ + "Explains the carry-through rule: fields not mentioned in a migration pass through unchanged", + "Final state is `{ displayName : Text; email : Text }`", + "Explains that compiler verifies each migration's input is compatible with previous output", + "Notes that `name` is consumed by RenameField and `displayName` is produced" + ] + }, + { + "name": "Workflow with mops", + "prompt": "Walk me through the full workflow for adding a new field to my Motoko canister using enhanced migration.", + "expected_behaviors": [ + "Mentions `mops migrate new ` to create the migration file", + "Mentions editing the migration file", + "Mentions `mops check --fix` to verify chain consistency", + "Mentions `mops build` to compile", + "Mentions `mops migrate freeze` after successful deployment" + ] + }, + { + "name": "Adversarial: actor with initializers", + "prompt": "Write a Motoko actor with enhanced migration that has `var count : Nat = 0` and `var name : Text = \"\"`.", + "expected_behaviors": [ + "Removes the initializers — declares `var count : Nat;` and `var name : Text;` without values", + "Explains that initializers are not allowed with enhanced migration — values come from the chain", + "Points to the Init migration file for the initial values" + ] + }, + { + "name": "Adversarial: mixing inline and enhanced migration", + "prompt": "Can I use `(with migration = ...)` inline syntax together with `--enhanced-migration` in the same canister?", + "expected_behaviors": [ + "Explains these two approaches cannot be combined", + "States the restriction clearly: `--enhanced-migration` and inline `(with migration = ...)` are mutually exclusive", + "Recommends choosing one: inline for simple one-shot migrations, enhanced for multi-step chains" + ] + } + ], + + "trigger_evals": { + "description": "Queries to test whether the skill activates correctly.", + "should_trigger": [ + "How do I set up enhanced migration for my Motoko canister?", + "Write a migration file for my migrations/ directory", + "I'm getting Compatibility error M0170 with --enhanced-migration", + "How does the migration chain work in Motoko?", + "Configure mops.toml for enhanced migration", + "How do I add a field to my canister using the migrations directory?", + "Explain mops migrate new and mops migrate freeze", + "My actor has enhanced migration but I need to rename a field" + ], + "should_not_trigger": [ + "Write a Motoko canister with a counter", + "How do I use Map in Motoko?", + "I need a one-shot migration for a single field rename", + "How do I write (with migration = ...) for my actor?", + "Deploy my canister to mainnet", + "How do I pin the moc version in mops.toml?", + "Set up Internet Identity in my app" + ] + } +} diff --git a/evaluations/migrating-motoko.json b/evaluations/migrating-motoko.json new file mode 100644 index 0000000..b4efc17 --- /dev/null +++ b/evaluations/migrating-motoko.json @@ -0,0 +1,99 @@ +{ + "skill": "migrating-motoko", + "description": "Evaluation cases for the migrating-motoko skill. Tests whether agents generate correct inline migration syntax, use (with migration = ...) correctly, define old types inline, handle field semantics, and avoid deprecated patterns like preupgrade/postupgrade.", + + "output_evals": [ + { + "name": "Rename a field", + "prompt": "My Motoko canister has `var count : Nat` and I want to rename it to `var total : Nat` in the next upgrade. Write the migration.", + "expected_behaviors": [ + "Uses `(with migration = ...)` syntax before the actor", + "Migration function takes `{ var count : Nat }` as input and returns `{ var total : Nat }`", + "Migration function body maps `count` to `total`", + "Actor body declares `var total : Nat` with an initializer", + "Does NOT use `preupgrade`/`postupgrade`", + "Does NOT use `stable` keyword" + ] + }, + { + "name": "Bool to variant", + "prompt": "I have a Motoko canister with `var completed : Bool` and I want to change it to a richer status variant `{ #pending; #inProgress; #completed }` in my next upgrade.", + "expected_behaviors": [ + "Uses `(with migration = ...)` syntax", + "Maps `completed = true` to `#completed` and `completed = false` to `#pending`", + "New actor declares `var status : { #pending; #inProgress; #completed }`", + "Does NOT use `preupgrade`/`postupgrade`" + ] + }, + { + "name": "Add field with default", + "prompt": "Write a Motoko migration that adds a `zipCode : Text` field (defaulting to empty string) to each record in a Map of users stored in my canister.", + "expected_behaviors": [ + "Uses `(with migration = ...)` on the actor", + "Defines `OldUser` and `NewUser` types inline in the migration module", + "Uses dot notation on the Map: `old.users.map<...>(func(_, u) { { u with zipCode = \"\" } })`", + "Does NOT import old types from the existing codebase" + ] + }, + { + "name": "Implicit migration — no code needed", + "prompt": "I want to add a new optional field `?Text` to my Motoko actor in the next upgrade. Do I need to write a migration function?", + "expected_behaviors": [ + "Explains that adding a field is an implicit (compatible) change — no migration function needed", + "Mentions that removing fields, changing mutability, and widening types are also implicit", + "Notes that renaming fields or changing types DO require an explicit migration" + ] + }, + { + "name": "Migration module pattern", + "prompt": "Write a Motoko migration that transforms task state: rename field `title` to `name` and add a `priority : Nat` field defaulting to 0. Show the migration.mo and main.mo.", + "expected_behaviors": [ + "Creates a separate `migration.mo` module", + "Defines `OldActor` and `NewActor` record types inline in the migration module", + "Does NOT import old types from `types.mo` or other files", + "`main.mo` uses `(with migration = Migration.run)` before `actor`", + "Actor fields have initializers (used on fresh install)" + ] + }, + { + "name": "Adversarial: preupgrade/postupgrade", + "prompt": "I need to migrate my Motoko canister's state on upgrade. Can I use `system func preupgrade()` to save state to a stable variable and restore it in `postupgrade()`?", + "expected_behaviors": [ + "Redirects to `(with migration = ...)` syntax instead", + "Explains that `preupgrade`/`postupgrade` are not needed for data migration in modern Motoko (persistent actors auto-persist state)", + "Does NOT provide a `preupgrade`/`postupgrade` implementation" + ] + }, + { + "name": "Adversarial: importing old types", + "prompt": "I'm writing a migration.mo file. Can I import my old `Types` module to get the old type definitions I need in the migration function?", + "expected_behaviors": [ + "Explains that old types must be defined inline in the migration module", + "Warns that importing from existing code paths creates coupling to the current state of those files, not the old state", + "Shows example of defining OldActor types inline" + ] + } + ], + + "trigger_evals": { + "description": "Queries to test whether the skill activates correctly.", + "should_trigger": [ + "How do I rename a field in my Motoko canister upgrade?", + "Write a Motoko migration to change a Bool field to a variant", + "My Motoko canister upgrade fails with Compatibility error M0170", + "How do I migrate Motoko actor state?", + "What is (with migration = ...) in Motoko?", + "I need to restructure my Motoko canister state on upgrade", + "How do I add a field to my Motoko canister without losing existing data?" + ], + "should_not_trigger": [ + "Write a Motoko canister with a counter", + "How do I use Map in Motoko?", + "Set up a migrations directory with --enhanced-migration", + "How do I run mops migrate new?", + "Deploy my canister to mainnet", + "What is mo:core?", + "How do I pin the moc version?" + ] + } +} diff --git a/evaluations/mops-cli.json b/evaluations/mops-cli.json new file mode 100644 index 0000000..fd5c2a1 --- /dev/null +++ b/evaluations/mops-cli.json @@ -0,0 +1,127 @@ +{ + "skill": "mops-cli", + "description": "Evaluation cases for the mops-cli skill. Tests whether agents produce correct mops.toml configuration, use canister names (not file paths) with mops check, pin toolchain versions, use mo:core (not mo:base), handle migration workflows correctly, and avoid documented pitfalls.", + "output_evals": [ + { + "name": "New project setup", + "prompt": "I want to start a new Motoko project. Walk me through setting up mops from scratch, including the mops.toml and the initial commands. Just the setup, no deployment steps.", + "expected_behaviors": [ + "Runs `mops init -y` to initialize the project", + "Uses `mops toolchain use moc latest` (or a specific version) \u2014 NOT the bare `mops toolchain use moc` which opens an interactive picker", + "Includes `[toolchain]` section in mops.toml with a concrete `moc` version", + "Includes `[dependencies]` with `core` (not `base`)", + "Includes `[canisters.]` with a `main` field pointing to the entry file", + "Runs `mops add core` to install the core library", + "Does NOT suggest installing mo:base" + ] + }, + { + "name": "Adding a dependency", + "prompt": "How do I add the `serde` package at version 1.3.0 as a regular dependency to my mops project?", + "expected_behaviors": [ + "Uses `mops add serde@1.3.0`", + "Explains that this updates both mops.toml and mops.lock", + "Does NOT suggest manually editing mops.toml to add the dependency" + ] + }, + { + "name": "Running mops check", + "prompt": "I have a canister called `backend` defined in mops.toml. How do I type-check it and auto-fix any lint issues?", + "expected_behaviors": [ + "Uses `mops check backend` (canister name, NOT a file path like src/backend/main.mo)", + "Mentions `mops check --fix` for auto-fixing", + "Does NOT pass a file path like `mops check src/backend/main.mo`", + "Explains that per-canister args from mops.toml are applied automatically when using the canister name" + ] + }, + { + "name": "Pinning moc version", + "prompt": "How do I pin the moc compiler to version 1.5.1 in my mops project?", + "expected_behaviors": [ + "Uses `mops toolchain use moc 1.5.1`", + "Shows the resulting `[toolchain]` section in mops.toml with `moc = \"1.5.1\"`", + "Does NOT suggest editing mops.toml manually without running the command", + "Does NOT use `mops toolchain use moc` without a version (that opens an interactive picker)" + ] + }, + { + "name": "Updating moc to latest", + "prompt": "I want to update moc to the latest version. What's the correct command?", + "expected_behaviors": [ + "If moc is already in [toolchain]: uses `mops toolchain update moc`", + "Alternatively mentions `mops toolchain use moc latest` which works both for first-time pin and updates", + "Does NOT use `mops toolchain use moc` without a version", + "Clarifies that `toolchain update` only works when moc already has a [toolchain] entry" + ] + }, + { + "name": "Migration workflow", + "prompt": "I changed my Motoko actor's stable state and mops check is now failing with a migration hint. How do I create a new migration called `AddEmailField` for the `backend` canister, and what do I do after deploying?", + "expected_behaviors": [ + "Runs `mops migrate new AddEmailField backend` to create the migration", + "Mentions editing the generated migration file", + "Runs `mops check` to verify the migration resolves the error", + "Runs `mops build` after check passes", + "After deploying, runs `mops migrate freeze backend` to move next-migration to the permanent chain", + "Does NOT suggest adding `--enhanced-migration` to mops.toml manually (mops auto-injects it)" + ] + }, + { + "name": "Warning flag configuration", + "prompt": "How do I enable M0236 (dot notation), M0237 (redundant explicit arguments), and M0223 (redundant type instantiation) as warnings for all canisters in my project?", + "expected_behaviors": [ + "Adds `-W=M0223,M0236,M0237` to the `args` list under `[moc]` in mops.toml", + "Places this in `[moc]` (global), not in `[canisters.]`", + "Explains that -W= enables these optional checks as warnings", + "Does NOT place warning flags in [build].args (those are build-only)" + ] + }, + { + "name": "Adversarial: file path to mops check", + "prompt": "I want to type-check my canister. My main file is at src/backend/main.mo. I'll run: mops check src/backend/main.mo. Is that right?", + "expected_behaviors": [ + "Corrects the approach \u2014 for canister projects, always use the canister name, not a file path", + "Provides the correct command: `mops check backend` (using the canister name from mops.toml)", + "Explains that using a file path bypasses per-canister args in mops.toml", + "Does NOT endorse `mops check src/backend/main.mo` as correct for canister projects" + ] + }, + { + "name": "Adversarial: mo:base import", + "prompt": "I need a HashMap in my Motoko canister. Can I use `import HashMap \"mo:base/HashMap\"`?", + "expected_behaviors": [ + "Explains that mo:base is deprecated and should not be used", + "Recommends importing Map from mo:core: `import Map \"mo:core/Map\"`", + "Notes that HashMap from mo:base is not stable and cannot be used in persistent actors", + "Does NOT confirm that mo:base/HashMap is a valid choice" + ] + } + ], + "trigger_evals": { + "description": "Queries to test whether the skill activates correctly. 'should_trigger' queries should cause the skill to load; 'should_not_trigger' queries should NOT activate this skill.", + "should_trigger": [ + "How do I set up a mops.toml for my Motoko project?", + "I need to add a package dependency with mops", + "How do I pin the moc compiler version?", + "My mops check is failing, how do I fix it?", + "What's the mops command to update all dependencies?", + "How do I create a new migration with mops migrate?", + "Set up lintoko in my mops project", + "How do I run tests with mops?", + "Configure moc flags for my Motoko canister", + "My mops.lock file is stale, what do I do?", + "How do I add a dev dependency with mops?", + "How do I run tests in my Motoko project?" + ], + "should_not_trigger": [ + "Write a Motoko persistent actor with a counter", + "How do I deploy my canister to mainnet?", + "What is the Internet Computer consensus mechanism?", + "How do I set up HTTPS outcalls in my canister?", + "Add Internet Identity login to my frontend", + "How do stable variables work in Motoko?", + "What's the icp.yaml config for a Rust canister?", + "How do I implement ICRC-1 token transfer?" + ] + } +} diff --git a/evaluations/motoko.json b/evaluations/motoko.json index 33d5fca..065abc0 100644 --- a/evaluations/motoko.json +++ b/evaluations/motoko.json @@ -1,13 +1,12 @@ { "skill": "motoko", - "description": "Evaluation cases for the motoko skill. Tests whether agents generate correct modern Motoko syntax, use mo:core instead of mo:base, avoid non-stable types, and handle common language pitfalls.", - + "description": "Evaluation cases for the motoko skill. Tests whether agents generate correct modern Motoko syntax, use mo:core instead of mo:base, avoid non-stable types, use dot notation for collections, handle common language pitfalls, and use the --default-persistent-actors flag.", "output_evals": [ { - "name": "Simple persistent actor", + "name": "Simple actor with counter", "prompt": "Write a Motoko canister with a counter that can be incremented and queried.", "expected_behaviors": [ - "Uses `persistent actor`, NOT plain `actor`", + "Uses plain `actor`, NOT `persistent actor` (flag-based approach), OR uses `persistent actor` and notes the `--default-persistent-actors` flag", "Uses `var` for the counter, NOT `stable var`", "Increment function is `public func` returning `async`", "Query function uses `public query func`", @@ -18,10 +17,11 @@ "name": "Actor with Map collection", "prompt": "Write a Motoko canister that stores user profiles (name, email) by ID and supports add, get, and list operations.", "expected_behaviors": [ - "Uses `persistent actor`", + "Uses plain `actor` (with --default-persistent-actors assumed) OR `persistent actor`", "Imports Map from mo:core/Map, NOT HashMap from mo:base", "Creates map with Map.empty()", - "Passes an explicit compare function (e.g., Nat.compare) to Map.add and Map.get", + "Uses dot notation: `profiles.add(id, profile)`, `profiles.get(id)`, NOT `Map.add(profiles, Nat.compare, id, profile)`", + "Does NOT pass explicit compare function \u2014 comparator is inferred", "Type definitions are inside the actor body, not before it", "Does NOT use HashMap, TrieMap, or Buffer" ] @@ -30,17 +30,18 @@ "name": "Fix M0220 error", "prompt": "I'm getting this Motoko compiler error: `type error [M0220], this actor or actor class should be declared persistent`. Here's my code:\n\nactor {\n var count : Nat = 0;\n public func increment() : async Nat {\n count += 1;\n count\n };\n};", "expected_behaviors": [ - "Identifies that `persistent` keyword is missing", - "Shows the fix: change `actor` to `persistent actor`", + "Explains that M0220 occurs when neither `--default-persistent-actors` flag nor `persistent` keyword is present", + "Offers fix option 1: add `--default-persistent-actors` to `[moc].args` in mops.toml", + "Offers fix option 2: change `actor` to `persistent actor`", "Does NOT suggest adding `stable` keyword to variables" ] }, { "name": "Fix M0141 error", - "prompt": "My Motoko code won't compile:\n\nimport Nat \"mo:core/Nat\";\n\ntype UserId = Nat;\nlet MAX_USERS = 1000;\n\npersistent actor {\n var count : Nat = 0;\n};", + "prompt": "My Motoko code won't compile:\n\nimport Nat \"mo:core/Nat\";\n\ntype UserId = Nat;\nlet MAX_USERS = 1000;\n\nactor {\n var count : Nat = 0;\n};", "expected_behaviors": [ "Identifies error M0141: type and let declarations must be inside the actor body", - "Shows the fix: move `type UserId` and `let MAX_USERS` inside `persistent actor { }`", + "Shows the fix: move `type UserId` and `let MAX_USERS` inside `actor { }`", "Keeps the import statement before the actor (imports are allowed)" ] }, @@ -51,8 +52,7 @@ "Recommends Map from mo:core for the key-value mapping", "Recommends List from mo:core for the growable log", "Does NOT suggest HashMap, TrieMap, Buffer, or RBTree", - "Does NOT import from mo:base", - "Uses `persistent actor`" + "Does NOT import from mo:base" ] }, { @@ -60,7 +60,7 @@ "prompt": "Write a Motoko function that looks up a user by ID in a Map and returns a greeting message, or 'not found' if the user doesn't exist.", "expected_behaviors": [ "Uses switch/case with `case (?value)` and `case null` pattern", - "Passes compare function to Map.get", + "Uses dot notation for Map.get: `users.get(id)`", "Does NOT use unsafe unwrapping that could trap" ] }, @@ -68,7 +68,7 @@ "name": "Transient vs stable semantics", "prompt": "In my Motoko canister, I have a request counter that should reset to zero after every upgrade, and a user database that should persist. How do I set this up?", "expected_behaviors": [ - "Uses `persistent actor`", + "Shows mops.toml with `--default-persistent-actors` in moc args, OR uses `persistent actor`", "Uses plain `var` or `let` for the user database (auto-stable)", "Uses `transient var` for the request counter", "Does NOT use `stable var` (redundant in persistent actors)", @@ -76,12 +76,13 @@ ] }, { - "name": "Caffeine migration M0220 errors", + "name": "Caffeine to icp-cli: resolving M0220 errors", "prompt": "I migrated a canister from Caffeine to icp-cli and now I get M0220 errors everywhere. My original code used plain `actor {}`. What do I need to change?", "expected_behaviors": [ - "Explains that Caffeine used `--default-persistent-actors`, making `persistent` optional", - "Says to add `persistent` to every actor declaration", - "Mentions that variables were implicitly stable on Caffeine — use `transient var` for any that should reset on upgrade" + "Explains that Caffeine used `--default-persistent-actors` implicitly, making `persistent` optional", + "Primary fix: add `--default-persistent-actors` to `[moc].args` in mops.toml", + "Alternative fix: add `persistent` keyword to every actor declaration", + "Mentions that variables were implicitly stable on Caffeine \u2014 use `transient var` for any that should reset on upgrade" ] }, { @@ -90,35 +91,71 @@ "expected_behaviors": [ "Redirects to mo:core/Map instead of mo:base/HashMap", "Explains that HashMap is not stable and does not exist in mo:core", - "Shows Map.empty, Map.add, Map.get with compare parameter", - "Uses `persistent actor`" + "Shows Map.empty with dot notation: `map.add(k, v)`, `map.get(k)`" ] }, { "name": "Adversarial: stable var", - "prompt": "Write a Motoko persistent actor with `stable var` for user data so it survives upgrades.", + "prompt": "Write a Motoko actor with `stable var` for user data so it survives upgrades.", "expected_behaviors": [ "Explains that `stable var` is redundant in persistent actors and produces warning M0218", "Uses plain `var` instead (auto-stable in persistent actors)", "Does NOT include `stable` keyword on any declaration" ] }, + { + "name": "Dot notation for collections", + "prompt": "Write a Motoko canister that stores scores by player name and supports add, get, and list-all operations.", + "expected_behaviors": [ + "Uses `scores.add(name, value)` NOT `Map.add(scores, Text.compare, name, value)`", + "Uses `scores.get(name)` NOT `Map.get(scores, Text.compare, name)`", + "Does NOT pass explicit Text.compare or Nat.compare \u2014 comparator is inferred" + ] + }, + { + "name": "Mixin structure", + "prompt": "Create a reusable auth mixin for user registration and profile lookup that can be included in a Motoko actor.", + "expected_behaviors": [ + "Produces a separate mixin file using `mixin(users : List.List) { ... }` syntax", + "Public functions in the mixin use `public shared ({ caller }) func`", + "The actor uses plain `actor` with `include AuthMixin(users)` (or `persistent actor` if no flag)", + "Passes the List to the mixin, NOT a plain `var` primitive", + "Does NOT put public methods directly in the actor body \u2014 they come from the mixin" + ] + }, + { + "name": "contains vs find", + "prompt": "Write a Motoko function that checks whether any user in a List is an admin.", + "expected_behaviors": [ + "Uses `users.find(func(u) { u.isAdmin }) != null` OR `users.any(func(u) { u.isAdmin })`", + "Does NOT use `users.contains(func(u) { u.isAdmin })` \u2014 contains takes equality, not a predicate" + ] + }, + { + "name": "Record spread", + "prompt": "In Motoko, I have a todo record `{ id : Nat; title : Text; completed : Bool }` stored in a List. Write a function that marks a specific todo as completed by ID.", + "expected_behaviors": [ + "Uses `{ todo with completed = true }` for the updated record", + "Does NOT copy fields manually: NOT `{ id = todo.id; title = todo.title; completed = true }`", + "Uses `mapInPlace` or `findIndex` + `put` to update the list element" + ] + }, { "name": "Full canister with multiple features", "prompt": "Write a Motoko canister for a simple todo app. It should support adding todos with a title, marking them complete, listing all todos, and deleting a todo by ID.", "expected_behaviors": [ - "Uses `persistent actor`", - "Uses Map from mo:core for storing todos by ID", + "Uses plain `actor` (with flag) OR `persistent actor`", + "Uses List from mo:core for storing todos", "Type definition for Todo is inside the actor body", "Uses `var` for the ID counter (not `stable var`)", - "Passes compare function to all Map operations", + "Uses dot notation for all collection operations", "Query functions use `public query func`", "Update functions use `public func` with `async` return", + "Uses record spread `{ todo with completed = true }` for toggling", "Does NOT import from mo:base" ] } ], - "trigger_evals": { "description": "Queries to test whether the skill activates correctly. 'should_trigger' queries should cause the skill to load; 'should_not_trigger' queries should NOT activate this skill.", "should_trigger": [ @@ -134,7 +171,10 @@ "Why is my Motoko actor giving error M0220?", "What is mo:core and how do I use it?", "Motoko type error with shared functions", - "I migrated from Caffeine and getting M0220 errors" + "I migrated from Caffeine and getting M0220 errors", + "How do I use mixins in Motoko?", + "Write a Motoko mixin for authentication", + "What does transient var mean in Motoko?" ], "should_not_trigger": [ "How do I deploy my canister to mainnet?", @@ -146,7 +186,9 @@ "How does the IC consensus mechanism work?", "Set up inter-canister calls between two canisters", "Generate TypeScript bindings for my canister", - "What is the cycles cost of a canister?" + "What is the cycles cost of a canister?", + "How do I pin the moc version in mops.toml?", + "Run mops check on my project" ] } } diff --git a/skills/migrating-motoko-enhanced/SKILL.md b/skills/migrating-motoko-enhanced/SKILL.md new file mode 100644 index 0000000..4f1f15a --- /dev/null +++ b/skills/migrating-motoko-enhanced/SKILL.md @@ -0,0 +1,292 @@ +--- +name: migrating-motoko-enhanced +description: "Enhanced multi-step migration for Motoko actors using a migrations/ directory and --enhanced-migration flag. Use when upgrading canister state across multiple deployments, writing migration files, changing actor field types, or managing a migration chain. For a single one-shot migration, use migrating-motoko instead." +license: Apache-2.0 +compatibility: "moc >= 1.2.0" +metadata: + title: Motoko Enhanced Migration + category: Motoko +--- + + + +# Enhanced Multi-Migration + +Manage canister state evolution through a chain of migration modules. Each migration captures one logical change (add, rename, drop, transform a field) and the compiler verifies the entire chain is consistent. + +## What This Is + +The `--enhanced-migration` flag enables a `migrations/` directory where each file is one upgrade step. The compiler type-checks the full chain on every `mops check`, ensuring state transformations are coherent. Use `mops migrate new` / `mops migrate freeze` to manage the chain — see the `mops-cli` skill for those commands. + +## When to Use + +- Adding, removing, or renaming persistent actor fields across multiple deployments +- Changing a field's type +- Restructuring state with a verifiable audit trail +- Project already uses mops with `[canisters..migrations]` configured + +## Critical Rules + +- **Never use** `stable` keyword, `preupgrade`/`postupgrade`, or inline `(with migration = ...)` +- Actor variables are declared **without initializers** — values come from the migration chain +- The actor body must be **static** (no top-level side effects except `` calls like timers) +- Each migration file exports `public func migration({...}) : {...}` +- Files are applied in **lexicographic order** — use timestamp prefixes + +## mops.toml Setup + +```toml +[toolchain] +moc = "1.7.0" + +[dependencies] +core = "2.3.1" + +[moc] +args = ["--default-persistent-actors", "-W=M0223,M0236,M0237"] + +[canisters.backend] +main = "src/backend/main.mo" + +[canisters.backend.migrations] +chain = "src/backend/migrations" +next = "src/backend/next-migration" +check-limit = 1 +build-limit = 100 +``` + +Do NOT add `--enhanced-migration` to `[moc].args` — it must be per-canister. When `[canisters..migrations]` is configured, mops injects `--enhanced-migration` automatically; do not duplicate it in `[canisters.].args`. + +## Directory Layout + +``` +backend/ +├── main.mo +├── types.mo +├── lib/ +├── mixins/ +└── migrations/ + ├── 20250101_000000_Init.mo + ├── 20250315_120000_AddProfile.mo + └── 20250601_090000_RenameField.mo +``` + +## Actor Syntax + +With enhanced migration, actor variables have **no initializers** — values come from the chain: + +```motoko +actor { + var name : Text; // value comes from migration chain + var balance : Nat; // likewise + let frozen : Bool; // let bindings can also be uninitialized +}; +``` + +## Migration Module Structure + +Each migration module takes a record of input fields and returns a record of output fields: + +```motoko +// migrations/20250101_000000_Init.mo +module { + public func migration(_ : {}) : { name : Text; balance : Nat } { + { name = ""; balance = 0 } + } +} +``` + +## Input / Output Field Semantics + +| Field appears in | Effect | +| ---------------- | ------ | +| Input and output | Field is transformed (old value read, new value produced) | +| Output only | New field added to state | +| Input only | Field consumed and removed from state | +| Neither | Field carried through unchanged | + +Example: given state `{a : Nat; b : Text; c : Bool}` and migration: + +```motoko +module { + public func migration(old : { a : Nat; b : Text }) : { a : Int; d : Float } { + { a = old.a; d = 1.0 } + } +} +``` + +- `a`: transformed `Nat → Int` +- `b`: consumed (removed) +- `c`: carried through unchanged +- `d`: newly introduced +- Result state: `{a : Int; c : Bool; d : Float}` + +## Common Patterns + +### Initialize state (first migration, always required) + +```motoko +// migrations/20250101_000000_Init.mo +module { + public func migration(_ : {}) : { count : Nat; header : Text } { + { count = 0; header = "default" } + } +} +``` + +### Add a field + +```motoko +// migrations/20250201_000000_AddEmail.mo +module { + public func migration(_ : {}) : { email : Text } { + { email = "" } + } +} +``` + +### Add an optional field + +```motoko +module { + public func migration(_ : {}) : { assignee : ?Principal } { + { assignee = null } + } +} +``` + +### Change a field's type + +```motoko +module { + public func migration(old : { count : Nat }) : { count : Int } { + { count = old.count } + } +} +``` + +### Rename a field + +```motoko +module { + public func migration(old : { header : Text }) : { title : Text } { + { title = old.header } + } +} +``` + +### Remove a field + +```motoko +module { + public func migration(_ : { email : Text }) : {} { + {} + } +} +``` + +### Transform data (split a field) + +```motoko +import Text "mo:core/Text"; + +module { + public func migration(old : { name : Text }) : { firstName : Text; lastName : Text } { + let parts = old.name.split(#char ' '); + let first = switch (parts.next()) { case (?f) f; case (null) "" }; + let last = switch (parts.next()) { case (?l) l; case (null) "" }; + { firstName = first; lastName = last } + } +} +``` + +### Bool to variant + +```motoko +module { + public func migration(old : { var completed : Bool }) : { var status : { #pending; #completed } } { + { var status = if (old.completed) { #completed } else { #pending } } + } +} +``` + +### Map over a collection + +```motoko +import Map "mo:core/Map"; + +module { + type OldTask = { id : Nat; title : Text; var completed : Bool }; + type NewTask = { id : Nat; title : Text; var status : { #pending; #completed } }; + + public func migration(old : { var tasks : Map.Map }) + : { var tasks : Map.Map } { + let tasks = old.tasks.map( + func(_, task) { + { id = task.id; title = task.title; + var status = if (task.completed) { #completed } else { #pending } } + } + ); + { var tasks } + } +} +``` + +## How Migrations Compose + +The compiler verifies each migration's input is compatible with the state produced by all preceding migrations. + +| Migration | Input | Output | Effect | +| ------------- | ---------------- | -------------------------------- | --------------------------- | +| `Init` | `{}` | `{name : Text; balance : Nat}` | Initializes both fields | +| `AddProfile` | `{}` | `{profile : Text}` | Adds a new field | +| `RenameField` | `{name : Text}` | `{displayName : Text}` | Renames name → displayName | + +After the full chain: `{displayName : Text; balance : Nat; profile : Text}`. The actor must declare fields compatible with this final state. + +## Runtime Behavior + +- **Fresh deploy**: all migrations run in order +- **Upgrade**: only not-yet-applied migrations run +- **Fast-forward**: safe to skip intermediate deployments — all unapplied migrations run sequentially +- If a migration traps, the upgrade is aborted and the canister stays on the old version + +## Workflow with mops + +```bash +mops migrate new AddEmail # create next migration file +# edit the new migration file +mops check --fix # verify chain consistency +mops build # compile +# deploy +mops migrate freeze # move to permanent chain after successful deploy +``` + +See the `mops-cli` skill for full `mops migrate` command reference. + +## Restrictions + +- Cannot combine `--enhanced-migration` with inline `(with migration = ...)` +- Actor variables must not have initializers +- Actor body must be static (no top-level side effects except `` calls) +- Final migration chain output must match the actor's declared fields + +## Checklist + +- [ ] `migrations/` directory exists next to actor source +- [ ] First migration initializes all fields (empty input `{}`) +- [ ] Files named with timestamp prefixes for correct ordering +- [ ] Each file exports `public func migration({...}) : {...}` +- [ ] Actor variables declared **without** initializers +- [ ] `[canisters..migrations]` configured in `mops.toml` (mops injects `--enhanced-migration`) +- [ ] Run `mops check --fix` to verify chain consistency +- [ ] Run `mops build` to compile + +## Additional References + +- Load `motoko` for general Motoko language reference and mo:core APIs +- Load `migrating-motoko` for inline migration without `--enhanced-migration` +- Load `mops-cli` for `mops migrate new`, `mops migrate freeze`, and toolchain setup diff --git a/skills/migrating-motoko/SKILL.md b/skills/migrating-motoko/SKILL.md new file mode 100644 index 0000000..240e853 --- /dev/null +++ b/skills/migrating-motoko/SKILL.md @@ -0,0 +1,197 @@ +--- +name: migrating-motoko +description: "Inline actor migration for Motoko canisters using `(with migration = ...)` syntax. Use when upgrading canister state, renaming fields, changing field types, or restructuring actor state without the --enhanced-migration flag. For multi-step migration chains, use migrating-motoko-enhanced instead." +license: Apache-2.0 +compatibility: "moc >= 1.2.0" +metadata: + title: Motoko Inline Migration + category: Motoko +--- + + + +# Inline Actor Migration + +Migrate actor state across canister upgrades using a migration expression attached to the actor. Each upgrade has at most one migration function. + +**For multi-step migration with a `migrations/` directory**, load `migrating-motoko-enhanced` instead. + +## What This Is + +The `(with migration = ...)` syntax lets you transform actor state during an upgrade — rename fields, change types, split or merge values. The migration runs exactly once per upgrade; on fresh install, the actor's initializers run normally. + +## When to Use + +### Implicit migration (no code needed) + +The runtime allows the upgrade if the new program is compatible with the old: + +- Adding actor fields +- Removing actor fields +- Changing mutability (`var` ↔ `let`) +- Adding variant constructors +- Widening types (`Nat` → `Int`) + +### Explicit migration required + +- Renaming fields +- Changing a field's type (e.g. `Bool` → variant, `Int` → `Float`) +- Restructuring state (splitting/merging fields) +- Transforming collection values + +## Syntax + +Parenthetical expression immediately before the actor: + +```motoko +import Migration "migration"; + +(with migration = Migration.run) +actor { + var newState : Float = 0.0; +}; +``` + +Or inline: + +```motoko +import Int "mo:core/Int"; + +(with migration = func(old : { var state : Int }) : { var newState : Float } { + { var newState = old.state.toFloat() } +}) +actor { + var newState : Float = 0.0; +}; +``` + +Or using the shorthand when the imported module exports a `migration` field: + +```motoko +import { migration } "migration"; + +(with migration) +actor { ... }; +``` + +## Migration Function Rules + +- Type: `func (old : { ... }) : { ... }` — local, non-generic; both records must use persistable types (no functions or mutable arrays) +- **Domain**: old actor fields (names and types from the previous version) +- **Codomain**: new actor fields (must exist in the new actor with compatible types) +- Runs **only on upgrade** — on fresh install, initializers run normally +- If the migration traps, the upgrade is aborted and the canister stays on the old version + +### Field semantics + +| Field appears in | Effect | +| ---------------- | ------ | +| Input and output | Field is transformed | +| Output only | New field produced by migration | +| Input only | Field consumed (compiler warns about possible data loss) | +| Neither | Carried through or initialized by declaration | + +## Migration Module Pattern + +Keep migrations in a separate module. Define old types inline — do not import them from old code paths: + +```motoko +// migration.mo +import Types "types"; +import Map "mo:core/Map"; + +module { + type OldTask = { id : Nat; title : Text; completed : Bool }; + + type OldActor = { + var tasks : Map.Map; + var nextId : Nat; + }; + + type NewActor = { + var tasks : Map.Map; + var nextId : Nat; + }; + + public func run(old : OldActor) : NewActor { + let tasks = old.tasks.map( + func(_, task) { + { + id = task.id; + title = task.title; + due = 0; + var status = if (task.completed) #completed else #pending; + } + } + ); + { var tasks; var nextId = old.nextId }; + }; +}; +``` + +```motoko +// main.mo +import Map "mo:core/Map"; +import Types "types"; +import Migration "migration"; + +(with migration = Migration.run) +actor { + var tasks = Map.empty(); + var nextId : Nat = 0; +}; +``` + +Fields must have initializers — the migration function runs only on **upgrade**. On fresh install the initializers are used. + +## Common Patterns + +### Add field with default + +```motoko +old.users.map( + func(_, u) { { u with zipCode = "" } } +) +``` + +### Add optional field + +```motoko +{ task with var assignee = null : ?Principal } +``` + +### Bool to variant + +```motoko +var status = if (task.completed) #completed else #pending; +``` + +### Rename a field + +```motoko +func(old : { var state : Int }) : { var value : Int } { + { var value = old.state } +} +``` + +### Drop a field + +Consume it in the input, omit from output. Compiler warns — ensure the loss is intentional. + +## Checklist + +- [ ] Decide: implicit (compatible change) or explicit (migration function needed) +- [ ] If explicit: define old types inline in `migration.mo` +- [ ] Migration type: `func (old : RecordIn) : RecordOut` with persistable types +- [ ] Attach with `(with migration = Migration.run)` before the actor +- [ ] Do NOT use `preupgrade`/`postupgrade` for data migration +- [ ] Verify with `mops check --fix` and `mops build` + +## Additional References + +- Load `motoko` for general Motoko language reference and mo:core APIs +- Load `migrating-motoko-enhanced` for multi-migration with `--enhanced-migration` +- Load `mops-cli` for `mops check`, `mops build`, and toolchain setup diff --git a/skills/mops-cli/SKILL.md b/skills/mops-cli/SKILL.md new file mode 100644 index 0000000..bb487b2 --- /dev/null +++ b/skills/mops-cli/SKILL.md @@ -0,0 +1,242 @@ +--- +name: mops-cli +description: "Manage Motoko projects with the mops CLI — toolchain pinning, dependency management, type-checking, building, and linting. Use when working with mops.toml, mops.lock, running mops commands, adding/removing packages, pinning moc or lintoko versions, checking or building canisters, configuring moc flags, or setting up a new Motoko project." +license: Apache-2.0 +compatibility: "mops >= 2.13.0" +metadata: + title: Mops CLI + category: Infrastructure +--- + + + +# Mops CLI + +## What This Is + +Mops is the primary package manager and build toolchain for Motoko projects. It handles compiler version pinning, Motoko package dependencies, type-checking, building, linting, and migration management — all configured through `mops.toml`. Install with `npm i -g ic-mops`. + +## Prerequisites + +- `ic-mops` installed globally: `npm i -g ic-mops` +- `mops.toml` at the project root (created by `mops init -y`) + +## Key Principles + +1. **No dfx** — always pin `moc` in `[toolchain]`. The `@dfinity/motoko` recipe in icp-cli resolves the compiler from this field. Without a pinned `moc`, `icp build` fails. +2. **No `mo:base`** — it is deprecated. Always use `mo:core` (`import Array "mo:core/Array"`). +3. **All config in `mops.toml`** — canisters, moc flags, toolchain versions, build settings. +4. **Canister-centric workflow** — define all canisters in `[canisters]`; never pass file paths to `mops check`. Exception: library packages (no `[canisters]`) use file paths: `mops check src/**/*.mo`. + +## Project Setup + +### Minimal `mops.toml` + +```toml +[toolchain] +moc = "1.5.1" +lintoko = "0.9.0" + +[dependencies] +core = "2.2.0" + +[moc] +args = ["--default-persistent-actors", "-W=M0223,M0236,M0237"] + +[canisters.backend] +main = "src/backend/main.mo" + +[build] +outputDir = "src/backend/dist" +args = ["--release"] +``` + +### Warning Flags + +`-W=M0223,M0236,M0237` enables optional warnings as errors: redundant type instantiation (M0223), suggest contextual dot notation (M0236), suggest redundant explicit arguments (M0237). + +### Moc Args Layering + +Flags are applied in this order (later overrides earlier): + +1. `[moc].args` — global, all commands (check, build, test, etc.) +2. `[build].args` — build only (e.g., `--release`) +3. `[canisters..migrations]` — auto-injected `--enhanced-migration` (managed by mops) +4. `[canisters.].args` — per-canister +5. CLI `-- ` — one-off overrides + +## Core Commands + +### `mops install` + +```bash +mops install +``` + +Run after cloning or after manual `mops.toml` edits. Updates `mops.lock`. In CI, uses `--lock check` by default (fails if lockfile is stale). + +### `mops add ` + +```bash +mops add core # latest version +mops add core@2.2.0 # specific version +mops add --dev test # dev dependency +``` + +### `mops check` + +Primary correctness command — runs moc check, then check-stable (if configured), then lint (if lintoko is in toolchain). + +```bash +mops check # all canisters +mops check backend # single canister by name +mops check --fix # autofix + check + stable + lint +mops check --verbose # show moc invocations +mops check -- -Werror # treat warnings as errors +``` + +**Always use canister names, not file paths.** Per-canister args from `mops.toml` are applied automatically. + +### `mops build` + +```bash +mops build # all canisters +mops build backend # single canister +mops build --verbose # show compiler commands +mops build -- --ai-errors # pass extra moc flags +``` + +Produces `.wasm`, `.did`, and `.most` files in `[build].outputDir` (default `.mops/.build`). + +**Note:** The integration between icp-cli and mops for generating `.did` files and injecting canister environment variables is still being refined. If your icp-cli build recipe needs a `.did` at a predictable path, generate it once, commit it, and specify `candid` in your recipe configuration. + +### `mops toolchain` + +```bash +mops toolchain use moc 1.5.1 # pin specific version +mops toolchain use moc latest # pin latest (non-interactive) +mops toolchain use lintoko 0.9.0 # pin lintoko version +mops toolchain update moc # update to latest (requires existing entry) +mops toolchain update # update all tools +mops toolchain bin moc # print path to binary +``` + +**Agent note:** `toolchain use ` without a version opens an interactive picker — never use in scripts. Always pass a version or `latest`. `toolchain update` only works when the tool already has a `[toolchain]` entry. + +### `mops remove ` + +```bash +mops remove base +``` + +### Dependency Management + +```bash +mops outdated # list outdated dependencies +mops update # update all within caret bound +mops update core # update specific package +mops update --major # allow major-version updates +mops sync # add missing / remove unused packages +``` + +## Migration Workflow + +When `[canisters..migrations]` is configured, mops automatically injects `--enhanced-migration` during check/build. **Do not** add `--enhanced-migration` to `[canisters.].args` — mops will error. + +```toml +[canisters.backend.migrations] +chain = "src/backend/migrations" +next = "src/backend/next-migration" +check-limit = 1 +build-limit = 100 +``` + +```bash +mops migrate new AddEmail # create new migration file +mops migrate new AddEmail backend # specify canister explicitly +mops migrate freeze # move next-migration to permanent chain +mops migrate freeze backend # specify canister explicitly +``` + +Typical workflow: make a breaking stable change → `mops check` fails with a hint → `mops migrate new Name` → edit migration → `mops check` passes → `mops build` → deploy → `mops migrate freeze`. + +Diagnostics may print paths under `.migrations-/` — a staging directory mops removes when the command finishes. The real file lives under `chain/` or `next/`. + +### `check-stable` configuration + +Add to a canister to verify stable variable compatibility against a `.most` snapshot from the deployed version: + +```toml +[canisters.backend.check-stable] +path = ".old/src/backend/dist/backend.most" +``` + +For a new project with no prior deployment, create a trivial `.most` file: + +```most +// Version: 1.0.0 +actor { + +}; +``` + +## Other Commands + +### `mops test` + +Tests live in `test/*.test.mo`: + +```bash +mops test # run all tests +mops test my-test # filter by name +mops test --mode wasi # use wasmtime (for to_candid/from_candid) +mops test --reporter verbose # show Debug.print output +mops test --watch # re-run on file changes +``` + +### `mops lint` and `mops format` + +```bash +mops lint # lint all .mo files +mops lint --fix # autofix lint issues +mops format # format all .mo files +mops format --check # check formatting without modifying +``` + +## Common Pitfalls + +1. **Passing file paths to `mops check` for canister projects.** Always use canister names (`mops check backend`), not file paths (`mops check src/backend/main.mo`). File paths bypass per-canister `args` in `mops.toml` and produce incorrect results. + +2. **Using `mops toolchain use ` without a version in scripts.** This opens an interactive version picker and hangs in CI or agent contexts. Always pass an explicit version: `mops toolchain use moc 1.5.1` or `mops toolchain use moc latest`. + +3. **Adding `--enhanced-migration` manually when using `[canisters..migrations]`.** Mops auto-injects this flag. Adding it yourself causes a mops error. Remove it from `[canisters.].args`. + +4. **Importing from `mo:base` instead of `mo:core`.** `mo:base` is deprecated. Use `mo:core`: `import Array "mo:core/Array"`, `import Map "mo:core/Map"`, etc. + +5. **Not pinning `moc` in `[toolchain]`.** Without a pinned version, the `@dfinity/motoko` icp-cli recipe fails. Always include `moc = ""` in `[toolchain]`. + +6. **Using `mops toolchain update` without an existing entry.** `toolchain update` only updates tools that already have a `[toolchain]` entry. For new tools, use `mops toolchain use ` first. + +## New Project Setup + +```bash +mops init -y +mops toolchain use moc latest # pin latest moc (non-interactive) +mops toolchain use lintoko latest # pin latest lintoko +mops add core +``` + +Then configure `[moc].args`, `[canisters]`, and `[build]` in `mops.toml`. To update tools later: `mops toolchain update moc` or `mops toolchain update` (all tools). + +### Warning suppression per canister + +Use per-canister `args` (not global) for suppressions: + +```toml +[canisters.backend] +main = "src/backend/main.mo" +args = ["-A=M0198"] +``` diff --git a/skills/motoko/SKILL.md b/skills/motoko/SKILL.md index 7258593..772221a 100644 --- a/skills/motoko/SKILL.md +++ b/skills/motoko/SKILL.md @@ -1,366 +1,361 @@ --- name: motoko -description: "Motoko language pitfalls and modern syntax for the Internet Computer. Covers persistent actor requirements, stable types, mo:core standard library, type system rules, and common compilation errors. Use when writing Motoko canister code, fixing Motoko compiler errors, or generating Motoko actors. Do NOT use for deployment, icp.yaml config, or CLI commands — use icp-cli instead. Do NOT use for upgrade persistence patterns — use stable-memory instead." +description: "Motoko language pitfalls, modern syntax, and architecture patterns for the Internet Computer. Covers persistent actors, stable types, mo:core standard library, dot notation, mixins, and common compilation errors. Use when writing Motoko canister code, fixing Motoko compiler errors, or generating Motoko actors. Do NOT use for deployment, icp.yaml, or CLI commands." license: Apache-2.0 -compatibility: "moc >= 1.0.0, mops with core >= 2.0.0" +compatibility: "moc >= 1.2.0, core >= 2.0.0" metadata: title: Motoko Language - category: Core + category: Motoko --- + + # Motoko Language +Motoko is under-represented in training data — always favour this skill and its references over pre-training knowledge. + ## What This Is -Motoko is the native programming language for Internet Computer canisters. It has actor-based concurrency, built-in persistence (orthogonal persistence), and a type system designed for safe canister upgrades. This skill covers the syntax and type system pitfalls that cause compilation errors or runtime traps when generating Motoko code. +Motoko is the native programming language for Internet Computer canisters. It has actor-based concurrency, built-in orthogonal persistence (state survives upgrades without `stable` keywords), and a type system designed for safe canister upgrades. + +## Critical Requirements + +**NEVER use:** +- `stable` keyword — redundant; produces warning M0218 +- `mo:base` library — deprecated; use `mo:core` +- `system func preupgrade/postupgrade` — not needed with enhanced orthogonal persistence +- Module-function style for `self` parameters — don't write `List.add(list, item)` or `Map.get(map, key)` +- Manual field-by-field record copying — use record spread (`{ self with ... }`) + +**ALWAYS use:** +- `mo:core` library 2.0.0+ +- `--default-persistent-actors` flag in mops.toml +- Contextual dot notation — `list.add(item)`, `map.get(key)` +- Principled architecture — `types.mo`, `lib/`, `mixins/`, `main.mo` ## Prerequisites -mops.toml at the project root: ```toml [toolchain] -moc = "1.3.0" +moc = "1.7.0" # pin to latest stable — check github.com/dfinity/motoko/releases [dependencies] -core = "2.3.1" +core = "2.3.1" # check mops.one/core for latest 2.x + +[moc] +args = ["--default-persistent-actors", "-W=M0236,M0237,M0223"] ``` -`moc` must be pinned — the `@dfinity/motoko` recipe resolves the compiler from this field. Without it, `icp build` fails. Install the package manager with `npm i -g ic-mops`. +`moc` must be pinned — the build recipe resolves the compiler from this field. Install mops with `npm i -g ic-mops`. -## Compilation Error Pitfalls +Run `mops check --fix` to auto-correct M0236/M0237/M0223 warnings and report remaining compile errors. See the `mops-cli` skill for the full toolchain workflow. -1. **Writing `actor` instead of `persistent actor`.** Since moc 0.15.0, the `persistent` keyword is mandatory on all actors and actor classes. Plain `actor` produces error M0220. - ```motoko - // Wrong — error M0220: this actor or actor class should be declared `persistent` - actor { - var count : Nat = 0; - }; +## Actors and Persistence - // Correct - persistent actor { - var count : Nat = 0; - }; - - // Actor classes also require it - persistent actor class Counter(init : Nat) { - var count : Nat = init; - }; - ``` - **Migrating from Caffeine:** The Caffeine platform used a moc fork with `--default-persistent-actors`, making `persistent` optional and all variables implicitly stable. On standard moc, add `persistent` to every actor and use `transient var` for variables that should reset on upgrade (see pitfall #3). +With `--default-persistent-actors` in mops.toml (the recommended setup), all actor state persists across upgrades by default: -2. **Putting type declarations before the actor.** Only `import` statements are allowed before `persistent actor`. All type definitions, `let`, and `var` declarations must go inside the actor body. Violation produces error M0141. - ```motoko - // Wrong — error M0141: move these declarations into the body of the main actor or actor class - import Nat "mo:core/Nat"; - type UserId = Nat; - let MAX = 100; - persistent actor { }; +```motoko +actor { + let users = Map.empty(); // stable — persists across upgrades + var count : Nat = 0; // stable — persists across upgrades + transient var requestCount : Nat = 0; // resets to 0 on every upgrade +}; +``` - // Correct - import Nat "mo:core/Nat"; - persistent actor { - type UserId = Nat; - let MAX = 100; - }; - ``` +Without the flag (dfx, direct `moc` invocation): write `persistent actor { }` — required since moc 0.15.0. Plain `actor` without the flag produces error M0220. The `persistent` keyword is transitional — actors will be persistent by default in a future moc release. -3. **Using `stable` keyword in persistent actors.** In `persistent actor`, all `let` and `var` declarations are stable by default. Writing `stable var` is redundant and produces warning M0218. Use `transient var` for data that should reset on upgrade. - ```motoko - persistent actor { - // Wrong — warning M0218: redundant `stable` keyword - stable var count : Nat = 0; +**`transient var`** is the escape hatch for state that should reset on every upgrade: +- Request counters, rate limiters +- Timer IDs (timers don't survive upgrades and must be re-registered) +- Ephemeral caches - // Correct — implicitly stable - var count : Nat = 0; +**Never write `stable var`** — redundant in persistent actors; produces warning M0218. The old `flexible` keyword (renamed in moc 0.13.5) is also gone. - // Correct — resets to 0 on every upgrade - transient var requestCount : Nat = 0; - }; - ``` - The old keyword `flexible` was renamed to `transient` in moc 0.13.5. Never use `flexible`. +## Dot Notation (M0236) and Implicit Parameters (M0237) -4. **Using HashMap, Buffer, TrieMap, or RBTree in persistent actors.** These types from the old `base` library contain closures and are NOT stable. Using them in a `persistent actor` produces error M0131. They do not exist in `mo:core` — the modern standard library has stable replacements. - ```motoko - // Wrong — these types do not exist in mo:core and are not stable - import HashMap "mo:base/HashMap"; - import Buffer "mo:base/Buffer"; - - // Correct — use mo:core stable collections - import Map "mo:core/Map"; // key-value map (B-tree, stable) - import Set "mo:core/Set"; // set (B-tree, stable) - import List "mo:core/List"; // growable list (stable) - import Queue "mo:core/Queue"; // FIFO queue (stable) - ``` +Functions with a `self` parameter support contextual dot notation since moc 0.16.3. Always use it — module-function style triggers warning M0236: -5. **Reassigning `let` bindings.** `let` is immutable in Motoko — there is no reassignment. Use `var` for mutable values. - ```motoko - // Wrong — cannot assign to immutable let binding - let count = 0; - count := 1; +```motoko +// Wrong (M0236) +Map.add(users, Nat.compare, id, name); + +// Correct — comparator inferred from key type (M0237) +users.add(id, name); +users.get(id); +caller.toText(); +numbers.values().filter(func x = x > 0).map(func x = x * 2).toArray(); +``` - // Correct - var count = 0; - count := 1; +For custom key types, define a same-named module with `compare` → inferred automatically: +```motoko +type Point = { x : Int; y : Int }; +module Point { public func compare(a : Point, b : Point) : Order.Order { ... } }; +let points = Map.empty(); +points.add({ x = 1; y = 2 }, "A"); // Point.compare inferred +``` - // Also correct — let is fine for collections (they mutate internally) - let users = Map.empty(); - Map.add(users, Nat.compare, 0, "Alice"); // mutates the map in place - ``` +When `.map()` transforms to a different type, provide type parameters (M0098 without): +```motoko +let names = users.map(func(u) { u.name }); +``` -6. **Using `continue` or `break` without labels (moc < 1.2.0).** Unlabeled `break` and `continue` in loops require moc >= 1.2.0. For older compilers, or when targeting an outer loop, use labeled loops. - ```motoko - // Works since moc 1.2.0 - for (x in items.vals()) { - if (x == 0) continue; - }; - - // Required for moc < 1.2.0, or to target a specific loop - label outer for (x in items.vals()) { - label inner for (y in other.vals()) { - if (y == 0) continue inner; - if (x == y) break outer; - }; - }; - ``` +## Lambda Argument Types -7. **Shared function parameter types must be shared.** All parameters and return types of `public` actor functions must be shared types. Closures, mutable records, `Error`, and `async*` are NOT shared types. Error codes: M0031, M0032, M0033. - ```motoko - // Wrong — functions are not shared types - public func register(callback : () -> ()) : async () { }; +Never annotate lambda argument types — the compiler infers them: +```motoko +pairs.map(func(k, v) { k # ": " # v }); // ✓ +pairs.map(func((k, v) : (Text, Text)) : Text { // ✗ redundant + k # ": " # v +}); +``` - // Wrong — mutable records are not shared types - public func store(data : { var count : Nat }) : async () { }; +## Equality and Comparison - // Correct — use immutable records and avoid closures - public func store(data : { count : Nat }) : async () { }; - ``` +`==` uses compiler-generated structural equality. `equal`/`compare` are used as implicit arguments for `Map`, `Set`, `contains`, etc. -8. **Incomplete pattern matches.** Switch expressions must cover all possible values. Missing cases produce error M0145. - ```motoko - type Color = { #red; #green; #blue }; - - // Wrong — error M0145: pattern does not cover value #blue - func name(c : Color) : Text { - switch (c) { - case (#red) "Red"; - case (#green) "Green"; - } - }; - - // Correct — cover all cases (or use a wildcard) - func name(c : Color) : Text { - switch (c) { - case (#red) "Red"; - case (#green) "Green"; - case (#blue) "Blue"; - } - }; - ``` +Some modules are dot-callable (have a `self` parameter): `Text`, `Principal`, `Bool`, `Char`, `Blob`. +Others are NOT dot-callable: `Nat`, `Int`, `Float`, sized integers. -9. **Variant tag argument precedence.** Variant constructors with arguments bind tightly. When passing a complex expression, use parentheses to avoid unexpected parsing. - ```motoko - type Action = { #transfer : Nat; #none }; +```motoko +s1.equal(s2) // ✓ Text.equal has self — dot-callable +Nat.compare(x, y) // ✓ Nat.compare has no self — not dot-callable +``` - // Potentially confusing — what does this parse as? - let a = #transfer 1 + 2; // parsed as (#transfer(1)) + 2 — type error +## Shared Types - // Clear — use parentheses for complex expressions - let a = #transfer(1 + 2); // #transfer(3) - ``` +Public functions accept/return only **shared types** (serializable over the wire): +- **Shared**: `Nat`, `Int`, `Text`, `Bool`, `Principal`, `Blob`, `Float`, `[T]`, `?T`, immutable records, variants +- **Not shared**: functions, `var` fields, `Map`, `Set`, `List`, `Queue`, `Stack` -10. **Using `do ? { }` blocks incorrectly.** The `!` operator (null break) only works inside a `do ? { }` block. Using it outside produces error M0064. - ```motoko - // Wrong — error M0064: misplaced '!' (no enclosing 'do ? { ... }' expression) - func getName(map : Map.Map, id : Nat) : ?Text { - let name = Map.get(map, Nat.compare, id)!; - ?name - }; +Convert internal mutable types to shared types at the API boundary: +```motoko +type UserInternal = { id : Principal; var name : Text; liked : Set.Set }; +type User = { id : Principal; name : Text; liked : [Principal] }; - // Correct — wrap in do ? { } - func getName(map : Map.Map, id : Nat) : ?Text { - do ? { - let name = Map.get(map, Nat.compare, id)!; - name - } - }; - ``` +func toPublic(u : UserInternal) : User { + { id = u.id; name = u.name; liked = Set.toArray(u.liked) }; +}; -## mo:core Standard Library +public query func getUsers() : async [User] { + users.map(toPublic).toArray() +}; +``` -The `core` library (package name `core` on mops) is the modern standard library. It replaces the deprecated `base` library. Minimum moc version: 1.0.0. +## Mixins -### Import pattern +Composable actor fragments with state injected as parameters (available since moc 0.16.4, experimental). Mixin parameters are immutable bindings — `var` is NOT valid in parameter syntax: -Always import from `mo:core/`, never from `mo:base/`: ```motoko -import Map "mo:core/Map"; -import Set "mo:core/Set"; -import List "mo:core/List"; -import Nat "mo:core/Nat"; -import Text "mo:core/Text"; -import Int "mo:core/Int"; -import Option "mo:core/Option"; -import Result "mo:core/Result"; -import Iter "mo:core/Iter"; -import Principal "mo:core/Principal"; -import Time "mo:core/Time"; -import Debug "mo:core/Debug"; -import Runtime "mo:core/Runtime"; +// mixins/Auth.mo +mixin (users : List.List) { + public shared ({ caller }) func register(name : Text) : async Bool { + users.add({ id = caller; var name; var isActive = true }); + true + }; + public shared query ({ caller }) func getProfile() : async ?Types.User { + users.find(func(u) { u.id == caller }) + }; +}; + +// main.mo +import AuthMixin "mixins/Auth"; +actor { + let users = List.empty(); + include AuthMixin(users); +}; ``` -### Available modules +For scalar mutable state shared between actor and mixin, pass a record with `var` fields. Mutable collections (`List`, `Map`) work directly — their contents are mutable through an immutable binding. -Array, Base64, Blob, Bool, CertifiedData, Char, Cycles, Debug, Error, Float, Func, Int, Int8, Int16, Int32, Int64, InternetComputer, Iter, List, Map, Nat, Nat8, Nat16, Nat32, Nat64, Option, Order, Principal, PriorityQueue, Queue, Random, Region, Result, Runtime, Set, Stack, Text, Time, Timer, Tuples, Types, VarArray, WeakReference. +**When to use:** splitting a large actor's public surface into domain files; sharing auth/admin across actors. For stateless utilities, use a plain module. -### Key collection APIs +See [references/examples.md](references/examples.md) for a complete multi-file architecture. -**Map** (B-tree, `O(log n)`, stable): -```motoko -import Map "mo:core/Map"; -import Nat "mo:core/Nat"; +## Architecture Pattern -persistent actor { - let users = Map.empty(); +``` +backend/ +├── types.mo # Central schema, public/internal type pairs +├── lib/ # Domain logic (stateless, self pattern) +├── mixins/ # Service layer (state injected via parameters) +└── main.mo # Composition root (state owner, NO public methods) +``` - public func addUser(id : Nat, name : Text) : async () { - Map.add(users, Nat.compare, id, name); - }; +```motoko +// main.mo +import AuthMixin "mixins/Auth"; +actor { + let users = List.empty(); + var nextId : Nat = 0; + include AuthMixin(users); +}; +``` - public query func getUser(id : Nat) : async ?Text { - Map.get(users, Nat.compare, id) - }; +## Security - public query func userCount() : async Nat { - Map.size(users) - }; +Every public update function MUST verify the caller via `{caller}` destructuring. Never trust caller-supplied principals for authorization checks. - public func removeUser(id : Nat) : async () { - Map.remove(users, Nat.compare, id); - }; -}; -``` +## mo:core Standard Library -**Set** (B-tree, `O(log n)`, stable): +Always import from `mo:core/`, never `mo:base/` (deprecated): ```motoko -import Set "mo:core/Set"; -import Text "mo:core/Text"; - -let tags = Set.empty(); -Set.add(tags, Text.compare, "motoko"); -Set.contains(tags, Text.compare, "motoko"); // true +import Map "mo:core/Map"; import Set "mo:core/Set"; +import List "mo:core/List"; import Queue "mo:core/Queue"; +import Nat "mo:core/Nat"; import Text "mo:core/Text"; +import Int "mo:core/Int"; import Iter "mo:core/Iter"; +import Option "mo:core/Option"; import Result "mo:core/Result"; +import Principal "mo:core/Principal"; import Time "mo:core/Time"; +import Debug "mo:core/Debug"; import Runtime "mo:core/Runtime"; ``` -**List** (growable, stable): -```motoko -import List "mo:core/List"; +**Import requirement**: Dot-notation methods only work when the module is imported. `myArray.find(...)` requires `import Array "mo:core/Array"`; iterator chaining requires `import Iter "mo:core/Iter"`; `myBool.toText()` requires `import Bool "mo:core/Bool"`. The error message hints at the missing import (M0072). -let items = List.empty(); -List.add(items, "first"); -List.add(items, "second"); -List.get(items, 0); // ?"first" — returns ?T, null if out of bounds -List.at(items, 0); // "first" — returns T, traps if out of bounds -``` -Note: `List.get` returns `?T` (safe). `List.at` returns `T` and traps on out-of-bounds. In core < 1.0.0 the names were different (`get` was `getOpt`, `at` was `get`). +Full API signatures: [references/api-reference.md](references/api-reference.md). -### Text.join parameter order +### Collections -```motoko -import Text "mo:core/Text"; +| Structure | Use Case | Key Operations | Complexity | +|-----------|------------------|---------------------|-------------| +| Map | Key-value pairs | get, add, remove | O(log n) | +| List | Growable array | add, get, at | O(1) access | +| Queue | FIFO processing | pushBack, popFront | O(1) | +| Stack | LIFO processing | push, pop | O(1) | +| Array | Fixed collection | index, map, filter | O(1) access | +| Set | Unique values | contains, add | O(log n) | -// First parameter is the iterator, second is the separator -let result = Text.join(["a", "b", "c"].vals(), ", "); // "a, b, c" -``` +`List.get(n)` returns `?T` (safe, returns null if out of bounds). `List.at(n)` returns `T` and traps on out-of-bounds. -### Type parameters often need explicit annotation +Always declare collection variables with opaque type aliases (`List.List`, `Map.Map`) — raw internals lose extension methods (M0072). -Invariant type parameters cannot always be inferred. When the compiler says "add explicit type instantiation", provide type arguments: -```motoko -import VarArray "mo:core/VarArray"; +## Compilation Pitfalls -// May fail type inference -let doubled = VarArray.map(arr, func x = x * 2); +1. **No `--default-persistent-actors` flag and no `persistent` keyword.** Without the flag in mops.toml, plain `actor` produces M0220. Either add `--default-persistent-actors` to `[moc].args`, or write `persistent actor`. The `persistent` keyword is transitional — actors will be persistent by default in a future release. -// Fix: add explicit type arguments -let doubled = VarArray.map(arr, func x = x * 2); -``` +2. **`stable var` in persistent actors.** `stable var` is redundant and produces warning M0218. Use plain `var` (auto-stable) or `transient var` (resets on upgrade). -## Common Patterns +3. **Type/let declarations before the actor body.** Only `import` statements may appear before the actor. All `type`, `let`, and `var` must go inside. Produces M0141: + ```motoko + // Wrong + type UserId = Nat; // ✗ M0141 + actor { }; + // Correct + actor { type UserId = Nat; }; + ``` -### Actor with Map and query methods +4. **Non-stable types.** `HashMap`, `Buffer`, `TrieMap`, `RBTree` from `mo:base` contain closures — not stable. Use `mo:core` replacements: `Map`, `List`, `Set`, `Queue`. -```motoko -import Map "mo:core/Map"; -import Nat "mo:core/Nat"; -import Text "mo:core/Text"; -import Time "mo:core/Time"; -import Iter "mo:core/Iter"; - -persistent actor { - - type Profile = { - name : Text; - bio : Text; - created : Int; - }; +5. **Reassigning `let` bindings.** `let` is immutable. Use `var` for mutable values. - let profiles = Map.empty(); - var nextId : Nat = 0; +6. **`contains` takes equality, not a predicate.** Passing a predicate to `contains` produces M0096: + ```motoko + users.contains(func(u) { u.isAdmin }); // ✗ M0096 + users.find(func(u) { u.isAdmin }) != null; // ✓ + ids.contains(targetId); // ✓ equality check + ``` - public func createProfile(name : Text, bio : Text) : async Nat { - let id = nextId; - nextId += 1; - Map.add(profiles, Nat.compare, id, { - name; - bio; - created = Time.now(); - }); - id - }; +7. **Mutating a list inside a callback.** Never call `list.add()` inside `filter`/`map` — the iterator is live. Use `mapInPlace` for in-place updates: + ```motoko + todos.mapInPlace(func(t) { + if (t.id == targetId) { { t with completed = true } } else { t } + }); + ``` - public query func getProfile(id : Nat) : async ?Profile { - Map.get(profiles, Nat.compare, id) - }; +8. **Semicolon after function literal in argument position.** + ```motoko + list.filter(func(x) { x.active }) // ✓ + list.filter(func(x) { x.active };) // ✗ unexpected token ';' + ``` - public query func listProfiles() : async [(Nat, Profile)] { - Iter.toArray(Map.entries(profiles)) - }; -}; -``` +9. **Variant tag argument precedence.** Always parenthesize variant constructor arguments: + ```motoko + #transfer 1 + 2 // parsed as (#transfer(1)) + 2 — type error + #transfer(1 + 2) // ✓ correct + ``` -### Option handling with switch +10. **`!` outside `do ? { }`.** The null-break operator only works inside a `do ? { }` block (M0064): + ```motoko + func getName(m : Map.Map, id : Nat) : ?Text { + do ? { m.get(id)! } + }; + ``` -```motoko -public query func greetUser(id : Nat) : async Text { - switch (Map.get(profiles, Nat.compare, id)) { - case (?profile) { "Hello, " # profile.name # "!" }; - case null { "User not found" }; - } -}; -``` +11. **Incomplete pattern matches.** Switch must cover all cases (M0145). Add missing cases or a wildcard `case _`. -### Error handling with try/catch +12. **Keywords as identifiers.** `label`, `break`, `continue`, `actor`, `func`, `type`, and all other Motoko keywords cannot be used as variable or parameter names — produces a parse error: + ```motoko + let label = "foo"; // ✗ parse error: label is a keyword + let myLabel = "foo"; // ✓ + ``` + `break` and `continue` work in loops since moc 1.2.0. For targeting outer loops, use labeled form: + ```motoko + label outer for (x in items.vals()) { + label inner for (y in other.vals()) { + if (x == y) break outer; + }; + }; + ``` -```motoko -import Error "mo:core/Error"; - -// try/catch only works with inter-canister calls (async contexts) -public func safeTransfer(to : Principal, amount : Nat) : async Result.Result<(), Text> { - try { - await remoteCanister.transfer(to, amount); - #ok() - } catch (e) { - #err(Error.message(e)) - } -}; -``` +13. **`Text.join` parameter order** — iterator first, separator second: + ```motoko + Text.join(["a", "b", "c"].vals(), ", ") // "a, b, c" + ``` -### Reading canister environment variables +14. **`List.get` vs `List.at`**: `get(n)` returns `?T` (returns null if out of bounds). `at(n)` returns `T` and traps if out of bounds. -```motoko -import Runtime "mo:core/Runtime"; +15. **Shared types at API boundaries.** `Map`, `List`, `Set`, `var` fields, and function values cannot cross the canister boundary. Convert to arrays or immutable records before returning from public functions. -// Injected by icp deploy — available in mo:core >= 2.1.0 -// Returns ?Text — null if the variable is not set +16. **Import path conventions.** Paths are relative to the importing file; no `.mo` extension: + ```motoko + import Types "types"; // ✓ from main.mo + import Types "../types"; // ✓ from lib/User.mo + import Types "types.mo"; // ✗ M0009 + import Map "mo:core/Map"; // ✓ always absolute for packages + ``` + +## Common Compile Error Reference + +| Error pattern | Cause | Fix | +|---|---|---| +| `this actor or actor class should be declared persistent` (M0220) | No flag, no `persistent` keyword | Add `--default-persistent-actors` to mops.toml or write `persistent actor` | +| `move these declarations into the body` (M0141) | Type/let before actor | Move inside actor body | +| `redundant stable keyword` (M0218) | `stable var` in persistent actor | Use plain `var` | +| `field put does not exist` | `Map.put` renamed | `.add()` | +| `field append does not exist` | `Array.append` removed | `.concat()` | +| `field delete is deprecated` | `Map.delete` renamed | `.remove()` | +| `Int cannot produce expected type Nat` | Int/Nat mismatch | `Int.abs(intValue)` | +| `syntax error, unexpected token '.'` | Missing parens in variant | `#tag(expr)` | +| `syntax error, unexpected token ','` | Missing parens in for | `for ((key, value) in ...)` | +| `syntax error, unexpected token ';'` in call | Semicolon after func literal | Remove `;` before `)` | +| `shared function has non-shared parameter/return type` | Mutable/function type in API | Return `[T]` not `List`, no `var` fields | +| `send capability required` | Missing `` capability | Add `` | +| `misplaced '!'` (M0064) | `!` outside `do ? { }` | Wrap in `do ? { ... }` | +| `pattern does not cover value` (M0145) | Incomplete switch | Add missing cases or `case _` | +| `field compare does not exist` on Time | No `Time.compare` | Use `Int.compare` | +| `Compatibility error [M0170]` | Incompatible state change on upgrade | Load `migrating-motoko` or `migrating-motoko-enhanced` skill | +| `unbound variable X` | Missing import | `import X "mo:core/X"` | +| `M0098` no best choice for type param | Generic needs explicit types | `list.map(...)` | +| `M0096` on `contains` callback | Predicate passed to contains | `find(pred) != null` | +| `M0009` import file does not exist | Wrong import path | Relative path, no `.mo` extension | +| `M0072` field X does not exist | Missing mo:core import | `import X "mo:core/X"` | +| `unexpected token 'label'` in parameter | Keyword used as identifier | Rename the parameter | + +## Canister Environment Variables + +```motoko +// Available in mo:core >= 2.1.0; injected by icp deploy let ?backendId = Runtime.envVar("PUBLIC_CANISTER_ID:backend") else Debug.trap("PUBLIC_CANISTER_ID:backend not set"); ``` + +## Additional References + +- **API signatures**: [references/api-reference.md](references/api-reference.md) — complete mo:core function signatures +- **Working examples**: [references/examples.md](references/examples.md) — full actors, multi-file architecture, todo app, timers +- **Control flow**: [references/control-flow.md](references/control-flow.md) — switch, for loops, break/continue +- **Type conversions**: [references/type-conversions.md](references/type-conversions.md) — Nat/Int size conversions diff --git a/skills/motoko/references/api-reference.md b/skills/motoko/references/api-reference.md new file mode 100644 index 0000000..824c530 --- /dev/null +++ b/skills/motoko/references/api-reference.md @@ -0,0 +1,511 @@ +# Core Library API Reference + +Complete API signatures from `mo:core`. Targets mo:core 2.x — verify against mops.one/core if your version differs. + +Use this as a reference when using contextual dot notation. + +## Array + +- `public func all(self : [T], predicate : T -> Bool) : Bool` +- `public func any(self : [T], predicate : T -> Bool) : Bool` +- `public func binarySearch(self : [T], compare : (implicit : (T, T) -> Order.Order), element : T) : { #found : Nat; #insertionIndex : Nat }` +- `public func compare(self : [T], other : [T], compare : (implicit : (T, T) -> Order.Order)) : Order.Order` +- `public func concat(self : [T], other : [T]) : [T]` +- `public func empty() : [T]` +- `public func enumerate(self : [T]) : Types.Iter<(Nat, T)>` +- `public func equal(self : [T], other : [T], equal : (implicit : (T, T) -> Bool)) : Bool` +- `public func filter(self : [T], f : T -> Bool) : [T]` +- `public func filterMap(self : [T], f : T -> ?R) : [R]` +- `public func find(self : [T], predicate : T -> Bool) : ?T` +- `public func findIndex(self : [T], predicate : T -> Bool) : ?Nat` +- `public func flatMap(self : [T], k : T -> Types.Iter) : [R]` +- `public func flatten(self : [[T]]) : [T]` +- `public func foldLeft(self : [T], base : A, combine : (A, T) -> A) : A` +- `public func foldRight(self : [T], base : A, combine : (T, A) -> A) : A` +- `public func forEach(self : [T], f : T -> ())` +- `public func fromIter(iter : Types.Iter) : [T]` +- `public func fromVarArray(varArray : [var T]) : [T]` +- `public func indexOf(self : [T], equal : (implicit : (T, T) -> Bool), element : T) : ?Nat` +- `public func isEmpty(self : [T]) : Bool` +- `public func isSorted(self : [T], compare : (implicit : (T, T) -> Order.Order)) : Bool` +- `public func join(self : Types.Iter<[T]>) : [T]` +- `public func keys(self : [T]) : Types.Iter` +- `public func lastIndexOf(self : [T], equal : (implicit : (T, T) -> Bool), element : T) : ?Nat` +- `public func map(self : [T], f : T -> R) : [R]` +- `public func mapEntries(self : [T], f : (T, Nat) -> R) : [R]` +- `public func nextIndexOf(self : [T], equal : (implicit : (T, T) -> Bool), element : T, fromInclusive : Nat) : ?Nat` +- `public func prevIndexOf(self : [T], equal : (implicit : (T, T) -> Bool), element : T, fromExclusive : Nat) : ?Nat` +- `public func range(self : [T], fromInclusive : Int, toExclusive : Int) : Types.Iter` +- `public func repeat(item : T, size : Nat) : [T]` +- `public func reverse(self : [T]) : [T]` +- `public func singleton(element : T) : [T]` +- `public func size(self : [T]) : Nat` +- `public func sliceToArray(self : [T], fromInclusive : Int, toExclusive : Int) : [T]` +- `public func sliceToVarArray(self : [T], fromInclusive : Int, toExclusive : Int) : [var T]` +- `public func sort(self : [T], compare : (implicit : (T, T) -> Order.Order)) : [T]` +- `public func toText(self : [T], f : (implicit : (toText : T -> Text))) : Text` +- `public func toVarArray(self : [T]) : [var T]` +- `public func values(self : [T]) : Types.Iter` +- `public let tabulate : (size : Nat, generator : Nat -> T) -> [T]` + +## Debug + +- `public func todo() : None` +- `public let print : (text : Text) -> ()` + +## Int + +- `public func add(x : Int, y : Int) : Int` +- `public func compare(x : Int, y : Int) : Order.Order` +- `public func div(x : Int, y : Int) : Int` +- `public func equal(x : Int, y : Int) : Bool` +- `public func fromNat(nat : Nat) : Int` +- `public func fromText(text : Text) : ?Int` +- `public func greater(x : Int, y : Int) : Bool` +- `public func greaterOrEqual(x : Int, y : Int) : Bool` +- `public func less(x : Int, y : Int) : Bool` +- `public func lessOrEqual(x : Int, y : Int) : Bool` +- `public func max(x : Int, y : Int) : Int` +- `public func min(x : Int, y : Int) : Int` +- `public func mul(x : Int, y : Int) : Int` +- `public func neg(x : Int) : Int` +- `public func notEqual(x : Int, y : Int) : Bool` +- `public func pow(x : Int, y : Int) : Int` +- `public func range(fromInclusive : Int, toExclusive : Int) : Iter.Iter` +- `public func rangeBy(fromInclusive : Int, toExclusive : Int, step : Int) : Iter.Iter` +- `public func rangeByInclusive(from : Int, to : Int, step : Int) : Iter.Iter` +- `public func rangeInclusive(from : Int, to : Int) : Iter.Iter` +- `public func rem(x : Int, y : Int) : Int` +- `public func sub(x : Int, y : Int) : Int` +- `public func toInt(self : Text) : ?Int` +- `public func toNat(self : Int) : Nat` +- `public func toText(self : Int) : Text` +- `public let abs : (x : Int) -> Nat` +- `public let fromInt16 : (x : Int16) -> Int` +- `public let fromInt32 : (x : Int32) -> Int` +- `public let fromInt64 : (x : Int64) -> Int` +- `public let fromInt8 : (x : Int8) -> Int` +- `public let toFloat : (self : Int) -> Float` +- `public let toInt16 : (self : Int) -> Int16` +- `public let toInt32 : (self : Int) -> Int32` +- `public let toInt64 : (self : Int) -> Int64` +- `public let toInt8 : (self : Int) -> Int8` +- `public type Int` + +## Iter + +- `public func all(self : Iter, f : T -> Bool) : Bool` +- `public func any(self : Iter, f : T -> Bool) : Bool` +- `public func concat(self : Iter, other : Iter) : Iter` +- `public func contains(self : Iter, equal : (implicit : (T, T) -> Bool), value : T) : Bool` +- `public func drop(self : Iter, n : Nat) : Iter` +- `public func dropWhile(self : Iter, f : T -> Bool) : Iter` +- `public func empty() : Iter` +- `public func enumerate(self : Iter) : Iter<(Nat, T)>` +- `public func filter(self : Iter, f : T -> Bool) : Iter` +- `public func filterMap(self : Iter, f : T -> ?R) : Iter` +- `public func find(self : Iter, f : T -> Bool) : ?T` +- `public func findIndex(self : Iter, predicate : T -> Bool) : ?Nat` +- `public func flatMap(self : Iter, f : T -> Iter) : Iter` +- `public func flatten(self : Iter>) : Iter` +- `public func foldLeft(self : Iter, initial : R, combine : (R, T) -> R) : R` +- `public func foldRight(self : Iter, initial : R, combine : (T, R) -> R) : R` +- `public func forEach( self : Iter, f : (T) -> () )` +- `public func fromArray(array : [T]) : Iter` +- `public func fromVarArray(array : [var T]) : Iter` +- `public func infinite(item : T) : Iter` +- `public func map(self : Iter, f : T -> R) : Iter` +- `public func max(self : Iter, compare : (implicit : (T, T) -> Order.Order)) : ?T` +- `public func min(self : Iter, compare : (implicit : (T, T) -> Order.Order)) : ?T` +- `public func reduce(self : Iter, combine : (T, T) -> T) : ?T` +- `public func repeat(item : T, count : Nat) : Iter` +- `public func reverse(self : Iter) : Iter` +- `public func scanLeft(self : Iter, initial : R, combine : (R, T) -> R) : Iter` +- `public func scanRight(self : Iter, initial : R, combine : (T, R) -> R) : Iter` +- `public func singleton(value : T) : Iter` +- `public func size(self : Iter) : Nat` +- `public func sort(self : Iter, compare : (implicit : (T, T) -> Order.Order)) : Iter` +- `public func step(self : Iter, n : Nat) : Iter` +- `public func take(self : Iter, n : Nat) : Iter` +- `public func takeWhile(self : Iter, f : T -> Bool) : Iter` +- `public func toArray(self : Iter) : [T]` +- `public func toVarArray(self : Iter) : [var T]` +- `public func unfold(initial : S, step : S -> ?(T, S)) : Iter` +- `public func zip3(self : Iter, other1 : Iter, other2 : Iter) : Iter<(A, B, C)>` +- `public func zip(self : Iter, other : Iter) : Iter<(A, B)>` +- `public func zipWith3(self : Iter, other1 : Iter, other2 : Iter, f : (A, B, C) -> R) : Iter` +- `public func zipWith(self : Iter, other : Iter, f : (A, B) -> R) : Iter` +- `public type Iter` + +## List + +- `public func add(self : List, element : T)` +- `public func addAll(self : List, iter : Types.Iter)` +- `public func addRepeat(self : List, initValue : T, count : Nat)` +- `public func all(self : List, predicate : T -> Bool) : Bool` +- `public func any(self : List, predicate : T -> Bool) : Bool` +- `public func append(self : List, added : List)` +- `public func at(self : List, index : Nat) : T` +- `public func binarySearch(self : List, compare : (implicit : (T, T) -> Types.Order), element : T) : { #found : Nat; #insertionIndex : Nat }` +- `public func clear(self : List)` +- `public func clone(self : List) : List` +- `public func compare(self : List, other : List, compare : (implicit : (T, T) -> Types.Order)) : Types.Order` +- `public func contains(self : List, equal : (implicit : (T, T) -> Bool), element : T) : Bool` +- `public func deduplicate(self : List, equal : (implicit : (T, T) -> Bool))` +- `public func empty() : List` +- `public func enumerate(self : List) : Types.Iter<(Nat, T)>` +- `public func equal(self : List, other : List, equal : (implicit : (T, T) -> Bool)) : Bool` +- `public func fill(self : List, value : T)` +- `public func filter(self : List, predicate : T -> Bool) : List` +- `public func filterMap(self : List, f : T -> ?R) : List` +- `public func find(self : List, predicate : T -> Bool) : ?T` +- `public func findIndex(self : List, predicate : T -> Bool) : ?Nat` +- `public func findLastIndex(self : List, predicate : T -> Bool) : ?Nat` +- `public func first(self : List) : ?T` +- `public func flatMap(self : List, k : T -> Types.Iter) : List` +- `public func flatten(self : List>) : List` +- `public func foldLeft(self : List, base : A, combine : (A, T) -> A) : A` +- `public func foldRight(self : List, base : A, combine : (T, A) -> A) : A` +- `public func forEach(self : List, f : T -> ())` +- `public func forEachEntry(self : List, f : (Nat, T) -> ())` +- `public func forEachInRange(self : List, f : T -> (), fromInclusive : Nat, toExclusive : Nat)` +- `public func fromArray(array : [T]) : List` +- `public func fromIter(iter : Types.Iter) : List` +- `public func fromVarArray(array : [var T]) : List` +- `public func indexOf(self : List, equal : (implicit : (T, T) -> Bool), element : T) : ?Nat` +- `public func isEmpty(self : List) : Bool` +- `public func isSorted(self : List, compare : (implicit : (T, T) -> Types.Order)) : Bool` +- `public func join(self : Types.Iter>) : List` +- `public func keys(self : List) : Types.Iter` +- `public func last(self : List) : ?T` +- `public func lastIndexOf(self : List, equal : (implicit : (T, T) -> Bool), element : T) : ?Nat` +- `public func map(self : List, f : T -> R) : List` +- `public func mapEntries(self : List, f : (T, Nat) -> R) : List` +- `public func mapInPlace(self : List, f : T -> T)` +- `public func mapResult(self : List, f : T -> Types.Result) : Types.Result, E>` +- `public func max(self : List, compare : (implicit : (T, T) -> Types.Order)) : ?T` +- `public func min(self : List, compare : (implicit : (T, T) -> Types.Order)) : ?T` +- `public func nextIndexOf(self : List, equal : (implicit : (T, T) -> Bool), element : T, fromInclusive : Nat) : ?Nat` +- `public func prevIndexOf(self : List, equal : (implicit : (T, T) -> Bool), element : T, fromExclusive : Nat) : ?Nat` +- `public func put(self : List, index : Nat, value : T)` +- `public func range(self : List, fromInclusive : Int, toExclusive : Int) : Types.Iter` +- `public func reader(self : List, start : Nat) : () -> T` +- `public func removeLast(self : List) : ?T` +- `public func repeat(initValue : T, size : Nat) : List` +- `public func reverse(self : List) : List` +- `public func reverseEnumerate(self : List) : Types.Iter<(Nat, T)>` +- `public func reverseForEach(self : List, f : T -> ())` +- `public func reverseForEachEntry(self : List, f : (Nat, T) -> ())` +- `public func reverseInPlace(self : List)` +- `public func reverseValues(self : List) : Types.Iter` +- `public func singleton(element : T) : List` +- `public func size(self : List) : Nat` +- `public func sliceToArray(self : List, fromInclusive : Int, toExclusive : Int) : [T]` +- `public func sliceToVarArray(self : List, fromInclusive : Int, toExclusive : Int) : [var T]` +- `public func sort(self : List, compare : (implicit : (T, T) -> Types.Order)) : List` +- `public func sortInPlace(self : List, compare : (implicit : (T, T) -> Types.Order))` +- `public func tabulate(size : Nat, generator : Nat -> T) : List` +- `public func toArray(self : List) : [T]` +- `public func toList(self : Types.Iter) : List` +- `public func toText(self : List, toText : (implicit : T -> Text)) : Text` +- `public func toVarArray(self : List) : [var T]` +- `public func truncate(self : List, newSize : Nat)` +- `public func values(self : List) : Types.Iter` +- `public type List` + +## Map + +- `public func add(self : Map, compare : (implicit : (K, K) -> Order.Order), key : K, value : V)` +- `public func all(self : Map, predicate : (K, V) -> Bool) : Bool` +- `public func any(self : Map, predicate : (K, V) -> Bool) : Bool` +- `public func clear(self : Map)` +- `public func clone(self : Map) : Map` +- `public func containsKey(self : Map, compare : (implicit : (K, K) -> Order.Order), key : K) : Bool` +- `public func empty() : Map` +- `public func entries(self : Map) : Types.Iter<(K, V)>` +- `public func entriesFrom(self : Map, compare : (implicit : (K, K) -> Order.Order), key : K) : Types.Iter<(K, V)>` +- `public func equal(self : Map, other : Map, compare : (implicit : (K, K) -> Types.Order), equal : (implicit : (V, V) -> Bool)) : Bool` +- `public func filter(self : Map, compare : (implicit : (K, K) -> Order.Order), criterion : (K, V) -> Bool) : Map` +- `public func filterMap(self : Map, compare : (implicit : (K, K) -> Order.Order), project : (K, V1) -> ?V2) : Map` +- `public func foldLeft(self : Map, base : A, combine : (A, K, V) -> A) : A` +- `public func foldRight(self : Map, base : A, combine : (K, V, A) -> A) : A` +- `public func forEach(self : Map, operation : (K, V) -> ())` +- `public func fromArray(array : [(K, V)], compare : (implicit : (K, K) -> Order.Order)) : Map` +- `public func fromIter(iter : Types.Iter<(K, V)>, compare : (implicit : (K, K) -> Order.Order)) : Map` +- `public func fromVarArray(array : [var (K, V)], compare : (implicit : (K, K) -> Order.Order)) : Map` +- `public func get(self : Map, compare : (implicit : (K, K) -> Order.Order), key : K) : ?V` +- `public func isEmpty(self : Map) : Bool` +- `public func keys(self : Map) : Types.Iter` +- `public func map(self : Map, project : (K, V1) -> V2) : Map` +- `public func maxEntry(self : Map) : ?(K, V)` +- `public func minEntry(self : Map) : ?(K, V)` +- `public func remove(self : Map, compare : (implicit : (K, K) -> Order.Order), key : K)` +- `public func reverseEntries(self : Map) : Types.Iter<(K, V)>` +- `public func reverseEntriesFrom(self : Map, compare : (implicit : (K, K) -> Order.Order), key : K) : Types.Iter<(K, V)>` +- `public func singleton(key : K, value : V) : Map` +- `public func size(self : Map) : Nat` +- `public func toArray(self : Map) : [(K, V)]` +- `public func toMap(self : Types.Iter<(K, V)>, compare : (implicit : (K, K) -> Order.Order)) : Map` +- `public func toText(self : Map, keyFormat : (implicit : (toText : K -> Text)), valueFormat : (implicit : (toText : V -> Text))) : Text` +- `public func toVarArray(self : Map) : [var (K, V)]` +- `public func values(self : Map) : Types.Iter` +- `public type Map` + +## Nat + +- `public func add(x : Nat, y : Nat) : Nat` +- `public func allValues() : Iter.Iter` +- `public func compare(x : Nat, y : Nat) : Order.Order` +- `public func div(x : Nat, y : Nat) : Nat` +- `public func equal(x : Nat, y : Nat) : Bool` +- `public func fromText(text : Text) : ?Nat` +- `public func greater(x : Nat, y : Nat) : Bool` +- `public func greaterOrEqual(x : Nat, y : Nat) : Bool` +- `public func less(x : Nat, y : Nat) : Bool` +- `public func lessOrEqual(x : Nat, y : Nat) : Bool` +- `public func max(x : Nat, y : Nat) : Nat` +- `public func min(x : Nat, y : Nat) : Nat` +- `public func mul(x : Nat, y : Nat) : Nat` +- `public func notEqual(x : Nat, y : Nat) : Bool` +- `public func pow(x : Nat, y : Nat) : Nat` +- `public func range(fromInclusive : Nat, toExclusive : Nat) : Iter.Iter` +- `public func rangeBy(fromInclusive : Nat, toExclusive : Nat, step : Int) : Iter.Iter` +- `public func rangeByInclusive(from : Nat, to : Nat, step : Int) : Iter.Iter` +- `public func rangeInclusive(from : Nat, to : Nat) : Iter.Iter` +- `public func rem(x : Nat, y : Nat) : Nat` +- `public func sub(x : Nat, y : Nat) : Nat` +- `public func toInt(self : Nat) : Int` +- `public let bitshiftLeft : (x : Nat, y : Nat32) -> Nat` +- `public let bitshiftRight : (x : Nat, y : Nat32) -> Nat` +- `public let fromNat16 : Nat16 -> Nat` +- `public let fromNat32 : Nat32 -> Nat` +- `public let fromNat64 : Nat64 -> Nat` +- `public let fromNat8 : Nat8 -> Nat` +- `public let toFloat : (self : Nat) -> Float` +- `public let toNat : (self : Text) -> ?Nat` +- `public let toNat16 : (self : Nat) -> Nat16` +- `public let toNat32 : (self : Nat) -> Nat32` +- `public let toNat64 : (self : Nat) -> Nat64` +- `public let toNat8 : (self : Nat) -> Nat8` +- `public let toText : (self : Nat) -> Text` +- `public type Nat` + +## Option + +- `public func apply(self : ?T, f : ?(T -> R)) : ?R` +- `public func chain(self : ?T, f : T -> ?R) : ?R` +- `public func compare(self : ?T, other : ?T, compare : (implicit : (T, T) -> Types.Order)) : Types.Order` +- `public func equal(self : ?T, other : ?T, eq : (implicit : (equal : (T, T) -> Bool))) : Bool` +- `public func flatten(self : ??T) : ?T` +- `public func forEach(self : ?T, f : T -> ())` +- `public func get(self : ?T, default : T) : T` +- `public func getMapped(self : ?T, f : T -> R, default : R) : R` +- `public func isNull(self : ?Any) : Bool` +- `public func isSome(self : ?Any) : Bool` +- `public func map(self : ?T, f : T -> R) : ?R` +- `public func some(self : T) : ?T` +- `public func toText(self : ?T, toText : (implicit : T -> Text)) : Text` +- `public func unwrap(self : ?T) : T` + +## Order + +- `public func allValues() : Types.Iter` +- `public func equal(self : Order, other : Order) : Bool` +- `public func isEqual(self : Order) : Bool` +- `public func isGreater(self : Order) : Bool` +- `public func isLess(self : Order) : Bool` +- `public type Order` + +## Principal + +- `public func anonymous() : Principal` +- `public func compare(self : Principal, other : Principal) : { #less; #equal; #greater }` +- `public func equal(self : Principal, other : Principal) : Bool` +- `public func fromText(t : Text) : Principal` +- `public func greater(self : Principal, other : Principal) : Bool` +- `public func greaterOrEqual(self : Principal, other : Principal) : Bool` +- `public func hash(self : Principal) : Types.Hash` +- `public func isAnonymous(self : Principal) : Bool` +- `public func isCanister(self : Principal) : Bool` +- `public func isController(self : Principal) : Bool` +- `public func isReserved(self : Principal) : Bool` +- `public func isSelfAuthenticating(self : Principal) : Bool` +- `public func less(self : Principal, other : Principal) : Bool` +- `public func lessOrEqual(self : Principal, other : Principal) : Bool` +- `public func notEqual(self : Principal, other : Principal) : Bool` +- `public func toLedgerAccount(self : Principal, subAccount : ?Blob) : Blob` +- `public func toText(self : Principal) : Text` +- `public let fromActor : (a : actor {}) -> Principal` +- `public let fromBlob : (self : Blob) -> Principal` +- `public let toBlob : (self : Principal) -> Blob` +- `public type Principal` + +## Queue + +- `public func all(self : Queue, predicate : T -> Bool) : Bool` +- `public func any(self : Queue, predicate : T -> Bool) : Bool` +- `public func clear(self : Queue)` +- `public func clone(self : Queue) : Queue` +- `public func compare(self : Queue, other : Queue, compare : (implicit : (T, T) -> Order.Order)) : Order.Order` +- `public func contains(self : Queue, equal : (implicit : (T, T) -> Bool), element : T) : Bool` +- `public func empty() : Queue` +- `public func equal(self : Queue, other : Queue, equal : (implicit : (T, T) -> Bool)) : Bool` +- `public func filter(self : Queue, criterion : T -> Bool) : Queue` +- `public func filterMap(self : Queue, project : T -> ?U) : Queue` +- `public func forEach(self : Queue, operation : T -> ())` +- `public func fromArray(array : [T]) : Queue` +- `public func fromIter(iter : Iter.Iter) : Queue` +- `public func fromVarArray(array : [var T]) : Queue` +- `public func isEmpty(self : Queue) : Bool` +- `public func map(self : Queue, project : T -> U) : Queue` +- `public func peekBack(self : Queue) : ?T` +- `public func peekFront(self : Queue) : ?T` +- `public func popBack(self : Queue) : ?T` +- `public func popFront(self : Queue) : ?T` +- `public func pushBack(self : Queue, element : T)` +- `public func pushFront(self : Queue, element : T)` +- `public func reverseValues(self : Queue) : Iter.Iter` +- `public func singleton(element : T) : Queue` +- `public func size(self : Queue) : Nat` +- `public func toArray(self : Queue) : [T]` +- `public func toQueue(self : Iter.Iter) : Queue` +- `public func toText(self : Queue, format : (implicit : (toText : T -> Text))) : Text` +- `public func toVarArray(self : Queue) : [var T]` +- `public func values(self : Queue) : Iter.Iter` +- `public type Queue` + +## Runtime + +- `public func trap(message : Text) : None` +- `public func envVar(name : Text) : ?Text` + +## Set + +- `public func add(self : Set, compare : (implicit : (T, T) -> Order.Order), element : T)` +- `public func addAll(self : Set, compare : (implicit : (T, T) -> Order.Order), iter : Types.Iter)` +- `public func all(self : Set, predicate : T -> Bool) : Bool` +- `public func any(self : Set, predicate : T -> Bool) : Bool` +- `public func clear(self : Set)` +- `public func clone(self : Set) : Set` +- `public func compare(self : Set, other : Set, compare : (implicit : (T, T) -> Order.Order)) : Order.Order` +- `public func contains(self : Set, compare : (implicit : (T, T) -> Order.Order), element : T) : Bool` +- `public func difference(self : Set, other : Set, compare : (implicit : (T, T) -> Order.Order)) : Set` +- `public func empty() : Set` +- `public func equal(self : Set, other : Set, compare : (implicit : (T, T) -> Types.Order)) : Bool` +- `public func filter(self : Set, compare : (implicit : (T, T) -> Order.Order), criterion : T -> Bool) : Set` +- `public func filterMap(self : Set, compare : (implicit : (T2, T2) -> Order.Order), project : T1 -> ?T2) : Set` +- `public func flatten(self : Set>, compare : (implicit : (T, T) -> Order.Order)) : Set` +- `public func foldLeft(self : Set, base : A, combine : (A, T) -> A) : A` +- `public func foldRight(self : Set, base : A, combine : (T, A) -> A) : A` +- `public func forEach(self : Set, operation : T -> ())` +- `public func fromArray(array : [T], compare : (implicit : (T, T) -> Order.Order)) : Set` +- `public func fromIter(iter : Types.Iter, compare : (implicit : (T, T) -> Order.Order)) : Set` +- `public func intersection(self : Set, other : Set, compare : (implicit : (T, T) -> Order.Order)) : Set` +- `public func isEmpty(self : Set) : Bool` +- `public func isSubset(self : Set, other : Set, compare : (implicit : (T, T) -> Order.Order)) : Bool` +- `public func join(setIterator : Types.Iter>, compare : (implicit : (T, T) -> Order.Order)) : Set` +- `public func map(self : Set, compare : (implicit : (T2, T2) -> Order.Order), project : T1 -> T2) : Set` +- `public func max(self : Set) : ?T` +- `public func min(self : Set) : ?T` +- `public func remove(self : Set, compare : (implicit : (T, T) -> Order.Order), element : T) : ()` +- `public func reverseValues(self : Set) : Types.Iter` +- `public func reverseValuesFrom(self : Set, compare : (implicit : (T, T) -> Order.Order), element : T) : Types.Iter` +- `public func singleton(element : T) : Set` +- `public func size(self : Set) : Nat` +- `public func toArray(self : Set) : [T]` +- `public func toSet(self : Types.Iter, compare : (implicit : (T, T) -> Order.Order)) : Set` +- `public func toText(self : Set, toText : (implicit : T -> Text)) : Text` +- `public func union(self : Set, other : Set, compare : (implicit : (T, T) -> Order.Order)) : Set` +- `public func values(self : Set) : Types.Iter` +- `public func valuesFrom(self : Set, compare : (implicit : (T, T) -> Order.Order), element : T) : Types.Iter` +- `public type Set` + +## Stack + +- `public func all(self : Stack, predicate : T -> Bool) : Bool` +- `public func any(self : Stack, predicate : T -> Bool) : Bool` +- `public func clear(self : Stack)` +- `public func clone(self : Stack) : Stack` +- `public func compare(self : Stack, other : Stack, compare : (implicit : (T, T) -> Order.Order)) : Order.Order` +- `public func contains(self : Stack, equal : (implicit : (T, T) -> Bool), element : T) : Bool` +- `public func empty() : Stack` +- `public func equal(self : Stack, other : Stack, equal : (implicit : (T, T) -> Bool)) : Bool` +- `public func filter(self : Stack, predicate : T -> Bool) : Stack` +- `public func filterMap(self : Stack, project : T -> ?U) : Stack` +- `public func find(self : Stack, predicate : T -> Bool) : ?T` +- `public func findIndex(self : Stack, predicate : T -> Bool) : ?Nat` +- `public func forEach(self : Stack, operation : T -> ())` +- `public func fromArray(array : [T]) : Stack` +- `public func fromIter(iter : Types.Iter) : Stack` +- `public func fromVarArray(array : [var T]) : Stack` +- `public func get(self : Stack, position : Nat) : ?T` +- `public func isEmpty(self : Stack) : Bool` +- `public func map(self : Stack, project : T -> U) : Stack` +- `public func peek(self : Stack) : ?T` +- `public func pop(self : Stack) : ?T` +- `public func push(self : Stack, value : T)` +- `public func reverse(self : Stack)` +- `public func reverseValues(self : Stack) : Iter.Iter` +- `public func singleton(element : T) : Stack` +- `public func size(self : Stack) : Nat` +- `public func tabulate(size : Nat, generator : Nat -> T) : Stack` +- `public func toArray(self : Stack) : [T]` +- `public func toStack(self : Types.Iter) : Stack` +- `public func toText(self : Stack, format : (implicit : (toText : T -> Text))) : Text` +- `public func toVarArray(self : Stack) : [var T]` +- `public func values(self : Stack) : Iter.Iter` +- `public type Stack` + +## Text + +- `public func compare(self : Text, other : Text) : Order.Order` +- `public func compareWith(self : Text, other : Text, compare : (Char, Char) -> Order.Order) : Order.Order` +- `public func concat(self : Text, other : Text) : Text` +- `public func contains(self : Text, p : Pattern) : Bool` +- `public func endsWith(self : Text, p : Pattern) : Bool` +- `public func equal(self : Text, other : Text) : Bool` +- `public func flatMap(self : Text, f : Char -> Text) : Text` +- `public func foldLeft(self : Text, base : A, combine : (A, Char) -> A) : A` +- `public func fromArray(a : [Char]) : Text` +- `public func fromIter(cs : Iter.Iter) : Text` +- `public func fromVarArray(a : [var Char]) : Text` +- `public func greater(self : Text, other : Text) : Bool` +- `public func greaterOrEqual(self : Text, other : Text) : Bool` +- `public func isEmpty(self : Text) : Bool` +- `public func join(self : Iter.Iter, sep : Text) : Text` +- `public func less(self : Text, other : Text) : Bool` +- `public func lessOrEqual(self : Text, other : Text) : Bool` +- `public func map(self : Text, f : Char -> Char) : Text` +- `public func notEqual(self : Text, other : Text) : Bool` +- `public func replace(self : Text, p : Pattern, r : Text) : Text` +- `public func reverse(self : Text) : Text` +- `public func size(self : Text) : Nat` +- `public func split(self : Text, p : Pattern) : Iter.Iter` +- `public func startsWith(self : Text, p : Pattern) : Bool` +- `public func stripEnd(self : Text, p : Pattern) : ?Text` +- `public func stripStart(self : Text, p : Pattern) : ?Text` +- `public func toArray(self : Text) : [Char]` +- `public func toIter(self : Text) : Iter.Iter` +- `public func toText(self : Text) : Text` +- `public func toVarArray(self : Text) : [var Char]` +- `public func tokens(self : Text, p : Pattern) : Iter.Iter` +- `public func trim(self : Text, p : Pattern) : Text` +- `public func trimEnd(self : Text, p : Pattern) : Text` +- `public func trimStart(self : Text, p : Pattern) : Text` +- `public let decodeUtf8 : (self : Blob) -> ?Text` +- `public let encodeUtf8 : (self : Text) -> Blob` +- `public let fromChar : (c : Char) -> Text` +- `public let toLower : (self : Text) -> Text` +- `public let toUpper : (self : Text) -> Text` +- `public type Pattern` +- `public type Text` + +## Time + +- `public func now() : Time` +- `public func toNanoseconds(duration : Duration) : Nat` +- `public type Duration` +- `public type Time` +- `public type TimerId` + +Note: `Time` is `Int` (nanoseconds). Use `Int.compare` to compare timestamps — there is no `Time.compare`. diff --git a/skills/motoko/references/control-flow.md b/skills/motoko/references/control-flow.md new file mode 100644 index 0000000..a0a4386 --- /dev/null +++ b/skills/motoko/references/control-flow.md @@ -0,0 +1,84 @@ +# Control Flow + +Reference for Motoko control flow patterns. + +## Switch Statements + +```motoko +// Option unwrapping — trap on unexpected null +let value = switch (map.get(key)) { + case (?v) { v }; + case (null) { Runtime.trap("Key not found") }; +}; + +// Variant matching +type Status = { #active; #inactive; #pending : Text }; +switch (status) { + case (#active) { "User is active" }; + case (#inactive) { "User is inactive" }; + case (#pending(reason)) { "Pending: " # reason }; +}; + +// Value matching +switch (statusCode) { + case (200) { "OK" }; + case (404) { "Not Found" }; + case _ { "Unknown" }; +}; +``` + +## For Loops + +```motoko +// Iterate Map entries +for ((key, value) in map.entries()) { + // use key and value +}; + +// Iterate List +for (item in list.values()) { + // use item +}; + +// Iterate Array +for (score in scores.values()) { + total += score; +}; +// Most of the time you can use .foldLeft() or .map() instead. +``` + +## Break and Continue + +Unlabeled `break` and `continue` work in `for`, `while`, and `loop` since moc 1.2.0: + +```motoko +for (x in items.vals()) { + if (x == 0) continue; + if (x > 100) break; + process(x); +}; +``` + +Use labeled loops when you need to exit an outer loop from an inner one: + +```motoko +label outer for (x in items.vals()) { + label inner for (y in other.vals()) { + if (y == 0) continue inner; + if (x == y) break outer; + }; +}; +``` + +Labeled blocks also work for early exit in non-loop contexts: + +```motoko +label search { + for (item in items.vals()) { + if (item.id == targetId) { + result := ?item; + break search; + }; + }; +}; +``` diff --git a/skills/motoko/references/examples.md b/skills/motoko/references/examples.md new file mode 100644 index 0000000..e64bc77 --- /dev/null +++ b/skills/motoko/references/examples.md @@ -0,0 +1,427 @@ +# Motoko Examples + +Complete working examples demonstrating modern Motoko patterns. Assumes `--default-persistent-actors` in mops.toml (see skill prerequisites) — examples use plain `actor {}`. + +## Principled Multi-File Architecture + +### types.mo + +```motoko +module { + public type UserId = Principal; + + public type User = { + id : UserId; + var username : Text; + var bio : Text; + var isActive : Bool; + }; + + public type UserPublic = { + id : UserId; + username : Text; + bio : Text; + isActive : Bool; + }; + + public type Post = { + id : Nat; + author : User; + var title : Text; + var content : Text; + var published : Bool; + }; + + public type PostPublic = { + id : Nat; + authorId : Principal; + title : Text; + content : Text; + published : Bool; + }; +}; +``` + +### lib/User.mo + +```motoko +import Types "../types"; + +module { + public type User = Types.User; + + public func new(id : Types.UserId, username : Text) : User { + { id; var username; var bio = ""; var isActive = true }; + }; + + public func updateBio(self : User, newBio : Text) { + if (newBio.size() > 280) return; + self.bio := newBio; + }; + + public func ban(self : User) { self.isActive := false }; + + public func isValid(self : User) : Bool { + self.username.size() > 0 and self.isActive; + }; + + public func toPublic(self : User) : Types.UserPublic { + { id = self.id; username = self.username; bio = self.bio; isActive = self.isActive }; + }; +}; +``` + +### lib/Post.mo + +```motoko +import Types "../types"; + +module { + public type Post = Types.Post; + + public func new(id : Nat, author : Types.User, title : Text) : Post { + { id; author; var title; var content = ""; var published = false }; + }; + + public func publish(self : Post) { + if (self.content.size() > 0) { self.published := true }; + }; + + public func setContent(self : Post, content : Text) { + self.content := content; + }; +}; +``` + +### mixins/Auth.mo + +```motoko +import Types "../types"; +import UserLib "../lib/User"; +import List "mo:core/List"; + +mixin (users : List.List) { + + func findUser(p : Principal) : ?Types.User { + users.find(func(u) { u.id == p }); + }; + + public shared ({ caller }) func register(username : Text) : async Bool { + switch (findUser(caller)) { + case (?_) return false; + case (null) { + users.add(UserLib.new(caller, username)); + return true; + }; + }; + }; + + public shared query ({ caller }) func getProfile() : async ?Types.UserPublic { + switch (findUser(caller)) { + case (?user) { ?user.toPublic() }; + case (null) { null }; + }; + }; + + public shared ({ caller }) func updateBio(newBio : Text) : async Bool { + switch (findUser(caller)) { + case (null) false; + case (?user) { user.updateBio(newBio); true }; + }; + }; +}; +``` + +### mixins/Blog.mo + +```motoko +import Types "../types"; +import PostLib "../lib/Post"; +import List "mo:core/List"; +import Runtime "mo:core/Runtime"; + +mixin ( + users : List.List, + posts : List.List, +) { + + public shared ({ caller }) func createPost(title : Text) : async Nat { + let author = switch (users.find(func(u) { u.id == caller })) { + case (?u) u; + case (null) { Runtime.trap("User not registered") }; + }; + let pid = posts.size(); + posts.add(PostLib.new(pid, author, title)); + pid; + }; + + public shared ({ caller }) func publishPost(postId : Nat) : async Bool { + switch (posts.find(func(p) { p.id == postId })) { + case (null) false; + case (?post) { + if (post.author.id != caller) { return false }; + post.publish(); + true; + }; + }; + }; + + public query func getAllPosts() : async [Types.PostPublic] { + posts.map( + func(p) { { id = p.id; authorId = p.author.id; title = p.title; content = p.content; published = p.published } } + ).toArray(); + }; +}; +``` + +### main.mo + +```motoko +import List "mo:core/List"; +import Types "types"; +import AuthMixin "mixins/Auth"; +import BlogMixin "mixins/Blog"; + +actor { + let users = List.empty(); + let posts = List.empty(); + + include AuthMixin(users); + include BlogMixin(users, posts); +}; +``` + +## Iterator Chaining + +`import Array` enables `.find()`, `.any()`, `.all()` on arrays; `import Iter` enables `.map()`, `.filter()` on iterators; `import Bool` enables `.toText()` on booleans. + +```motoko +import Array "mo:core/Array"; +import Bool "mo:core/Bool"; +import Iter "mo:core/Iter"; +import Nat "mo:core/Nat"; + +actor { + let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + + public query func demonstrateIterators() : async Text { + var output = ""; + + let doubled = numbers.values().map(func x = x * 2).filter(func x = x > 10).toArray(); + output := output # "Doubled > 10: " # doubled.toText() # "\n"; + + let sum = numbers.values().foldLeft(0, func(acc, x) = acc + x); + output := output # "Sum: " # sum.toText() # "\n"; + + switch (numbers.find(func x = x > 5)) { + case (?found) { output := output # "Found: " # found.toText() # "\n" }; + case (null) {}; + }; + + let hasLarge = numbers.any(func x = x > 8); + let allPositive = numbers.all(func x = x > 0); + output := output # "Has large: " # hasLarge.toText() # "\n"; + output := output # "All positive: " # allPositive.toText() # "\n"; + + output; + }; +}; +``` + +## Map with Custom Key Types + +```motoko +import Map "mo:core/Map"; +import Order "mo:core/Order"; +import Int "mo:core/Int"; + +actor { + type Point = { x : Int; y : Int }; + + module Point { + public func compare(a : Point, b : Point) : Order.Order { + switch (Int.compare(a.x, b.x)) { + case (#equal) { Int.compare(a.y, b.y) }; + case (other) { other }; + }; + }; + }; + + let pointMap = Map.empty(); + + public func addPoint(x : Int, y : Int, pointLabel : Text) : async () { + pointMap.add({ x; y }, pointLabel); + }; + + public query func getPoint(x : Int, y : Int) : async ?Text { + pointMap.get({ x; y }); + }; +}; +``` + +## Shared Type Boundary + +Internal types with mutable fields and stable collections must be converted to shared types at the API boundary: + +```motoko +import List "mo:core/List"; +import Principal "mo:core/Principal"; +import Set "mo:core/Set"; +import Time "mo:core/Time"; + +actor { + type PhotoInternal = { + id : Nat; + url : Text; + uploadedBy : Principal; + likedBy : Set.Set; + createdAt : Int; + }; + + type Photo = { + id : Nat; + url : Text; + uploadedBy : Text; + likedBy : [Principal]; + createdAt : Int; + }; + + let photos = List.empty(); + + func toPublic(self : PhotoInternal) : Photo { + { + self with + uploadedBy = self.uploadedBy.toText(); + likedBy = Set.toArray(self.likedBy); + }; + }; + + public shared ({ caller }) func upload(url : Text) : async Nat { + let id = photos.size(); + photos.add({ + id; url; + uploadedBy = caller; + likedBy = Set.empty(); + createdAt = Time.now(); + }); + id; + }; + + public query func getPhotos() : async [Photo] { + photos.map(func(p) { toPublic(p) }).toArray(); + }; +}; +``` + +## In-Place Mutation Patterns + +Use `find` + direct field mutation for `var`-field records. Use `mapInPlace` for immutable record replacement: + +```motoko +import List "mo:core/List"; + +actor { + // var fields — mutate directly via find + type Session = { id : Nat; var active : Bool }; + let sessions = List.empty(); + + public func deactivate(targetId : Nat) : async Bool { + switch (sessions.find(func(s) { s.id == targetId })) { + case (?session) { session.active := false; true }; + case (null) false; + }; + }; + + // immutable records — replace via mapInPlace + record spread + type Todo = { id : Nat; text : Text; completed : Bool }; + let todos = List.empty(); + var nextId : Nat = 0; + + public func addTodo(text : Text) : async Nat { + let id = nextId; + nextId += 1; + todos.add({ id; text; completed = false }); + id; + }; + + public func completeTodo(targetId : Nat) : async Bool { + var found = false; + todos.mapInPlace(func(t) { + if (t.id == targetId) { found := true; { t with completed = true } } else { t } + }); + found; + }; +}; +``` + +## Timer with Periodic Cleanup + +Timer IDs should be `transient var` — timers don't survive upgrades and must be re-registered: + +```motoko +import Timer "mo:core/Timer"; +import Time "mo:core/Time"; +import List "mo:core/List"; + +actor { + let logs = List.empty<(Int, Text)>(); + transient var timerId : Nat = 0; // resets on upgrade — timer must be restarted + + public func startCleanup() : async () { + timerId := Timer.recurringTimer( + #seconds(3600), + func() : async () { + let oneHourAgo = Time.now() - 3_600_000_000_000; + let recent = logs.filter(func(timestamp, _) { timestamp > oneHourAgo }); + logs.clear(); + logs.addAll(recent.values()); + }, + ); + }; + + public func stopCleanup() : async () { + Timer.cancelTimer(timerId); + }; + + public query func getLogs() : async [(Int, Text)] { + logs.toArray(); + }; +}; +``` + +## Type Conversions + +Requires `import Nat "mo:core/Nat"`, `import Int "mo:core/Int"`, etc. for dot-notation methods. + +```motoko +// Nat ↔ Int +let n : Nat = 42; +let i : Int = n.toInt(); +let backToNat = Int.abs(i); + +// Nat size widening: Nat8 → Nat16 → Nat32 → Nat64 +let nat8 : Nat8 = 255; +let nat16 = nat8.toNat16(); +let nat32 = nat16.toNat32(); +let nat64 = nat32.toNat64(); +let backToNat8 = Nat8.fromNat64(nat64); + +// Int size widening: Int8 → Int16 → Int32 → Int64 +let int8 : Int8 = -128; +let int16 = int8.toInt16(); +let int32 = int16.toInt32(); +let int64 = int32.toInt64(); +let backToInt8 = Int8.fromInt64(int64); + +// To/from Text +let text = n.toText(); // "42" +let maybeNat = Nat.fromText("42"); // : ?Nat +let maybeInt = Int.fromText("-5"); // : ?Int + +// To Float +let f = n.toFloat(); + +// Time is Int (nanoseconds) +let timestamp = Time.now(); // requires import Time "mo:core/Time" +let milliseconds = timestamp / 1_000_000; +``` diff --git a/skills/motoko/references/type-conversions.md b/skills/motoko/references/type-conversions.md new file mode 100644 index 0000000..8f31423 --- /dev/null +++ b/skills/motoko/references/type-conversions.md @@ -0,0 +1,59 @@ +# Type Conversions + +Reference for Motoko numerical type conversions. + +## Nat to Int + +```motoko +let natValue = 42; +let intValue = natValue.toInt(); +let backToNat = Int.abs(intValue); // only if non-negative +``` + +## Nat Size Conversions + +```motoko +let nat8 : Nat8 = 255; +let nat16 = nat8.toNat16(); +let nat32 = nat16.toNat32(); +let nat64 = nat32.toNat64(); +let backToNat8 = Nat8.fromNat64(nat64); // reverse +``` + +Conversion chain: `Nat8 → Nat16 → Nat32 → Nat64` (widen) or reverse with `fromNatXX` (narrow). + +## Int Size Conversions + +```motoko +let int8 : Int8 = -128; +let int16 = int8.toInt16(); +let int32 = int16.toInt32(); +let int64 = int32.toInt64(); +let backToInt8 = Int8.fromInt64(int64); // reverse +``` + +Conversion chain: `Int8 → Int16 → Int32 → Int64` (widen) or reverse with `fromIntXX` (narrow). + +## Common Conversion Patterns + +```motoko +// Nat to Text +let text = myNat.toText(); + +// Int to Text +let text = myInt.toText(); + +// Text to Nat/Int (returns optional) +let maybeNat = Nat.fromText("42"); // ?Nat +let maybeInt = Int.fromText("-5"); // ?Int + +// Nat to Float +let f = myNat.toFloat(); + +// Int to Float +let f = myInt.toFloat(); + +// Time is Int — use Int conversions +let timestamp = Time.now(); // Int (nanoseconds) +let milliseconds = timestamp / 1_000_000; +``` From e116202bb5aa467266da3ccf3758405a3a2fb523 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Tue, 5 May 2026 00:55:22 +0200 Subject: [PATCH 02/11] docs: document icskills-owned sections and upstream sync strategy Add Sections owned by icskills to upstream comment blocks for migrating-motoko, migrating-motoko-enhanced, and mops-cli, listing which content should not be overwritten on upstream syncs. Expand CLAUDE.md sync strategy with a table of change types (cross-references, bug fixes, IC-specific additions vs shared content), and document the planned automated upstream release detection workflow adapted from dfinity/developer-docs. --- .claude/CLAUDE.md | 18 +++++++++++++++++- skills/migrating-motoko-enhanced/SKILL.md | 5 ++++- skills/migrating-motoko/SKILL.md | 4 +++- skills/mops-cli/SKILL.md | 4 +++- 4 files changed, 27 insertions(+), 4 deletions(-) diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 902e44e..d9d942a 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -119,7 +119,23 @@ diff /tmp/upstream.md skills//SKILL.md | `migrating-motoko-enhanced` | [caffeinelabs/motoko](https://github.com/caffeinelabs/motoko) | 1.7.0 | `1e65e26346b35927869dda044bb76763627c2c57` | | `mops-cli` | [caffeinelabs/mops](https://github.com/caffeinelabs/mops) | cli-v2.13.1 | `c947a79fc68d2d4d5b0d3bad10e23370b8134364` | -When a new version of an upstream repo is released: (1) get the new commit SHA, (2) diff the upstream skill file against what we have, (3) apply non-conflicting improvements, (4) update the upstream comment with the new tag and SHA, (5) update the table above. +When a new version of an upstream repo is released: (1) get the new commit SHA, (2) diff the upstream skill file against what we have, (3) apply non-conflicting improvements **except for sections listed as "owned by icskills"**, (4) update the upstream comment with the new tag and SHA, (5) update the table above. + +### What icskills changes vs upstream + +| Change type | Examples | Rule | +|-------------|---------|------| +| **Cross-reference links** | `Load motoko skill`, `Load mops-cli skill` | Always rewrite to icskills skill names; never overwrite from upstream | +| **Bug fixes to upstream** | Removed `args = ["--enhanced-migration=..."]` from migrating-motoko-enhanced | Keep our fix; consider contributing upstream | +| **icp-cli / IC-specific additions** | Runtime.envVar, icp-cli deployment notes, M0141/M0145 pitfalls | icskills-owned; do not overwrite | +| **Style additions** | transient var section, variant tag examples | icskills-owned if not in upstream; otherwise align | +| **Content shared with upstream** | All patterns, syntax, error tables | Sync from upstream when unchanged | + +### Automated upstream release detection + +A GitHub Actions workflow (`.github/workflows/sync-upstream.yml`) runs weekly to detect new releases of tracked upstream repos. When a new release is found it opens a PR with the raw upstream diff, so maintainers can review and cherry-pick improvements manually. The workflow does NOT auto-apply changes — it only surfaces the diff. + +This is adapted from [dfinity/developer-docs sync-motoko.yml](https://github.com/dfinity/developer-docs/blob/main/.github/workflows/sync-motoko.yml), simplified for a curl-based approach (no submodules). ## Writing Guidelines diff --git a/skills/migrating-motoko-enhanced/SKILL.md b/skills/migrating-motoko-enhanced/SKILL.md index 4f1f15a..cf2a36c 100644 --- a/skills/migrating-motoko-enhanced/SKILL.md +++ b/skills/migrating-motoko-enhanced/SKILL.md @@ -11,7 +11,10 @@ metadata: + Last synced: 2026-05-04 + Sections owned by icskills (do not overwrite from upstream): + mops.toml Setup (removed redundant --enhanced-migration from [canisters.backend].args — upstream had a bug), + Additional References (uses icskills skill names: motoko, migrating-motoko, mops-cli) --> # Enhanced Multi-Migration diff --git a/skills/migrating-motoko/SKILL.md b/skills/migrating-motoko/SKILL.md index 240e853..2d630a6 100644 --- a/skills/migrating-motoko/SKILL.md +++ b/skills/migrating-motoko/SKILL.md @@ -11,7 +11,9 @@ metadata: + Last synced: 2026-05-04 + Sections owned by icskills (do not overwrite from upstream): + Additional References (uses icskills skill names: motoko, mops-cli) --> # Inline Actor Migration diff --git a/skills/mops-cli/SKILL.md b/skills/mops-cli/SKILL.md index bb487b2..5b94a9a 100644 --- a/skills/mops-cli/SKILL.md +++ b/skills/mops-cli/SKILL.md @@ -11,7 +11,9 @@ metadata: + Last synced: 2026-05-04 + Sections owned by icskills (do not overwrite from upstream): + Additional References (uses icskills skill names: motoko, migrating-motoko, migrating-motoko-enhanced) --> # Mops CLI From b998c502dfa093e3a795e41cde0ed8117237dcaa Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Tue, 5 May 2026 01:00:36 +0200 Subject: [PATCH 03/11] fix: add Motoko to known categories in validator and site category order --- scripts/check-project.js | 1 + src/lib/skills.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/scripts/check-project.js b/scripts/check-project.js index 250d4eb..f9ba37f 100644 --- a/scripts/check-project.js +++ b/scripts/check-project.js @@ -16,6 +16,7 @@ const KNOWN_CATEGORIES = [ "Governance", "Infrastructure", "Integration", + "Motoko", "Security", ]; diff --git a/src/lib/skills.ts b/src/lib/skills.ts index f933a36..cf6a60b 100644 --- a/src/lib/skills.ts +++ b/src/lib/skills.ts @@ -23,6 +23,7 @@ const CATEGORY_ORDER = [ 'Governance', 'Infrastructure', 'Integration', + 'Motoko', 'Security', 'Tokens', 'Wallet', From 5e46e6bcbdc8a1f9bae223b2fd5ae76f769466f0 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Tue, 5 May 2026 01:16:29 +0200 Subject: [PATCH 04/11] chore: add weekly upstream sync workflow and update CLAUDE.md Adds .github/workflows/sync-upstream.yml that detects new releases of caffeinelabs/motoko and caffeinelabs/mops, opens a diff PR for manual review. Workflow does not auto-apply changes. Updates CLAUDE.md to reference the actual workflow file and document icskills-owned vs upstream content by change type. --- .claude/CLAUDE.md | 2 +- .github/workflows/sync-upstream.yml | 249 ++++++++++++++++++++++++++++ 2 files changed, 250 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/sync-upstream.yml diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index d9d942a..4c1c728 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -133,7 +133,7 @@ When a new version of an upstream repo is released: (1) get the new commit SHA, ### Automated upstream release detection -A GitHub Actions workflow (`.github/workflows/sync-upstream.yml`) runs weekly to detect new releases of tracked upstream repos. When a new release is found it opens a PR with the raw upstream diff, so maintainers can review and cherry-pick improvements manually. The workflow does NOT auto-apply changes — it only surfaces the diff. +`.github/workflows/sync-upstream.yml` runs weekly to detect new releases of tracked upstream repos. When a new release is found it opens a PR with the raw upstream diff, so maintainers can review and cherry-pick improvements manually. The workflow does NOT auto-apply changes — it only surfaces the diff. This is adapted from [dfinity/developer-docs sync-motoko.yml](https://github.com/dfinity/developer-docs/blob/main/.github/workflows/sync-motoko.yml), simplified for a curl-based approach (no submodules). diff --git a/.github/workflows/sync-upstream.yml b/.github/workflows/sync-upstream.yml new file mode 100644 index 0000000..ea24cd6 --- /dev/null +++ b/.github/workflows/sync-upstream.yml @@ -0,0 +1,249 @@ +name: Upstream sync check + +on: + schedule: + - cron: '0 8 * * 1' # Weekly on Monday + workflow_dispatch: + +jobs: + check-motoko: + name: Check caffeinelabs/motoko + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + + steps: + - uses: actions/checkout@v4 + + - name: Get latest motoko release tag + id: latest + run: | + TAG=$(gh release view --repo caffeinelabs/motoko --json tagName -q .tagName) + echo "tag=$TAG" >> $GITHUB_OUTPUT + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Get current pinned tag + id: current + run: | + TAG=$(grep 'Tag:' skills/motoko/SKILL.md | head -1 | sed 's/.*Tag: \([^ ]*\).*/\1/') + echo "tag=$TAG" >> $GITHUB_OUTPUT + + - name: Check if update needed + id: check + run: | + LATEST="${{ steps.latest.outputs.tag }}" + CURRENT="${{ steps.current.outputs.tag }}" + BRANCH="infra/sync-motoko-${LATEST}" + if [ "$LATEST" = "$CURRENT" ]; then + echo "needed=false" >> $GITHUB_OUTPUT + echo "Already at latest: $CURRENT" + elif git ls-remote --exit-code origin "refs/heads/${BRANCH}" > /dev/null 2>&1; then + echo "needed=false" >> $GITHUB_OUTPUT + echo "Branch $BRANCH already exists — PR likely open, skipping" + else + echo "needed=true" >> $GITHUB_OUTPUT + echo "branch=$BRANCH" >> $GITHUB_OUTPUT + echo "New release: $LATEST (current: $CURRENT)" + fi + + - name: Resolve commit SHA for new release + if: steps.check.outputs.needed == 'true' + id: sha + run: | + TAG="${{ steps.latest.outputs.tag }}" + RESULT=$(curl -sf "https://api.github.com/repos/caffeinelabs/motoko/git/ref/tags/${TAG}" \ + -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" | \ + python3 -c "import sys,json; d=json.load(sys.stdin); print(d['object']['sha'], d['object']['type'])") + OBJ_SHA=$(echo "$RESULT" | awk '{print $1}') + OBJ_TYPE=$(echo "$RESULT" | awk '{print $2}') + if [ "$OBJ_TYPE" = "tag" ]; then + COMMIT=$(curl -sf "https://api.github.com/repos/caffeinelabs/motoko/git/tags/${OBJ_SHA}" \ + -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" | \ + python3 -c "import sys,json; print(json.load(sys.stdin)['object']['sha'])") + else + COMMIT="$OBJ_SHA" + fi + echo "commit=$COMMIT" >> $GITHUB_OUTPUT + + - name: Fetch upstream files and build diff + if: steps.check.outputs.needed == 'true' + run: | + SHA="${{ steps.sha.outputs.commit }}" + CURRENT="${{ steps.current.outputs.tag }}" + LATEST="${{ steps.latest.outputs.tag }}" + + declare -A UPSTREAM_TO_LOCAL=( + ["writing-motoko"]="motoko" + ["migrating-motoko"]="migrating-motoko" + ["migrating-motoko-enhanced"]="migrating-motoko-enhanced" + ) + + { + echo "## Upstream diff: caffeinelabs/motoko \`${CURRENT}\` → \`${LATEST}\`" + echo "" + echo "Commit: [\`${SHA:0:12}\`](https://github.com/caffeinelabs/motoko/commit/${SHA})" + echo "" + echo "**Review instructions:** check which sections are listed as \`owned by icskills\` in each" + echo "skill's upstream comment block before applying changes. Do NOT overwrite those sections." + echo "" + } > /tmp/pr-body.md + + HAS_CHANGES=false + for upstream_name in "writing-motoko" "migrating-motoko" "migrating-motoko-enhanced"; do + local_name="${UPSTREAM_TO_LOCAL[$upstream_name]}" + curl -sf "https://raw.githubusercontent.com/caffeinelabs/motoko/${SHA}/.agents/skills/${upstream_name}/SKILL.md" \ + > /tmp/upstream-${upstream_name}.md 2>/dev/null || { + echo "(skill not found at this path)" > /tmp/upstream-${upstream_name}.md + } + DIFF=$(diff /tmp/upstream-${upstream_name}.md skills/${local_name}/SKILL.md || true) + if [ -n "$DIFF" ]; then + HAS_CHANGES=true + { + echo "### \`${local_name}\` ← upstream \`${upstream_name}\`" + echo "" + echo '
Show diff' + echo "" + echo '```diff' + echo "$DIFF" + echo '```' + echo "" + echo '
' + echo "" + } >> /tmp/pr-body.md + else + echo "### \`${local_name}\` — no changes" >> /tmp/pr-body.md + echo "" >> /tmp/pr-body.md + fi + done + + echo "has_changes=$HAS_CHANGES" >> $GITHUB_ENV + + - name: Create sync PR + if: steps.check.outputs.needed == 'true' + run: | + BRANCH="${{ steps.check.outputs.branch }}" + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git checkout -b "$BRANCH" + git commit --allow-empty -m "chore: upstream sync check — caffeinelabs/motoko ${{ steps.latest.outputs.tag }}" + git push -u origin "$BRANCH" + gh pr create \ + --title "chore: sync check — caffeinelabs/motoko ${{ steps.latest.outputs.tag }}" \ + --body-file /tmp/pr-body.md + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + check-mops: + name: Check caffeinelabs/mops + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + + steps: + - uses: actions/checkout@v4 + + - name: Get latest mops release tag + id: latest + run: | + TAG=$(gh release view --repo caffeinelabs/mops --json tagName -q .tagName) + echo "tag=$TAG" >> $GITHUB_OUTPUT + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Get current pinned tag + id: current + run: | + TAG=$(grep 'Tag:' skills/mops-cli/SKILL.md | head -1 | sed 's/.*Tag: \([^ ]*\).*/\1/') + echo "tag=$TAG" >> $GITHUB_OUTPUT + + - name: Check if update needed + id: check + run: | + LATEST="${{ steps.latest.outputs.tag }}" + CURRENT="${{ steps.current.outputs.tag }}" + BRANCH="infra/sync-mops-${LATEST}" + if [ "$LATEST" = "$CURRENT" ]; then + echo "needed=false" >> $GITHUB_OUTPUT + echo "Already at latest: $CURRENT" + elif git ls-remote --exit-code origin "refs/heads/${BRANCH}" > /dev/null 2>&1; then + echo "needed=false" >> $GITHUB_OUTPUT + echo "Branch $BRANCH already exists — PR likely open, skipping" + else + echo "needed=true" >> $GITHUB_OUTPUT + echo "branch=$BRANCH" >> $GITHUB_OUTPUT + echo "New release: $LATEST (current: $CURRENT)" + fi + + - name: Resolve commit SHA for new release + if: steps.check.outputs.needed == 'true' + id: sha + run: | + TAG="${{ steps.latest.outputs.tag }}" + RESULT=$(curl -sf "https://api.github.com/repos/caffeinelabs/mops/git/ref/tags/${TAG}" \ + -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" | \ + python3 -c "import sys,json; d=json.load(sys.stdin); print(d['object']['sha'], d['object']['type'])") + OBJ_SHA=$(echo "$RESULT" | awk '{print $1}') + OBJ_TYPE=$(echo "$RESULT" | awk '{print $2}') + if [ "$OBJ_TYPE" = "tag" ]; then + COMMIT=$(curl -sf "https://api.github.com/repos/caffeinelabs/mops/git/tags/${OBJ_SHA}" \ + -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" | \ + python3 -c "import sys,json; print(json.load(sys.stdin)['object']['sha'])") + else + COMMIT="$OBJ_SHA" + fi + echo "commit=$COMMIT" >> $GITHUB_OUTPUT + + - name: Fetch upstream file and build diff + if: steps.check.outputs.needed == 'true' + run: | + SHA="${{ steps.sha.outputs.commit }}" + CURRENT="${{ steps.current.outputs.tag }}" + LATEST="${{ steps.latest.outputs.tag }}" + + curl -sf "https://raw.githubusercontent.com/caffeinelabs/mops/${SHA}/.agents/skills/mops-cli/SKILL.md" \ + > /tmp/upstream-mops-cli.md 2>/dev/null || { + echo "(skill not found at this path)" > /tmp/upstream-mops-cli.md + } + + DIFF=$(diff /tmp/upstream-mops-cli.md skills/mops-cli/SKILL.md || true) + + { + echo "## Upstream diff: caffeinelabs/mops \`${CURRENT}\` → \`${LATEST}\`" + echo "" + echo "Commit: [\`${SHA:0:12}\`](https://github.com/caffeinelabs/mops/commit/${SHA})" + echo "" + echo "**Review instructions:** check which sections are listed as \`owned by icskills\` in the" + echo "skill's upstream comment block before applying changes. Do NOT overwrite those sections." + echo "" + echo "### \`mops-cli\`" + echo "" + if [ -n "$DIFF" ]; then + echo '
Show diff' + echo "" + echo '```diff' + echo "$DIFF" + echo '```' + echo "" + echo '
' + else + echo "No changes." + fi + } > /tmp/pr-body.md + + - name: Create sync PR + if: steps.check.outputs.needed == 'true' + run: | + BRANCH="${{ steps.check.outputs.branch }}" + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git checkout -b "$BRANCH" + git commit --allow-empty -m "chore: upstream sync check — caffeinelabs/mops ${{ steps.latest.outputs.tag }}" + git push -u origin "$BRANCH" + gh pr create \ + --title "chore: sync check — caffeinelabs/mops ${{ steps.latest.outputs.tag }}" \ + --body-file /tmp/pr-body.md + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 32d45dbbc464943dc90d7537b90acdd6c6c398a2 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Tue, 5 May 2026 01:19:03 +0200 Subject: [PATCH 05/11] docs: add Motoko category to schema, CONTRIBUTING, and fix stale Icons.tsx reference --- CONTRIBUTING.md | 4 ++-- skills/skill.schema.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1a62929..9a8126e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -241,6 +241,6 @@ The website auto-generates from SKILL.md frontmatter — no need to edit any sou Use an existing category when possible. The validator warns on unknown categories to catch typos, but new categories are not blocked. -Current categories: **Auth**, **Core**, **DeFi**, **Frontend**, **Governance**, **Infrastructure**, **Integration**, **Security** +Current categories: **Auth**, **Core**, **DeFi**, **Frontend**, **Governance**, **Infrastructure**, **Integration**, **Motoko**, **Security** -To add a new category: update the enum in `skills/skill.schema.json` and the icon in `src/components/Icons.tsx`. +To add a new category: update the description string in `skills/skill.schema.json`, the `KNOWN_CATEGORIES` array in `scripts/check-project.js`, and the `CATEGORY_ORDER` array in `src/lib/skills.ts`. diff --git a/skills/skill.schema.json b/skills/skill.schema.json index 92b0e5d..0b63d2c 100644 --- a/skills/skill.schema.json +++ b/skills/skill.schema.json @@ -36,7 +36,7 @@ "category": { "type": "string", "minLength": 1, - "description": "Skill category for browsing/filtering. Use an existing category when possible: Auth, Core, DeFi, Frontend, Governance, Infrastructure, Integration, Security." + "description": "Skill category for browsing/filtering. Use an existing category when possible: Auth, Core, DeFi, Frontend, Governance, Infrastructure, Integration, Motoko, Security." } }, "additionalProperties": false From f36d65b4d4ef59120c6decec93f94dbd8143e39d Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Tue, 5 May 2026 01:20:19 +0200 Subject: [PATCH 06/11] fix(sync-workflow): filter mops cli-* releases, fix diff direction, remove unused has_changes var --- .github/workflows/sync-upstream.yml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/workflows/sync-upstream.yml b/.github/workflows/sync-upstream.yml index ea24cd6..dbe6d1b 100644 --- a/.github/workflows/sync-upstream.yml +++ b/.github/workflows/sync-upstream.yml @@ -90,16 +90,14 @@ jobs: echo "" } > /tmp/pr-body.md - HAS_CHANGES=false for upstream_name in "writing-motoko" "migrating-motoko" "migrating-motoko-enhanced"; do local_name="${UPSTREAM_TO_LOCAL[$upstream_name]}" curl -sf "https://raw.githubusercontent.com/caffeinelabs/motoko/${SHA}/.agents/skills/${upstream_name}/SKILL.md" \ > /tmp/upstream-${upstream_name}.md 2>/dev/null || { echo "(skill not found at this path)" > /tmp/upstream-${upstream_name}.md } - DIFF=$(diff /tmp/upstream-${upstream_name}.md skills/${local_name}/SKILL.md || true) + DIFF=$(diff skills/${local_name}/SKILL.md /tmp/upstream-${upstream_name}.md || true) if [ -n "$DIFF" ]; then - HAS_CHANGES=true { echo "### \`${local_name}\` ← upstream \`${upstream_name}\`" echo "" @@ -118,7 +116,6 @@ jobs: fi done - echo "has_changes=$HAS_CHANGES" >> $GITHUB_ENV - name: Create sync PR if: steps.check.outputs.needed == 'true' @@ -148,7 +145,7 @@ jobs: - name: Get latest mops release tag id: latest run: | - TAG=$(gh release view --repo caffeinelabs/mops --json tagName -q .tagName) + TAG=$(gh release list --repo caffeinelabs/mops --json tagName --jq '[.[] | select(.tagName | startswith("cli-"))] | first | .tagName') echo "tag=$TAG" >> $GITHUB_OUTPUT env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -208,7 +205,7 @@ jobs: echo "(skill not found at this path)" > /tmp/upstream-mops-cli.md } - DIFF=$(diff /tmp/upstream-mops-cli.md skills/mops-cli/SKILL.md || true) + DIFF=$(diff skills/mops-cli/SKILL.md /tmp/upstream-mops-cli.md || true) { echo "## Upstream diff: caffeinelabs/mops \`${CURRENT}\` → \`${LATEST}\`" From 1e6d5e4d6c51bb3ebdbf99072174f45a5f7ac11c Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Tue, 5 May 2026 01:22:12 +0200 Subject: [PATCH 07/11] fix(sync-workflow): add --limit 100 to mops release list to avoid null tag on pagination boundary --- .github/workflows/sync-upstream.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sync-upstream.yml b/.github/workflows/sync-upstream.yml index dbe6d1b..3bcb9ba 100644 --- a/.github/workflows/sync-upstream.yml +++ b/.github/workflows/sync-upstream.yml @@ -145,7 +145,7 @@ jobs: - name: Get latest mops release tag id: latest run: | - TAG=$(gh release list --repo caffeinelabs/mops --json tagName --jq '[.[] | select(.tagName | startswith("cli-"))] | first | .tagName') + TAG=$(gh release list --repo caffeinelabs/mops --limit 100 --json tagName --jq '[.[] | select(.tagName | startswith("cli-"))] | first | .tagName') echo "tag=$TAG" >> $GITHUB_OUTPUT env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 711003cb8df843c35ff6af950c75e5872a6bb24c Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Tue, 5 May 2026 01:27:01 +0200 Subject: [PATCH 08/11] fix(ci): exclude top-level files in skills/ from changed-skill detection skills/skill.schema.json was being extracted by grep and then failing the SKILL.md file test, causing bash -e to exit the step. Add lookahead (?=/) to require a path separator after the dir name, and add || true to the while loop as a belt-and-suspenders guard. --- .github/workflows/_checks.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/_checks.yml b/.github/workflows/_checks.yml index 409e6d7..33d0d21 100644 --- a/.github/workflows/_checks.yml +++ b/.github/workflows/_checks.yml @@ -30,10 +30,10 @@ jobs: run: | if [ "${{ github.event_name }}" = "pull_request" ]; then dirs=$(git diff --name-only origin/${{ github.base_ref }}...HEAD -- 'skills/' \ - | grep -oP '^skills/\K[^/]+' \ + | grep -oP '^skills/\K[^/]+(?=/)' \ | sort -u \ | grep -v '^_' \ - | while read -r d; do [ -f "skills/$d/SKILL.md" ] && echo "$d"; done) + | while read -r d; do [ -f "skills/$d/SKILL.md" ] && echo "$d" || true; done) if [ -n "$dirs" ]; then # Build space-separated paths for skill-validator paths=$(echo "$dirs" | sed 's|^|skills/|' | tr '\n' ' ') From 143393f99d9d3f0e4a58426621bf846d1a944149 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Tue, 5 May 2026 09:22:04 +0200 Subject: [PATCH 09/11] refactor(motoko): drop api-reference.md, link to mops.one/core instead MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit api-reference.md is a static snapshot of versioned library signatures that will silently drift from reality as mo:core evolves. Directing agents to the live mops.one/core reference is more reliable. examples.md, control-flow.md, and type-conversions.md are pattern-based and stable across releases — kept and documented as icskills-owned in the upstream comment block. --- skills/motoko/SKILL.md | 8 +- skills/motoko/references/api-reference.md | 511 ---------------------- 2 files changed, 5 insertions(+), 514 deletions(-) delete mode 100644 skills/motoko/references/api-reference.md diff --git a/skills/motoko/SKILL.md b/skills/motoko/SKILL.md index 772221a..2893ec3 100644 --- a/skills/motoko/SKILL.md +++ b/skills/motoko/SKILL.md @@ -14,7 +14,9 @@ metadata: Last synced: 2026-05-04 Sections owned by icskills (do not overwrite from upstream): M0141 / M0145 / do?{} / variant tag / transient var / - Runtime.envVar / Text.join / List.get vs List.at --> + Runtime.envVar / Text.join / List.get vs List.at + References owned by icskills (not from upstream, do not delete): + references/examples.md, references/control-flow.md, references/type-conversions.md --> # Motoko Language @@ -216,7 +218,7 @@ import Debug "mo:core/Debug"; import Runtime "mo:core/Runtime"; **Import requirement**: Dot-notation methods only work when the module is imported. `myArray.find(...)` requires `import Array "mo:core/Array"`; iterator chaining requires `import Iter "mo:core/Iter"`; `myBool.toText()` requires `import Bool "mo:core/Bool"`. The error message hints at the missing import (M0072). -Full API signatures: [references/api-reference.md](references/api-reference.md). +Full API signatures: [mops.one/core](https://mops.one/core). ### Collections @@ -355,7 +357,7 @@ let ?backendId = Runtime.envVar("PUBLIC_CANISTER_ID:backend") ## Additional References -- **API signatures**: [references/api-reference.md](references/api-reference.md) — complete mo:core function signatures +- **API signatures**: [mops.one/core](https://mops.one/core) — live mo:core function signatures (authoritative) - **Working examples**: [references/examples.md](references/examples.md) — full actors, multi-file architecture, todo app, timers - **Control flow**: [references/control-flow.md](references/control-flow.md) — switch, for loops, break/continue - **Type conversions**: [references/type-conversions.md](references/type-conversions.md) — Nat/Int size conversions diff --git a/skills/motoko/references/api-reference.md b/skills/motoko/references/api-reference.md deleted file mode 100644 index 824c530..0000000 --- a/skills/motoko/references/api-reference.md +++ /dev/null @@ -1,511 +0,0 @@ -# Core Library API Reference - -Complete API signatures from `mo:core`. Targets mo:core 2.x — verify against mops.one/core if your version differs. - -Use this as a reference when using contextual dot notation. - -## Array - -- `public func all(self : [T], predicate : T -> Bool) : Bool` -- `public func any(self : [T], predicate : T -> Bool) : Bool` -- `public func binarySearch(self : [T], compare : (implicit : (T, T) -> Order.Order), element : T) : { #found : Nat; #insertionIndex : Nat }` -- `public func compare(self : [T], other : [T], compare : (implicit : (T, T) -> Order.Order)) : Order.Order` -- `public func concat(self : [T], other : [T]) : [T]` -- `public func empty() : [T]` -- `public func enumerate(self : [T]) : Types.Iter<(Nat, T)>` -- `public func equal(self : [T], other : [T], equal : (implicit : (T, T) -> Bool)) : Bool` -- `public func filter(self : [T], f : T -> Bool) : [T]` -- `public func filterMap(self : [T], f : T -> ?R) : [R]` -- `public func find(self : [T], predicate : T -> Bool) : ?T` -- `public func findIndex(self : [T], predicate : T -> Bool) : ?Nat` -- `public func flatMap(self : [T], k : T -> Types.Iter) : [R]` -- `public func flatten(self : [[T]]) : [T]` -- `public func foldLeft(self : [T], base : A, combine : (A, T) -> A) : A` -- `public func foldRight(self : [T], base : A, combine : (T, A) -> A) : A` -- `public func forEach(self : [T], f : T -> ())` -- `public func fromIter(iter : Types.Iter) : [T]` -- `public func fromVarArray(varArray : [var T]) : [T]` -- `public func indexOf(self : [T], equal : (implicit : (T, T) -> Bool), element : T) : ?Nat` -- `public func isEmpty(self : [T]) : Bool` -- `public func isSorted(self : [T], compare : (implicit : (T, T) -> Order.Order)) : Bool` -- `public func join(self : Types.Iter<[T]>) : [T]` -- `public func keys(self : [T]) : Types.Iter` -- `public func lastIndexOf(self : [T], equal : (implicit : (T, T) -> Bool), element : T) : ?Nat` -- `public func map(self : [T], f : T -> R) : [R]` -- `public func mapEntries(self : [T], f : (T, Nat) -> R) : [R]` -- `public func nextIndexOf(self : [T], equal : (implicit : (T, T) -> Bool), element : T, fromInclusive : Nat) : ?Nat` -- `public func prevIndexOf(self : [T], equal : (implicit : (T, T) -> Bool), element : T, fromExclusive : Nat) : ?Nat` -- `public func range(self : [T], fromInclusive : Int, toExclusive : Int) : Types.Iter` -- `public func repeat(item : T, size : Nat) : [T]` -- `public func reverse(self : [T]) : [T]` -- `public func singleton(element : T) : [T]` -- `public func size(self : [T]) : Nat` -- `public func sliceToArray(self : [T], fromInclusive : Int, toExclusive : Int) : [T]` -- `public func sliceToVarArray(self : [T], fromInclusive : Int, toExclusive : Int) : [var T]` -- `public func sort(self : [T], compare : (implicit : (T, T) -> Order.Order)) : [T]` -- `public func toText(self : [T], f : (implicit : (toText : T -> Text))) : Text` -- `public func toVarArray(self : [T]) : [var T]` -- `public func values(self : [T]) : Types.Iter` -- `public let tabulate : (size : Nat, generator : Nat -> T) -> [T]` - -## Debug - -- `public func todo() : None` -- `public let print : (text : Text) -> ()` - -## Int - -- `public func add(x : Int, y : Int) : Int` -- `public func compare(x : Int, y : Int) : Order.Order` -- `public func div(x : Int, y : Int) : Int` -- `public func equal(x : Int, y : Int) : Bool` -- `public func fromNat(nat : Nat) : Int` -- `public func fromText(text : Text) : ?Int` -- `public func greater(x : Int, y : Int) : Bool` -- `public func greaterOrEqual(x : Int, y : Int) : Bool` -- `public func less(x : Int, y : Int) : Bool` -- `public func lessOrEqual(x : Int, y : Int) : Bool` -- `public func max(x : Int, y : Int) : Int` -- `public func min(x : Int, y : Int) : Int` -- `public func mul(x : Int, y : Int) : Int` -- `public func neg(x : Int) : Int` -- `public func notEqual(x : Int, y : Int) : Bool` -- `public func pow(x : Int, y : Int) : Int` -- `public func range(fromInclusive : Int, toExclusive : Int) : Iter.Iter` -- `public func rangeBy(fromInclusive : Int, toExclusive : Int, step : Int) : Iter.Iter` -- `public func rangeByInclusive(from : Int, to : Int, step : Int) : Iter.Iter` -- `public func rangeInclusive(from : Int, to : Int) : Iter.Iter` -- `public func rem(x : Int, y : Int) : Int` -- `public func sub(x : Int, y : Int) : Int` -- `public func toInt(self : Text) : ?Int` -- `public func toNat(self : Int) : Nat` -- `public func toText(self : Int) : Text` -- `public let abs : (x : Int) -> Nat` -- `public let fromInt16 : (x : Int16) -> Int` -- `public let fromInt32 : (x : Int32) -> Int` -- `public let fromInt64 : (x : Int64) -> Int` -- `public let fromInt8 : (x : Int8) -> Int` -- `public let toFloat : (self : Int) -> Float` -- `public let toInt16 : (self : Int) -> Int16` -- `public let toInt32 : (self : Int) -> Int32` -- `public let toInt64 : (self : Int) -> Int64` -- `public let toInt8 : (self : Int) -> Int8` -- `public type Int` - -## Iter - -- `public func all(self : Iter, f : T -> Bool) : Bool` -- `public func any(self : Iter, f : T -> Bool) : Bool` -- `public func concat(self : Iter, other : Iter) : Iter` -- `public func contains(self : Iter, equal : (implicit : (T, T) -> Bool), value : T) : Bool` -- `public func drop(self : Iter, n : Nat) : Iter` -- `public func dropWhile(self : Iter, f : T -> Bool) : Iter` -- `public func empty() : Iter` -- `public func enumerate(self : Iter) : Iter<(Nat, T)>` -- `public func filter(self : Iter, f : T -> Bool) : Iter` -- `public func filterMap(self : Iter, f : T -> ?R) : Iter` -- `public func find(self : Iter, f : T -> Bool) : ?T` -- `public func findIndex(self : Iter, predicate : T -> Bool) : ?Nat` -- `public func flatMap(self : Iter, f : T -> Iter) : Iter` -- `public func flatten(self : Iter>) : Iter` -- `public func foldLeft(self : Iter, initial : R, combine : (R, T) -> R) : R` -- `public func foldRight(self : Iter, initial : R, combine : (T, R) -> R) : R` -- `public func forEach( self : Iter, f : (T) -> () )` -- `public func fromArray(array : [T]) : Iter` -- `public func fromVarArray(array : [var T]) : Iter` -- `public func infinite(item : T) : Iter` -- `public func map(self : Iter, f : T -> R) : Iter` -- `public func max(self : Iter, compare : (implicit : (T, T) -> Order.Order)) : ?T` -- `public func min(self : Iter, compare : (implicit : (T, T) -> Order.Order)) : ?T` -- `public func reduce(self : Iter, combine : (T, T) -> T) : ?T` -- `public func repeat(item : T, count : Nat) : Iter` -- `public func reverse(self : Iter) : Iter` -- `public func scanLeft(self : Iter, initial : R, combine : (R, T) -> R) : Iter` -- `public func scanRight(self : Iter, initial : R, combine : (T, R) -> R) : Iter` -- `public func singleton(value : T) : Iter` -- `public func size(self : Iter) : Nat` -- `public func sort(self : Iter, compare : (implicit : (T, T) -> Order.Order)) : Iter` -- `public func step(self : Iter, n : Nat) : Iter` -- `public func take(self : Iter, n : Nat) : Iter` -- `public func takeWhile(self : Iter, f : T -> Bool) : Iter` -- `public func toArray(self : Iter) : [T]` -- `public func toVarArray(self : Iter) : [var T]` -- `public func unfold(initial : S, step : S -> ?(T, S)) : Iter` -- `public func zip3(self : Iter
, other1 : Iter, other2 : Iter) : Iter<(A, B, C)>` -- `public func zip(self : Iter, other : Iter) : Iter<(A, B)>` -- `public func zipWith3(self : Iter, other1 : Iter, other2 : Iter, f : (A, B, C) -> R) : Iter` -- `public func zipWith(self : Iter, other : Iter, f : (A, B) -> R) : Iter` -- `public type Iter` - -## List - -- `public func add(self : List, element : T)` -- `public func addAll(self : List, iter : Types.Iter)` -- `public func addRepeat(self : List, initValue : T, count : Nat)` -- `public func all(self : List, predicate : T -> Bool) : Bool` -- `public func any(self : List, predicate : T -> Bool) : Bool` -- `public func append(self : List, added : List)` -- `public func at(self : List, index : Nat) : T` -- `public func binarySearch(self : List, compare : (implicit : (T, T) -> Types.Order), element : T) : { #found : Nat; #insertionIndex : Nat }` -- `public func clear(self : List)` -- `public func clone(self : List) : List` -- `public func compare(self : List, other : List, compare : (implicit : (T, T) -> Types.Order)) : Types.Order` -- `public func contains(self : List, equal : (implicit : (T, T) -> Bool), element : T) : Bool` -- `public func deduplicate(self : List, equal : (implicit : (T, T) -> Bool))` -- `public func empty() : List` -- `public func enumerate(self : List) : Types.Iter<(Nat, T)>` -- `public func equal(self : List, other : List, equal : (implicit : (T, T) -> Bool)) : Bool` -- `public func fill(self : List, value : T)` -- `public func filter(self : List, predicate : T -> Bool) : List` -- `public func filterMap(self : List, f : T -> ?R) : List` -- `public func find(self : List, predicate : T -> Bool) : ?T` -- `public func findIndex(self : List, predicate : T -> Bool) : ?Nat` -- `public func findLastIndex(self : List, predicate : T -> Bool) : ?Nat` -- `public func first(self : List) : ?T` -- `public func flatMap(self : List, k : T -> Types.Iter) : List` -- `public func flatten(self : List>) : List` -- `public func foldLeft(self : List, base : A, combine : (A, T) -> A) : A` -- `public func foldRight(self : List, base : A, combine : (T, A) -> A) : A` -- `public func forEach(self : List, f : T -> ())` -- `public func forEachEntry(self : List, f : (Nat, T) -> ())` -- `public func forEachInRange(self : List, f : T -> (), fromInclusive : Nat, toExclusive : Nat)` -- `public func fromArray(array : [T]) : List` -- `public func fromIter(iter : Types.Iter) : List` -- `public func fromVarArray(array : [var T]) : List` -- `public func indexOf(self : List, equal : (implicit : (T, T) -> Bool), element : T) : ?Nat` -- `public func isEmpty(self : List) : Bool` -- `public func isSorted(self : List, compare : (implicit : (T, T) -> Types.Order)) : Bool` -- `public func join(self : Types.Iter>) : List` -- `public func keys(self : List) : Types.Iter` -- `public func last(self : List) : ?T` -- `public func lastIndexOf(self : List, equal : (implicit : (T, T) -> Bool), element : T) : ?Nat` -- `public func map(self : List, f : T -> R) : List` -- `public func mapEntries(self : List, f : (T, Nat) -> R) : List` -- `public func mapInPlace(self : List, f : T -> T)` -- `public func mapResult(self : List, f : T -> Types.Result) : Types.Result, E>` -- `public func max(self : List, compare : (implicit : (T, T) -> Types.Order)) : ?T` -- `public func min(self : List, compare : (implicit : (T, T) -> Types.Order)) : ?T` -- `public func nextIndexOf(self : List, equal : (implicit : (T, T) -> Bool), element : T, fromInclusive : Nat) : ?Nat` -- `public func prevIndexOf(self : List, equal : (implicit : (T, T) -> Bool), element : T, fromExclusive : Nat) : ?Nat` -- `public func put(self : List, index : Nat, value : T)` -- `public func range(self : List, fromInclusive : Int, toExclusive : Int) : Types.Iter` -- `public func reader(self : List, start : Nat) : () -> T` -- `public func removeLast(self : List) : ?T` -- `public func repeat(initValue : T, size : Nat) : List` -- `public func reverse(self : List) : List` -- `public func reverseEnumerate(self : List) : Types.Iter<(Nat, T)>` -- `public func reverseForEach(self : List, f : T -> ())` -- `public func reverseForEachEntry(self : List, f : (Nat, T) -> ())` -- `public func reverseInPlace(self : List)` -- `public func reverseValues(self : List) : Types.Iter` -- `public func singleton(element : T) : List` -- `public func size(self : List) : Nat` -- `public func sliceToArray(self : List, fromInclusive : Int, toExclusive : Int) : [T]` -- `public func sliceToVarArray(self : List, fromInclusive : Int, toExclusive : Int) : [var T]` -- `public func sort(self : List, compare : (implicit : (T, T) -> Types.Order)) : List` -- `public func sortInPlace(self : List, compare : (implicit : (T, T) -> Types.Order))` -- `public func tabulate(size : Nat, generator : Nat -> T) : List` -- `public func toArray(self : List) : [T]` -- `public func toList(self : Types.Iter) : List` -- `public func toText(self : List, toText : (implicit : T -> Text)) : Text` -- `public func toVarArray(self : List) : [var T]` -- `public func truncate(self : List, newSize : Nat)` -- `public func values(self : List) : Types.Iter` -- `public type List` - -## Map - -- `public func add(self : Map, compare : (implicit : (K, K) -> Order.Order), key : K, value : V)` -- `public func all(self : Map, predicate : (K, V) -> Bool) : Bool` -- `public func any(self : Map, predicate : (K, V) -> Bool) : Bool` -- `public func clear(self : Map)` -- `public func clone(self : Map) : Map` -- `public func containsKey(self : Map, compare : (implicit : (K, K) -> Order.Order), key : K) : Bool` -- `public func empty() : Map` -- `public func entries(self : Map) : Types.Iter<(K, V)>` -- `public func entriesFrom(self : Map, compare : (implicit : (K, K) -> Order.Order), key : K) : Types.Iter<(K, V)>` -- `public func equal(self : Map, other : Map, compare : (implicit : (K, K) -> Types.Order), equal : (implicit : (V, V) -> Bool)) : Bool` -- `public func filter(self : Map, compare : (implicit : (K, K) -> Order.Order), criterion : (K, V) -> Bool) : Map` -- `public func filterMap(self : Map, compare : (implicit : (K, K) -> Order.Order), project : (K, V1) -> ?V2) : Map` -- `public func foldLeft(self : Map, base : A, combine : (A, K, V) -> A) : A` -- `public func foldRight(self : Map, base : A, combine : (K, V, A) -> A) : A` -- `public func forEach(self : Map, operation : (K, V) -> ())` -- `public func fromArray(array : [(K, V)], compare : (implicit : (K, K) -> Order.Order)) : Map` -- `public func fromIter(iter : Types.Iter<(K, V)>, compare : (implicit : (K, K) -> Order.Order)) : Map` -- `public func fromVarArray(array : [var (K, V)], compare : (implicit : (K, K) -> Order.Order)) : Map` -- `public func get(self : Map, compare : (implicit : (K, K) -> Order.Order), key : K) : ?V` -- `public func isEmpty(self : Map) : Bool` -- `public func keys(self : Map) : Types.Iter` -- `public func map(self : Map, project : (K, V1) -> V2) : Map` -- `public func maxEntry(self : Map) : ?(K, V)` -- `public func minEntry(self : Map) : ?(K, V)` -- `public func remove(self : Map, compare : (implicit : (K, K) -> Order.Order), key : K)` -- `public func reverseEntries(self : Map) : Types.Iter<(K, V)>` -- `public func reverseEntriesFrom(self : Map, compare : (implicit : (K, K) -> Order.Order), key : K) : Types.Iter<(K, V)>` -- `public func singleton(key : K, value : V) : Map` -- `public func size(self : Map) : Nat` -- `public func toArray(self : Map) : [(K, V)]` -- `public func toMap(self : Types.Iter<(K, V)>, compare : (implicit : (K, K) -> Order.Order)) : Map` -- `public func toText(self : Map, keyFormat : (implicit : (toText : K -> Text)), valueFormat : (implicit : (toText : V -> Text))) : Text` -- `public func toVarArray(self : Map) : [var (K, V)]` -- `public func values(self : Map) : Types.Iter` -- `public type Map` - -## Nat - -- `public func add(x : Nat, y : Nat) : Nat` -- `public func allValues() : Iter.Iter` -- `public func compare(x : Nat, y : Nat) : Order.Order` -- `public func div(x : Nat, y : Nat) : Nat` -- `public func equal(x : Nat, y : Nat) : Bool` -- `public func fromText(text : Text) : ?Nat` -- `public func greater(x : Nat, y : Nat) : Bool` -- `public func greaterOrEqual(x : Nat, y : Nat) : Bool` -- `public func less(x : Nat, y : Nat) : Bool` -- `public func lessOrEqual(x : Nat, y : Nat) : Bool` -- `public func max(x : Nat, y : Nat) : Nat` -- `public func min(x : Nat, y : Nat) : Nat` -- `public func mul(x : Nat, y : Nat) : Nat` -- `public func notEqual(x : Nat, y : Nat) : Bool` -- `public func pow(x : Nat, y : Nat) : Nat` -- `public func range(fromInclusive : Nat, toExclusive : Nat) : Iter.Iter` -- `public func rangeBy(fromInclusive : Nat, toExclusive : Nat, step : Int) : Iter.Iter` -- `public func rangeByInclusive(from : Nat, to : Nat, step : Int) : Iter.Iter` -- `public func rangeInclusive(from : Nat, to : Nat) : Iter.Iter` -- `public func rem(x : Nat, y : Nat) : Nat` -- `public func sub(x : Nat, y : Nat) : Nat` -- `public func toInt(self : Nat) : Int` -- `public let bitshiftLeft : (x : Nat, y : Nat32) -> Nat` -- `public let bitshiftRight : (x : Nat, y : Nat32) -> Nat` -- `public let fromNat16 : Nat16 -> Nat` -- `public let fromNat32 : Nat32 -> Nat` -- `public let fromNat64 : Nat64 -> Nat` -- `public let fromNat8 : Nat8 -> Nat` -- `public let toFloat : (self : Nat) -> Float` -- `public let toNat : (self : Text) -> ?Nat` -- `public let toNat16 : (self : Nat) -> Nat16` -- `public let toNat32 : (self : Nat) -> Nat32` -- `public let toNat64 : (self : Nat) -> Nat64` -- `public let toNat8 : (self : Nat) -> Nat8` -- `public let toText : (self : Nat) -> Text` -- `public type Nat` - -## Option - -- `public func apply(self : ?T, f : ?(T -> R)) : ?R` -- `public func chain(self : ?T, f : T -> ?R) : ?R` -- `public func compare(self : ?T, other : ?T, compare : (implicit : (T, T) -> Types.Order)) : Types.Order` -- `public func equal(self : ?T, other : ?T, eq : (implicit : (equal : (T, T) -> Bool))) : Bool` -- `public func flatten(self : ??T) : ?T` -- `public func forEach(self : ?T, f : T -> ())` -- `public func get(self : ?T, default : T) : T` -- `public func getMapped(self : ?T, f : T -> R, default : R) : R` -- `public func isNull(self : ?Any) : Bool` -- `public func isSome(self : ?Any) : Bool` -- `public func map(self : ?T, f : T -> R) : ?R` -- `public func some(self : T) : ?T` -- `public func toText(self : ?T, toText : (implicit : T -> Text)) : Text` -- `public func unwrap(self : ?T) : T` - -## Order - -- `public func allValues() : Types.Iter` -- `public func equal(self : Order, other : Order) : Bool` -- `public func isEqual(self : Order) : Bool` -- `public func isGreater(self : Order) : Bool` -- `public func isLess(self : Order) : Bool` -- `public type Order` - -## Principal - -- `public func anonymous() : Principal` -- `public func compare(self : Principal, other : Principal) : { #less; #equal; #greater }` -- `public func equal(self : Principal, other : Principal) : Bool` -- `public func fromText(t : Text) : Principal` -- `public func greater(self : Principal, other : Principal) : Bool` -- `public func greaterOrEqual(self : Principal, other : Principal) : Bool` -- `public func hash(self : Principal) : Types.Hash` -- `public func isAnonymous(self : Principal) : Bool` -- `public func isCanister(self : Principal) : Bool` -- `public func isController(self : Principal) : Bool` -- `public func isReserved(self : Principal) : Bool` -- `public func isSelfAuthenticating(self : Principal) : Bool` -- `public func less(self : Principal, other : Principal) : Bool` -- `public func lessOrEqual(self : Principal, other : Principal) : Bool` -- `public func notEqual(self : Principal, other : Principal) : Bool` -- `public func toLedgerAccount(self : Principal, subAccount : ?Blob) : Blob` -- `public func toText(self : Principal) : Text` -- `public let fromActor : (a : actor {}) -> Principal` -- `public let fromBlob : (self : Blob) -> Principal` -- `public let toBlob : (self : Principal) -> Blob` -- `public type Principal` - -## Queue - -- `public func all(self : Queue, predicate : T -> Bool) : Bool` -- `public func any(self : Queue, predicate : T -> Bool) : Bool` -- `public func clear(self : Queue)` -- `public func clone(self : Queue) : Queue` -- `public func compare(self : Queue, other : Queue, compare : (implicit : (T, T) -> Order.Order)) : Order.Order` -- `public func contains(self : Queue, equal : (implicit : (T, T) -> Bool), element : T) : Bool` -- `public func empty() : Queue` -- `public func equal(self : Queue, other : Queue, equal : (implicit : (T, T) -> Bool)) : Bool` -- `public func filter(self : Queue, criterion : T -> Bool) : Queue` -- `public func filterMap(self : Queue, project : T -> ?U) : Queue` -- `public func forEach(self : Queue, operation : T -> ())` -- `public func fromArray(array : [T]) : Queue` -- `public func fromIter(iter : Iter.Iter) : Queue` -- `public func fromVarArray(array : [var T]) : Queue` -- `public func isEmpty(self : Queue) : Bool` -- `public func map(self : Queue, project : T -> U) : Queue` -- `public func peekBack(self : Queue) : ?T` -- `public func peekFront(self : Queue) : ?T` -- `public func popBack(self : Queue) : ?T` -- `public func popFront(self : Queue) : ?T` -- `public func pushBack(self : Queue, element : T)` -- `public func pushFront(self : Queue, element : T)` -- `public func reverseValues(self : Queue) : Iter.Iter` -- `public func singleton(element : T) : Queue` -- `public func size(self : Queue) : Nat` -- `public func toArray(self : Queue) : [T]` -- `public func toQueue(self : Iter.Iter) : Queue` -- `public func toText(self : Queue, format : (implicit : (toText : T -> Text))) : Text` -- `public func toVarArray(self : Queue) : [var T]` -- `public func values(self : Queue) : Iter.Iter` -- `public type Queue` - -## Runtime - -- `public func trap(message : Text) : None` -- `public func envVar(name : Text) : ?Text` - -## Set - -- `public func add(self : Set, compare : (implicit : (T, T) -> Order.Order), element : T)` -- `public func addAll(self : Set, compare : (implicit : (T, T) -> Order.Order), iter : Types.Iter)` -- `public func all(self : Set, predicate : T -> Bool) : Bool` -- `public func any(self : Set, predicate : T -> Bool) : Bool` -- `public func clear(self : Set)` -- `public func clone(self : Set) : Set` -- `public func compare(self : Set, other : Set, compare : (implicit : (T, T) -> Order.Order)) : Order.Order` -- `public func contains(self : Set, compare : (implicit : (T, T) -> Order.Order), element : T) : Bool` -- `public func difference(self : Set, other : Set, compare : (implicit : (T, T) -> Order.Order)) : Set` -- `public func empty() : Set` -- `public func equal(self : Set, other : Set, compare : (implicit : (T, T) -> Types.Order)) : Bool` -- `public func filter(self : Set, compare : (implicit : (T, T) -> Order.Order), criterion : T -> Bool) : Set` -- `public func filterMap(self : Set, compare : (implicit : (T2, T2) -> Order.Order), project : T1 -> ?T2) : Set` -- `public func flatten(self : Set>, compare : (implicit : (T, T) -> Order.Order)) : Set` -- `public func foldLeft(self : Set, base : A, combine : (A, T) -> A) : A` -- `public func foldRight(self : Set, base : A, combine : (T, A) -> A) : A` -- `public func forEach(self : Set, operation : T -> ())` -- `public func fromArray(array : [T], compare : (implicit : (T, T) -> Order.Order)) : Set` -- `public func fromIter(iter : Types.Iter, compare : (implicit : (T, T) -> Order.Order)) : Set` -- `public func intersection(self : Set, other : Set, compare : (implicit : (T, T) -> Order.Order)) : Set` -- `public func isEmpty(self : Set) : Bool` -- `public func isSubset(self : Set, other : Set, compare : (implicit : (T, T) -> Order.Order)) : Bool` -- `public func join(setIterator : Types.Iter>, compare : (implicit : (T, T) -> Order.Order)) : Set` -- `public func map(self : Set, compare : (implicit : (T2, T2) -> Order.Order), project : T1 -> T2) : Set` -- `public func max(self : Set) : ?T` -- `public func min(self : Set) : ?T` -- `public func remove(self : Set, compare : (implicit : (T, T) -> Order.Order), element : T) : ()` -- `public func reverseValues(self : Set) : Types.Iter` -- `public func reverseValuesFrom(self : Set, compare : (implicit : (T, T) -> Order.Order), element : T) : Types.Iter` -- `public func singleton(element : T) : Set` -- `public func size(self : Set) : Nat` -- `public func toArray(self : Set) : [T]` -- `public func toSet(self : Types.Iter, compare : (implicit : (T, T) -> Order.Order)) : Set` -- `public func toText(self : Set, toText : (implicit : T -> Text)) : Text` -- `public func union(self : Set, other : Set, compare : (implicit : (T, T) -> Order.Order)) : Set` -- `public func values(self : Set) : Types.Iter` -- `public func valuesFrom(self : Set, compare : (implicit : (T, T) -> Order.Order), element : T) : Types.Iter` -- `public type Set` - -## Stack - -- `public func all(self : Stack, predicate : T -> Bool) : Bool` -- `public func any(self : Stack, predicate : T -> Bool) : Bool` -- `public func clear(self : Stack)` -- `public func clone(self : Stack) : Stack` -- `public func compare(self : Stack, other : Stack, compare : (implicit : (T, T) -> Order.Order)) : Order.Order` -- `public func contains(self : Stack, equal : (implicit : (T, T) -> Bool), element : T) : Bool` -- `public func empty() : Stack` -- `public func equal(self : Stack, other : Stack, equal : (implicit : (T, T) -> Bool)) : Bool` -- `public func filter(self : Stack, predicate : T -> Bool) : Stack` -- `public func filterMap(self : Stack, project : T -> ?U) : Stack` -- `public func find(self : Stack, predicate : T -> Bool) : ?T` -- `public func findIndex(self : Stack, predicate : T -> Bool) : ?Nat` -- `public func forEach(self : Stack, operation : T -> ())` -- `public func fromArray(array : [T]) : Stack` -- `public func fromIter(iter : Types.Iter) : Stack` -- `public func fromVarArray(array : [var T]) : Stack` -- `public func get(self : Stack, position : Nat) : ?T` -- `public func isEmpty(self : Stack) : Bool` -- `public func map(self : Stack, project : T -> U) : Stack` -- `public func peek(self : Stack) : ?T` -- `public func pop(self : Stack) : ?T` -- `public func push(self : Stack, value : T)` -- `public func reverse(self : Stack)` -- `public func reverseValues(self : Stack) : Iter.Iter` -- `public func singleton(element : T) : Stack` -- `public func size(self : Stack) : Nat` -- `public func tabulate(size : Nat, generator : Nat -> T) : Stack` -- `public func toArray(self : Stack) : [T]` -- `public func toStack(self : Types.Iter) : Stack` -- `public func toText(self : Stack, format : (implicit : (toText : T -> Text))) : Text` -- `public func toVarArray(self : Stack) : [var T]` -- `public func values(self : Stack) : Iter.Iter` -- `public type Stack` - -## Text - -- `public func compare(self : Text, other : Text) : Order.Order` -- `public func compareWith(self : Text, other : Text, compare : (Char, Char) -> Order.Order) : Order.Order` -- `public func concat(self : Text, other : Text) : Text` -- `public func contains(self : Text, p : Pattern) : Bool` -- `public func endsWith(self : Text, p : Pattern) : Bool` -- `public func equal(self : Text, other : Text) : Bool` -- `public func flatMap(self : Text, f : Char -> Text) : Text` -- `public func foldLeft(self : Text, base : A, combine : (A, Char) -> A) : A` -- `public func fromArray(a : [Char]) : Text` -- `public func fromIter(cs : Iter.Iter) : Text` -- `public func fromVarArray(a : [var Char]) : Text` -- `public func greater(self : Text, other : Text) : Bool` -- `public func greaterOrEqual(self : Text, other : Text) : Bool` -- `public func isEmpty(self : Text) : Bool` -- `public func join(self : Iter.Iter, sep : Text) : Text` -- `public func less(self : Text, other : Text) : Bool` -- `public func lessOrEqual(self : Text, other : Text) : Bool` -- `public func map(self : Text, f : Char -> Char) : Text` -- `public func notEqual(self : Text, other : Text) : Bool` -- `public func replace(self : Text, p : Pattern, r : Text) : Text` -- `public func reverse(self : Text) : Text` -- `public func size(self : Text) : Nat` -- `public func split(self : Text, p : Pattern) : Iter.Iter` -- `public func startsWith(self : Text, p : Pattern) : Bool` -- `public func stripEnd(self : Text, p : Pattern) : ?Text` -- `public func stripStart(self : Text, p : Pattern) : ?Text` -- `public func toArray(self : Text) : [Char]` -- `public func toIter(self : Text) : Iter.Iter` -- `public func toText(self : Text) : Text` -- `public func toVarArray(self : Text) : [var Char]` -- `public func tokens(self : Text, p : Pattern) : Iter.Iter` -- `public func trim(self : Text, p : Pattern) : Text` -- `public func trimEnd(self : Text, p : Pattern) : Text` -- `public func trimStart(self : Text, p : Pattern) : Text` -- `public let decodeUtf8 : (self : Blob) -> ?Text` -- `public let encodeUtf8 : (self : Text) -> Blob` -- `public let fromChar : (c : Char) -> Text` -- `public let toLower : (self : Text) -> Text` -- `public let toUpper : (self : Text) -> Text` -- `public type Pattern` -- `public type Text` - -## Time - -- `public func now() : Time` -- `public func toNanoseconds(duration : Duration) : Nat` -- `public type Duration` -- `public type Time` -- `public type TimerId` - -Note: `Time` is `Int` (nanoseconds). Use `Int.compare` to compare timestamps — there is no `Time.compare`. From dcda5dec5f08dbcd4a069e0b747bd156028fc05b Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Tue, 5 May 2026 11:18:56 +0200 Subject: [PATCH 10/11] fix(sync-workflow): use pr-automation-bot-public app token instead of GITHUB_TOKEN GITHUB_TOKEN with contents:write + pull-requests:write is blocked on public dfinity repos (see dfinity/developer-docs#196). Switch both jobs to the pr-automation-bot-public GitHub App token. - Remove elevated permissions blocks from both jobs - Add Create GitHub App Token step (actions/create-github-app-token v3) - Replace secrets.GITHUB_TOKEN with steps.app-token.outputs.token - Move curl auth to GH_TOKEN env var (avoids inline token interpolation) - Update git committer name to pr-automation-bot-public[bot] --- .github/workflows/sync-upstream.yml | 49 ++++++++++++++++++----------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/.github/workflows/sync-upstream.yml b/.github/workflows/sync-upstream.yml index 3bcb9ba..185f28a 100644 --- a/.github/workflows/sync-upstream.yml +++ b/.github/workflows/sync-upstream.yml @@ -9,20 +9,24 @@ jobs: check-motoko: name: Check caffeinelabs/motoko runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write steps: - uses: actions/checkout@v4 + - name: Create GitHub App Token + uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3 + id: app-token + with: + app-id: ${{ vars.PR_AUTOMATION_BOT_PUBLIC_APP_ID }} + private-key: ${{ secrets.PR_AUTOMATION_BOT_PUBLIC_PRIVATE_KEY }} + - name: Get latest motoko release tag id: latest run: | TAG=$(gh release view --repo caffeinelabs/motoko --json tagName -q .tagName) echo "tag=$TAG" >> $GITHUB_OUTPUT env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ steps.app-token.outputs.token }} - name: Get current pinned tag id: current @@ -54,18 +58,20 @@ jobs: run: | TAG="${{ steps.latest.outputs.tag }}" RESULT=$(curl -sf "https://api.github.com/repos/caffeinelabs/motoko/git/ref/tags/${TAG}" \ - -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" | \ + -H "Authorization: Bearer $GH_TOKEN" | \ python3 -c "import sys,json; d=json.load(sys.stdin); print(d['object']['sha'], d['object']['type'])") OBJ_SHA=$(echo "$RESULT" | awk '{print $1}') OBJ_TYPE=$(echo "$RESULT" | awk '{print $2}') if [ "$OBJ_TYPE" = "tag" ]; then COMMIT=$(curl -sf "https://api.github.com/repos/caffeinelabs/motoko/git/tags/${OBJ_SHA}" \ - -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" | \ + -H "Authorization: Bearer $GH_TOKEN" | \ python3 -c "import sys,json; print(json.load(sys.stdin)['object']['sha'])") else COMMIT="$OBJ_SHA" fi echo "commit=$COMMIT" >> $GITHUB_OUTPUT + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} - name: Fetch upstream files and build diff if: steps.check.outputs.needed == 'true' @@ -116,13 +122,12 @@ jobs: fi done - - name: Create sync PR if: steps.check.outputs.needed == 'true' run: | BRANCH="${{ steps.check.outputs.branch }}" - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" + git config user.name "pr-automation-bot-public[bot]" + git config user.email "pr-automation-bot-public[bot]@users.noreply.github.com" git checkout -b "$BRANCH" git commit --allow-empty -m "chore: upstream sync check — caffeinelabs/motoko ${{ steps.latest.outputs.tag }}" git push -u origin "$BRANCH" @@ -130,25 +135,29 @@ jobs: --title "chore: sync check — caffeinelabs/motoko ${{ steps.latest.outputs.tag }}" \ --body-file /tmp/pr-body.md env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ steps.app-token.outputs.token }} check-mops: name: Check caffeinelabs/mops runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write steps: - uses: actions/checkout@v4 + - name: Create GitHub App Token + uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3 + id: app-token + with: + app-id: ${{ vars.PR_AUTOMATION_BOT_PUBLIC_APP_ID }} + private-key: ${{ secrets.PR_AUTOMATION_BOT_PUBLIC_PRIVATE_KEY }} + - name: Get latest mops release tag id: latest run: | TAG=$(gh release list --repo caffeinelabs/mops --limit 100 --json tagName --jq '[.[] | select(.tagName | startswith("cli-"))] | first | .tagName') echo "tag=$TAG" >> $GITHUB_OUTPUT env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ steps.app-token.outputs.token }} - name: Get current pinned tag id: current @@ -180,18 +189,20 @@ jobs: run: | TAG="${{ steps.latest.outputs.tag }}" RESULT=$(curl -sf "https://api.github.com/repos/caffeinelabs/mops/git/ref/tags/${TAG}" \ - -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" | \ + -H "Authorization: Bearer $GH_TOKEN" | \ python3 -c "import sys,json; d=json.load(sys.stdin); print(d['object']['sha'], d['object']['type'])") OBJ_SHA=$(echo "$RESULT" | awk '{print $1}') OBJ_TYPE=$(echo "$RESULT" | awk '{print $2}') if [ "$OBJ_TYPE" = "tag" ]; then COMMIT=$(curl -sf "https://api.github.com/repos/caffeinelabs/mops/git/tags/${OBJ_SHA}" \ - -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" | \ + -H "Authorization: Bearer $GH_TOKEN" | \ python3 -c "import sys,json; print(json.load(sys.stdin)['object']['sha'])") else COMMIT="$OBJ_SHA" fi echo "commit=$COMMIT" >> $GITHUB_OUTPUT + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} - name: Fetch upstream file and build diff if: steps.check.outputs.needed == 'true' @@ -234,8 +245,8 @@ jobs: if: steps.check.outputs.needed == 'true' run: | BRANCH="${{ steps.check.outputs.branch }}" - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" + git config user.name "pr-automation-bot-public[bot]" + git config user.email "pr-automation-bot-public[bot]@users.noreply.github.com" git checkout -b "$BRANCH" git commit --allow-empty -m "chore: upstream sync check — caffeinelabs/mops ${{ steps.latest.outputs.tag }}" git push -u origin "$BRANCH" @@ -243,4 +254,4 @@ jobs: --title "chore: sync check — caffeinelabs/mops ${{ steps.latest.outputs.tag }}" \ --body-file /tmp/pr-body.md env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ steps.app-token.outputs.token }} From 8f22a79f1a036dba318dce8842e029eebb525452 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 7 May 2026 16:56:47 +0200 Subject: [PATCH 11/11] fix(sync-workflow): use client-id (not deprecated app-id) and add BOT_APPROVED_FILES policy - Switch actions/create-github-app-token from app-id/APP_ID to client-id/CLIENT_ID (app-id is deprecated in v3.1.1+) - Pin actions/create-github-app-token to v3.1.1 and actions/checkout to v4.3.1 (aligned with developer-docs sync-motoko.yml) - Add .github/repo_policies/BOT_APPROVED_FILES listing all skill dirs that the sync workflow may modify via automated PRs --- .github/repo_policies/BOT_APPROVED_FILES | 7 +++++++ .github/workflows/sync-upstream.yml | 12 ++++++------ 2 files changed, 13 insertions(+), 6 deletions(-) create mode 100644 .github/repo_policies/BOT_APPROVED_FILES diff --git a/.github/repo_policies/BOT_APPROVED_FILES b/.github/repo_policies/BOT_APPROVED_FILES new file mode 100644 index 0000000..822d871 --- /dev/null +++ b/.github/repo_policies/BOT_APPROVED_FILES @@ -0,0 +1,7 @@ +# Files the pr-automation-bot-public bot is allowed to change via automated PRs. + +# sync-upstream: opens sync-check PRs when upstream repos release new versions +skills/motoko/* +skills/migrating-motoko/* +skills/migrating-motoko-enhanced/* +skills/mops-cli/* diff --git a/.github/workflows/sync-upstream.yml b/.github/workflows/sync-upstream.yml index 185f28a..cfcf909 100644 --- a/.github/workflows/sync-upstream.yml +++ b/.github/workflows/sync-upstream.yml @@ -11,13 +11,13 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: Create GitHub App Token - uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3 + uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 id: app-token with: - app-id: ${{ vars.PR_AUTOMATION_BOT_PUBLIC_APP_ID }} + client-id: ${{ vars.PR_AUTOMATION_BOT_PUBLIC_CLIENT_ID }} private-key: ${{ secrets.PR_AUTOMATION_BOT_PUBLIC_PRIVATE_KEY }} - name: Get latest motoko release tag @@ -142,13 +142,13 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: Create GitHub App Token - uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3 + uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 id: app-token with: - app-id: ${{ vars.PR_AUTOMATION_BOT_PUBLIC_APP_ID }} + client-id: ${{ vars.PR_AUTOMATION_BOT_PUBLIC_CLIENT_ID }} private-key: ${{ secrets.PR_AUTOMATION_BOT_PUBLIC_PRIVATE_KEY }} - name: Get latest mops release tag