Skip to content

upstream sync available — caffeinelabs/skills 6173cbc → eeb232a #353

Description

@pr-automation-bot-public

Upstream diff: caffeinelabs/skills 6173cbceeb232a

Commit: eeb232ac0b18

To sync: create branch chore/sync-upstream-skills-eeb232a, follow the
Upstream Sync Strategy
in CLAUDE.md, run npm run validate, and open a PR that closes this issue.

Before applying: check .claude/upstream.md for icskills-owned sections.
Do NOT overwrite those sections from upstream. Also check whether any owned
section is now covered by the upstream changes — if so, drop the icskills copy
and remove it from the owned list to avoid duplicating content.

writing-motoko ← upstream writing-motoko

SKILL.md

Show diff (- old upstream, + new upstream)
6c6
< version: 0.1.5
---
> version: 0.1.6
32c32,33
< - Any Motoko reserved keyword as a declared identifier -- Before writing, check parameter, variable, function, type, field, and label names against Motoko's reserved words. `query` and `label` are reserved and must never be identifiers. Rename a colliding domain term instead of relying on its position or inferred meaning.
---
> - Any Motoko reserved keyword as a declared identifier -- Before writing, check parameter, variable, function, type, field, and label names against the full list in [references/reserved-keywords.md](references/reserved-keywords.md). `query` and `label` are reserved and must never be identifiers. Rename a colliding domain term instead of relying on its position or inferred meaning.
> - Type annotations on an inline `func` passed as a **call argument** -- write `xs.filter(func x = x > 1)`, not `xs.filter(func(x : Nat) : Bool { x > 1 })`. The call supplies the types. If a generic cannot be inferred, instantiate the call (`map<In, Out>`), never the lambda. This applies only in argument position — named declarations still carry full signatures. **One exception:** keep `: async ()` on an async callback (`func() : async () { ... }`) — it is what makes the body async, and removing it fails with M0096
54c55,56
< - Never hand-edit `mops.toml` or `mops.lock`; use the `mops` CLI so dependency metadata and the lockfile stay atomic.
---
> - Never hand-edit dependency entries in `mops.toml`, and never touch `mops.lock`; use the `mops` CLI so dependency metadata and the lockfile stay atomic. (`[toolchain]` has a CLI too: `mops toolchain use <tool> [version]`.)
> - Leave `[moc] args` alone. Compiler flags are a one-time project-setup concern, and many platforms own `mops.toml` and set them for you — do not inspect or change them while writing code. If you are setting up a project yourself, see [references/project-setup.md](references/project-setup.md).
65c67
< - **`mops check --fix`** (fast — use for iteration) — Auto-fixes warnings (dot-notation, redundant type instantiation, redundant implicit arguments) and reports remaining compile errors. Exit 0 = success. Error format: `file:startLine.startCol-endLine.endCol: severity [code], message`. Iterate on this until it passes.
---
> - **`mops check --fix`** (fast — use for iteration) — Reports compile errors and auto-fixes the style warnings, where the project has them enabled (dot-notation, redundant type instantiation, redundant implicit arguments). Follow this skill's rules whether or not `--fix` enforces them. Exit 0 = success. Error format: `file:startLine.startCol-endLine.endCol: severity [code], message`. Iterate on this until it passes.
94c96,97
< // Two-arg functions are NOT dot notation
---
> // Equality: Principal declares `equal` with a self parameter, so it is dot notation too
> a.equal(b) // PREFERRED
96d98
< a == b // also OK
99a102,105
> **Prefer `equal` / `compare` over `==`.** `==` is compiler-generated structural equality and exists only for **shared** types, so one `var` field takes a record out of shared and `==` stops compiling (M0060). Use `==` only for the numeric primitives that have no receiver form: `Nat`, `Int`, `Float`, and the sized int types declare `equal(x, y)` without a `self` parameter, so `myNat.equal(other)` fails with M0070 and `a == b` is the right call. Other receiver methods on those types (`myNat.toText()`) are fine.
> 
> Your own records and variants get nothing derived — a record `compare` must be an explicit function, and custom variants need both `equal` and `compare` written out. See [references/equality.md](references/equality.md).
> 
191c197
< let user = users.find(func(u : User) : Bool { u.id == caller })
---
> let user = users.find(func u = u.id == caller)
221c227,229
< Compiler infers comparison functions automatically:
---
> `Map` and `Set` operations take the comparison function as an **implicit** argument. `Map.empty()` itself takes no arguments — the comparator is resolved at the operations that need it (`add`, `get`, `remove`, …), not at construction.
> 
> Inference works by finding a `compare` in the module imported **for the key type**. So the import is what makes it work:
223a232,234
> import Map "mo:core/Map";
> import Nat "mo:core/Nat"; // this import is what supplies Nat.compare
> 
225c236,251
< map.add(5, "hello"); // Nat.compare inferred
---
> map.add(5, "hello"); // compare resolved from the imported Nat
> ```
> 
> Without `import Nat`, the same code fails — the type is known, but there is no module to take `compare` from:
> 
> ```text
> type error [M0230], Cannot determine implicit argument `compare` of type (Nat, Nat) -> Order
> note: Did you mean to import mo:core/Int or mo:core/Nat?
> ```
> 
> Do **not** pass the comparator explicitly when it can be inferred; that is M0237, which `mops check --fix` removes:
> 
> ```motoko
> ages.add("Alice", 30);               // CORRECT
> ages.add(Text.compare, "Alice", 30); // WRONG (M0237)
> ```
227,228c253
< let ages = Map.empty<Text, Nat>();
< ages.add(Text.compare, "Alice", 30); // explicit when needed
---
> A custom key type works the same way — give its module a `compare` and it is inferred:
230c255
< // Define module with compare for custom types → auto-inferred
---
> ```motoko
236d260
< 
238a263,264
> Type instantiation on `empty()` follows the usual rule — needed only when the binding is unannotated. `let m : Map.Map<Nat, Text> = Map.empty();` infers it, and `Map.empty<Nat, Text>()` there would be M0223.
> 
275a302,305
> ### The Actor Must Come Last
> 
> Imports and `type`/`let` declarations may precede the actor. Nothing may follow it — the actor is the file's result, so a trailing declaration makes the actor a non-`()` statement and fails with M0096 (`expression of type actor {...} cannot produce expected type ()`). Prefer keeping shared types in `types.mo` regardless.
> 
294c324
<   { self with likedBy = Set.toArray(self.likedBy) };
---
>   { self with likedBy = self.likedBy.toArray() };
342c372
< - If a value is an array (`[T]`) or came from `.toArray()` / `.filter(...)`, then `.map(...)` already returns an array; do not append `.toArray()` to that array-map result. (`List.List<T>.map(...)` may still need explicit type instantiation and `.toArray()` when mapping records to another type.)
---
> - If a value is an array (`[T]`) or came from `.toArray()` / `.filter(...)`, then `.map(...)` already returns an array; do not append `.toArray()` to that array-map result. (`List.List<T>.map(...)` returns a `List`, so it still needs `.toArray()` when the caller expects an array.)
348,350c378
< let matched = leftTags.any(func(left : Text) : Bool {
<   rightTags.any(func(right : Text) : Bool { left == right })
< });
---
> let matched = leftTags.any(func left = rightTags.any(func right = left == right));
358c386,412
< When a predicate uses a block body, declare the return type as `: Bool`. Do not write `func(todo : Types.Todo) { todo.id == id }`; that block can compile as a `()`-returning callback in argument position. Use `func(todo : Types.Todo) : Bool { todo.id == id }`.
---
> **An inline `func` passed as a call argument takes no type annotations.** The call already fixes the parameter and result types, so annotating repeats them and lets them drift as the code changes. Use the expression form `func x = <expr>`:
> 
> ```motoko
> todos.find(func todo = todo.id == targetId);            // CORRECT
> todos.find(func(todo : Types.Todo) : Bool { todo.id == targetId }); // WRONG: annotated
> ```
> 
> This is about **argument position only**. A named declaration still carries its full signature, and a lambda bound on its own has nothing to infer from — `let f = func x = x > 1` fails with M0103 (`cannot infer type of variable`).
> 
> ```motoko
> public func toView(t : Types.Todo) : Types.TodoView { ... }; // annotated, as always
> ```
> 
> When the types are not obvious to a reader, or a generic cannot be inferred, say it **on the call** rather than on the lambda — it reads better and keeps one source of truth:
> 
> ```motoko
> photos.map(func p = { id = p.id; url = p.url.toText() }); // inferred — preferred
> photos.map<PhotoInternal, Photo>(func p = { ... });       // when M0098 demands it
> ```
> 
> Add `<In, Out>` only when the compiler actually reports M0098; adding it when inference already succeeded is M0223 (redundant type instantiation).
> 
> The one exception is a callback that must return `async`. There `: async ()` is load-bearing — it is what makes the body async, and there is no unannotated form (`func() = async { ... }` does not work either). Without it the lambda infers `() -> ()` and the call fails with M0096:
> 
> ```motoko
> Timer.recurringTimer<system>(#seconds(3600), func() : async () { cleanup() });
> ```
362c416
< switch (todos.find(func(todo : Types.Todo) : Bool { todo.id == targetId })) {
---
> switch (todos.find(func todo = todo.id == targetId)) {
408c462
< switch (numbers.find(func(n : Nat) : Bool { n > 5 })) {
---
> switch (numbers.find(func n = n > 5)) {
421c475
< let sorted = all.sort(func(a : Types.Todo, b : Types.Todo) : { #less; #equal; #greater } {
---
> let sorted = all.sort(func (a, b) =
425,427c479,481
< });
< sorted.map<Types.Todo, Types.TodoView>(func(todo) {
<   { id = todo.id; text = todo.text; completed = todo.completed; createdAt = todo.createdAt }
---
> );
> sorted.map(func todo = {
>   id = todo.id; text = todo.text; completed = todo.completed; createdAt = todo.createdAt
431,432c485,486
< all.sort(func(a, b) { Int.compare(b.createdAt, a.createdAt) });
< all.map<Types.Todo, Types.TodoView>(func(todo) { ... });
---
> all.sort(func (a, b) = Int.compare(b.createdAt, a.createdAt));
> all.map(func todo = { ... });
442,444c496,498
< numbers.contains(3); // implicit Nat.equal
< friends.contains(Principal.equal, p); // explicit equality
< todos.find(func(todo : Types.Todo) : Bool { todo.id == targetId }); // returns ?Todo
---
> numbers.contains(3); // equal inferred from the imported Nat
> friends.contains(p); // likewise from Principal — passing Principal.equal here is M0237
> todos.find(func todo = todo.id == targetId); // returns ?Todo
457a512,531
> ### Joining Text
> 
> `join` takes the **iterator as its receiver and the separator as its argument** — easy to invert. Use dot notation; the module form is an M0236 violation that `mops check --fix` rewrites for you.
> 
> ```motoko
> ["a", "b"].values().join(", ");     // CORRECT → "a, b"
> Text.join(["a", "b"].values(), ", "); // WRONG (M0236)
> ```
> 
> Note the receiver is an **iterator**, not an array: call `.values()` on an array first.
> 
> ### Variant Tag Arguments
> 
> Always parenthesize a variant tag's argument. A tag binds only to the atom immediately after it, tighter than any operator, so an unparenthesized argument silently loses everything past the first term:
> 
> ```motoko
> #tag(n + 1) // CORRECT
> #tag n + 1  // WRONG: parses as (#tag n) + 1 → M0060, operator is not defined for operand types
> ```
> 
460c534
< When `.map()` transforms to a **different** type, provide type parameters explicitly (M0098 without):
---
> Let inference work first. With unannotated lambdas the compiler resolves `.map()` to a different type on its own, so write the plain call:
463,464c537,538
< let photos = internalPhotos.map<PhotoInternal, Photo>(
<   func(p) { { id = p.id; url = p.url; uploadedBy = p.uploadedBy.toText() } }
---
> let photos = internalPhotos.map(
>   func p = { id = p.id; url = p.url; uploadedBy = p.uploadedBy.toText() }
465a540
> ```
466a542,545
> Add explicit type parameters **only** when the compiler reports M0098 (`no best choice for type parameter`):
> 
> ```motoko
> let photos = internalPhotos.map<PhotoInternal, Photo>(func p = { ... });
468a548,549
> Adding them when inference already succeeded is a warning of its own — M0223, redundant type instantiation — which `mops check --fix` strips. Annotating the lambda instead of instantiating the call is always wrong.
> 
524c605
< let user = users.find(func(u : User) : Bool { u.id == caller })
---
> let user = users.find(func u = u.id == caller)
528c609
< let label = optLabel ?? "(untitled)";
---
> let caption = optLabel ?? "(untitled)"; // not `label` — reserved word
532c613
<   users.find(func(u : User) : Bool { u.name == name });
---
>   users.find(func u = u.name == name);
536c617
< switch (todos.find(func(todo : Types.Todo) : Bool { todo.id == targetId })) {
---
> switch (todos.find(func todo = todo.id == targetId)) {
670,671c751
< | `unexpected token '<name>'` at an identifier declaration | Reserved word used as an identifier | Rename the identifier consistently across its contract and callers |
< | `unexpected token 'break'`                             | `break` reserved             | Use helper function with early return       |
---
> | `unexpected token '<name>'` at an identifier declaration | Reserved word used as an identifier | Rename it consistently across its contract and callers; see [references/reserved-keywords.md](references/reserved-keywords.md) |
672a753,762
> | `M0219` implicitly transient                           | Actor not persistent         | Write `persistent actor`; see [references/project-setup.md](references/project-setup.md) |
> | `M0220` actor should be declared `persistent`          | Actor not persistent         | Write `persistent actor`; see [references/project-setup.md](references/project-setup.md) |
> | `M0218` redundant `stable` keyword                     | `stable` under EOP           | Remove `stable` — a plain `let`/`var` is already stable |
> | `M0064` misplaced `'!'`                                | `!` outside an option block  | Wrap in `do ? { ... }`                      |
> | `M0145` `does not cover value`                         | Non-exhaustive switch        | Add the missing cases or a `case _`         |
> | `M0060` operator not defined for `{#tag : T}`          | Unparenthesized variant tag  | `#tag(x)`, never `#tag x`                   |
> | `M0060` operator not defined, on `==`                  | `==` on a record with a `var` field (not shared) | Use an `equal` function instead |
> | `M0230` cannot determine implicit argument `compare`   | Record/variant key with no findable `compare` | Add `compare` to the type's module; or `import` the module for a primitive key |
> | `M0070` expected object type, produces `Nat`           | Receiver `.equal`/`.compare` on a number | Use `==` or `Nat.equal(a, b)`   |
> | `M0096` actor cannot produce expected type `()`        | Declaration after the actor  | The actor must be the last declaration in the file |
707a798
> 11. Inline `func` arguments carry no type annotations (except `: async ()` on async callbacks); instantiate the call instead, and only when the compiler reports M0098
711c802,804
< - **Control flow**: [references/control-flow.md](references/control-flow.md) — `??`, switch statements, loops, `break` / `continue`
---
> - **Control flow**: [references/control-flow.md](references/control-flow.md) — `??`, `do ? { ... }` option chaining, switch statements, loops, `break` / `continue`
> - **Reserved keywords**: [references/reserved-keywords.md](references/reserved-keywords.md) — full list to check identifiers against
> - **Equality & comparison**: [references/equality.md](references/equality.md) — which types support receiver `.equal`, and when `==` differs from `equal`
712a806
> - **Project setup**: [references/project-setup.md](references/project-setup.md) — one-time `[moc] args` flags. Skip this if your platform manages `mops.toml`

examples.md

Show diff (- old upstream, + new upstream)
390,391c390,391
<       users = List.empty<User>();
<       posts = List.empty<Post>();
---
>       users = List.empty();
>       posts = List.empty();
731c731
<       todos = List.empty<TodoItem>();
---
>       todos = List.empty();

references/control-flow.md

Show diff (- old upstream, + new upstream)
3c3
< Reference for Motoko control flow patterns. Load when you need `??`, switch, or loop syntax.
---
> Reference for Motoko control flow patterns. Load when you need `??`, `do ?`, switch, or loop syntax.
28a29,52
> 
> ## Option Chaining (`do ? { ... }`)
> 
> Inside a `do ? { ... }` block, postfix `!` unwraps an option. If any `!` hits `null`, the **whole block** short-circuits to `null`. Use it when several lookups must all succeed and you want one combined result — `??` only handles a single option at a time.
> 
> ```motoko
> // Every step must succeed; any null makes cityOf return null
> func cityOf(id : Text) : ?Text {
>   do ? { users.get(id)!.city! }
> };
> 
> // `!` composes with ordinary computation inside the block
> func nameAndCity(id : Text) : ?Text {
>   do ? { nick.get(id)! # " of " # cityOf(id)! }
> };
> ```
> 
> `!` is only legal inside `do ? { ... }`. Using it anywhere else is an error:
> 
> ```text
> type error [M0064], misplaced '!' (no enclosing 'do ? { ... }' expression)
> ```
> 
> Reach for `do ?` over nested `switch`es when unwrapping several options; reach for `??` when there is one option and a default.

references/equality.md

Show diff (- old upstream, + new upstream)
0a1,92
> # Equality and Comparison
> 
> Load when choosing between `==`, `equal`, and `compare`, or when a `.equal(...)` call fails with M0070.
> 
> ## `==` vs `equal`
> 
> **Default to `equal` / `compare`. Use `==` only for the primitives that have no receiver form** — `Nat`, `Int`, `Float`, and the sized int types, where `a == b` is shorter than `Nat.equal(a, b)` and needs no import.
> 
> `==` is compiler-generated structural equality, and it only exists for **shared** types. That is the catch: a single `var` field takes a record out of shared, and `==` stops compiling:
> 
> ```motoko
> type Todo = { id : Nat; var completed : Bool };
> a == b
> // type error [M0060], operator is not defined for operand types
> ```
> 
> Internal state records routinely have `var` fields, so code written around `==` breaks the moment a field becomes mutable. `equal`/`compare` functions do not have that failure mode, which is why they are the default even where `==` would work today.
> 
> `equal` and `compare` are also what **collections** take, as implicit arguments: `Map`, `Set`, `contains`, and the collection-level `equal`/`compare` helpers. The compiler infers them, so you rarely name them:
> 
> ```motoko
> let map = Map.empty<Nat, Text>(); // compare resolved at add/get, from the imported Nat
> numbers.contains(3);              // likewise Nat.equal
> ```
> 
> Inference finds `equal`/`compare` in the module imported for the element type, so the import is what makes it work — without `import Nat`, you get M0230. Passing the module's own function explicitly is redundant and warns:
> 
> ```motoko
> friends.contains(p);                  // CORRECT
> friends.contains(Principal.equal, p); // WRONG (M0237)
> ```
> 
> Name a function only when you want **different** behaviour from the module default — a case-insensitive match, a reversed order:
> 
> ```motoko
> names.contains(func (x, y) = x.toLower() == y.toLower(), q);
> ```
> 
> ## Which types allow receiver `.equal` / `.compare`
> 
> The general dot-notation rule applies: a receiver call works only when the module declares the function with a `self` parameter. `Text.equal` is `(self : Text, other : Text)`, so `a.equal(b)` resolves. `Nat.equal` is `(x : Nat, y : Nat)` — no `self`, so it does **not**.
> 
> | Type | `a.equal(b)` | `a.compare(b)` | Module form |
> |---|---|---|---|
> | `Text`, `Principal`, `Bool`, `Char`, `Blob` | yes | yes | also fine |
> | `Order` | yes | **no such function** | `Order.equal(a, b)` only |
> | `Nat`, `Int`, `Float` | **no** | **no** | `Nat.equal(a, b)` |
> | `Nat8`…`Nat64`, `Int8`…`Int64` | **no** | **no** | `Nat64.equal(a, b)` |
> 
> `mo:core/Order` has no `compare` in any form — `Order.compare(a, b)` is M0072 (`field compare does not exist`), not just a missing receiver.
> 
> Calling the receiver form on a numeric type fails:
> 
> ```motoko
> myNat.equal(other)   // WRONG
> // type error [M0070], expected object type, but expression produces type Nat
> 
> Nat.equal(myNat, other)  // CORRECT — or just: myNat == other
> ```
> 
> This is the one place where "always use dot notation" does not hold, and it is worth remembering: numeric types **do** support other receiver methods (`myNat.toText()` is correct), just not `equal` and `compare`.
> 
> ## Your own records, tuples, and variants
> 
> Nothing is derived for them. A record or variant used as a `Map`/`Set` key needs a `compare` the compiler can find, or you get:
> 
> ```text
> type error [M0230], Cannot determine implicit argument `compare`
> ```
> 
> **Record `compare` must be an explicit function.** There is no sensible default — which field dominates, and in what direction, is a decision only you can make. Write it out and be deliberate about the tie-breaking:
> 
> ```motoko
> module Point {
>   public func compare(a : Point, b : Point) : Order.Order {
>     switch (Nat.compare(a.x, b.x)) {
>       case (#equal) { Nat.compare(a.y, b.y) }; // x first, then y
>       case other { other };
>     }
>   };
> };
> ```
> 
> Put it in the module named after the type and inference will find it, exactly as it finds `Nat.compare`.
> 
> **Custom variants need both `equal` and `compare` written out.** `mo:core` types are the exception — `Result` already ships them:
> 
> ```motoko
> a.equal(b, Nat.equal, Text.equal); // Result.equal, sub-functions for Ok and Err
> ```
> 
> For a one-off equality check on an immutable record, tuple, or variant, `==` still works and is fine — the guidance above is about the functions collections need, and about not building on `==` for types whose fields may become `var`.

references/project-setup.md

Show diff (- old upstream, + new upstream)
0a1,39
> # Project Setup: Compiler Flags
> 
> **Read this only when you are setting up a Motoko project yourself.** These are one-time `mops.toml` settings, not something to revisit while writing code.
> 
> **If your platform manages `mops.toml` for you, skip this file entirely** — do not inspect, add, or change `[moc] args`. The flags below are already set on your behalf, and editing them is not yours to do. This is the normal case for hosted platforms; it is only self-managed projects that need anything here.
> 
> ## Persistence
> 
> Enhanced orthogonal persistence is `moc`'s default, so a top-level `let`/`var` in an actor is stable without the `stable` keyword. What is **not** default is letting a plain `actor { ... }` be persistent:
> 
> ```toml
> [moc]
> args = ["--default-persistent-actors"]
> ```
> 
> Every actor example in this skill assumes that flag. Without it a plain `actor` fails to compile, with one of two errors depending on whether it holds state:
> 
> ```text
> // actor { var count = 0 }
> type error [M0219], this declaration is currently implicitly transient, please declare it explicitly `transient`
> 
> // actor { public func f() : async Nat { 1 } }  — no stable declaration to complain about
> type error [M0220], this actor or actor class should be declared `persistent`
> ```
> 
> If you cannot set the flag, write `persistent actor { ... }` instead — same semantics, declared per actor. (`persistent` is transitional; actors become persistent-by-default in a future major `moc` release.) `--default-persistent-actors` is not listed in `moc --help`, but it is supported.
> 
> ## Style warnings
> 
> The style rules this skill enforces are compiler warnings that are **off by default**. Enabling them makes `moc` flag violations for you, and `mops check --fix` then auto-corrects all three:
> 
> ```toml
> [moc]
> args = ["--default-persistent-actors", "-W", "M0236,M0237,M0223"]
> ```
> 
> `M0236` non-dot-notation calls, `M0237` redundant explicit implicit arguments, `M0223` redundant type instantiation.
> 
> Without `-W` in `[moc] args` these never fire, so `mops check --fix` has nothing to correct — the rules still hold, you just have to follow them unaided.

references/reserved-keywords.md

Show diff (- old upstream, + new upstream)
0a1,36
> # Reserved Keywords
> 
> Motoko's reserved words. **None** of these may be used as an identifier — not as a variable, parameter, function, type, field, or label name. Check a name against this list before declaring it, especially when a domain term happens to collide (`query`, `label`, `system`, `object`, `class`, `type`, and `in` are the ones that bite most often).
> 
> ```text
> actor        and          assert       async        await
> break        case         catch        class        composite
> continue     debug        debug_show   do           else
> false        finally      flexible     for          from_candid
> func         if           ignore       implicit     import
> in           include      label        let          loop
> mixin        module       not          null         object
> or           persistent   private      public       query
> return       shared       stable       switch       system
> throw        to_candid    transient    true         try
> type         var          while        with
> ```
> 
> `async*`, `await*`, and `await?` are also reserved. They cannot collide with an identifier anyway, since `*` and `?` are not identifier characters.
> 
> Using a reserved word as an identifier is a parse error at the declaration:
> 
> ```text
> syntax error [M0001], unexpected token '<name>', expected one of token or <phrase> sequence: ...
> ```
> 
> Rename the colliding term rather than relying on position or inferred meaning — there is no escaping or quoting mechanism. Conventional renames: `query` → `request` / `searchTerm`, `label` → `caption` / `tag`, `type` → `kind` / `category`, `object` → `item` / `entity`, `class` → `group` / `kind`.
> 
> ## Not reserved
> 
> These read like keywords but are ordinary identifiers in Motoko — several are Candid keywords rather than Motoko ones, which is the usual source of confusion:
> 
> ```text
> blob    bool    char    int     nat     opt     record  service
> struct  variant vec     enum    match   state   result  status
> ```

migrating-motoko-actors — no changes

troubleshooting-motoko-migrations — no changes

Metadata

Metadata

Assignees

No one assigned

    Labels

    upstream-skillsUpstream sync: caffeinelabs/skills

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions