diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index c5f195a..4c1c728 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -69,6 +69,74 @@ 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 **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 + +`.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 - **Write for agents, not humans.** Be explicit with canister IDs, function signatures, and error messages. @@ -78,7 +146,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/.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/_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' ' ') diff --git a/.github/workflows/sync-upstream.yml b/.github/workflows/sync-upstream.yml new file mode 100644 index 0000000..cfcf909 --- /dev/null +++ b/.github/workflows/sync-upstream.yml @@ -0,0 +1,257 @@ +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 + + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + + - name: Create GitHub App Token + uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 + id: app-token + with: + client-id: ${{ vars.PR_AUTOMATION_BOT_PUBLIC_CLIENT_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: ${{ steps.app-token.outputs.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 $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 $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' + 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 + + 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 skills/${local_name}/SKILL.md /tmp/upstream-${upstream_name}.md || true) + if [ -n "$DIFF" ]; then + { + 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 + + - name: Create sync PR + if: steps.check.outputs.needed == 'true' + run: | + BRANCH="${{ steps.check.outputs.branch }}" + 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" + gh pr create \ + --title "chore: sync check — caffeinelabs/motoko ${{ steps.latest.outputs.tag }}" \ + --body-file /tmp/pr-body.md + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + + check-mops: + name: Check caffeinelabs/mops + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + + - name: Create GitHub App Token + uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 + id: app-token + with: + client-id: ${{ vars.PR_AUTOMATION_BOT_PUBLIC_CLIENT_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: ${{ steps.app-token.outputs.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 $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 $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' + 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 skills/mops-cli/SKILL.md /tmp/upstream-mops-cli.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 "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" + gh pr create \ + --title "chore: sync check — caffeinelabs/mops ${{ steps.latest.outputs.tag }}" \ + --body-file /tmp/pr-body.md + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} 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/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/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/skills/migrating-motoko-enhanced/SKILL.md b/skills/migrating-motoko-enhanced/SKILL.md new file mode 100644 index 0000000..cf2a36c --- /dev/null +++ b/skills/migrating-motoko-enhanced/SKILL.md @@ -0,0 +1,295 @@ +--- +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..2d630a6 --- /dev/null +++ b/skills/migrating-motoko/SKILL.md @@ -0,0 +1,199 @@ +--- +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..5b94a9a --- /dev/null +++ b/skills/mops-cli/SKILL.md @@ -0,0 +1,244 @@ +--- +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..2893ec3 100644 --- a/skills/motoko/SKILL.md +++ b/skills/motoko/SKILL.md @@ -1,366 +1,363 @@ --- 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: [mops.one/core](https://mops.one/core). -### 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**: [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/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; +``` 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 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',