算法艺术创作
algorithmic-art
通过代码和可调参数创作生成艺术与交互式视觉效果。
reviewing-motoko【设计与创意】Design review for Motoko backends — invariants encoded in types, enhanced migration state discipline, and multi-file structure. Load when reviewing, auditing, or refactoring existing .mo files rather than writing new ones.
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
This skill judges whether Motoko code is well designed. It does not teach the language — load writing-motoko for syntax, mo:core APIs, and mechanical rules, and migrating-motoko-actors before changing anything that is stable state.
Four questions decide a review, in this order — the first three because each can invalidate the next, the fourth last because it is the least consequential:
types.mo / lib/ / mixins/ / main.mo is a contract about what may depend on what, not a folder convention.mops check --fix handles most of this; only what it cannot see is the reviewer's job.Run the mechanical pass first — do not spend review attention on what the compiler already reports.
mops check --fix
Its style auto-fixes (M0236 dot notation, M0237 redundant implicits, M0223 redundant instantiation) only fire where they are enabled: M0236 is on by default from moc 2, and the rest need -W in [moc] args. writing-motoko's references/project-setup.md has the setup. Where they are off, the Conventions findings below are all yours to make by hand.
Then read in this order — outside in, because a misplaced file makes every type inside it suspect:
mops.toml — is there a [canisters.*.migrations] chain? If not, finding B1 below outranks everything else.types.mo — every type declaration. This is where most findings are.main.mo, and what each include passes. See references/state-and-persistence.md.public shared / public query function: its parameter types, its return type, its authorization.Report findings with one of three levels. Do not inflate: a reviewer who calls everything a blocker gets ignored.
| Level | Meaning |
|---|---|
| Blocker | Can corrupt or silently lose state, or lets a caller reach an unauthorized action. |
| Warning | An invariant the type system could enforce is left to runtime, a comment, or a convention. |
| Note | Readability, duplication, or naming. Fix while you are in the file; do not gate on it. |
State the concrete failure, not the rule. "status : Text accepts "complete", and the three call sites spell it "completed"" beats "prefer variants over strings".
The rule: if a value has a fixed set of shapes, the type must have that set of shapes. Full recipes, including how to carry each change through a migration, are in references/type-encoding.md.
Text for a closed set of values — Warning, Blocker if it gates authorizationThe most common defect in generated Motoko. Every producer and consumer agrees on spelling by convention and the compiler checks nothing — no exhaustiveness, no error on a typo, no error when a new state is added and half the call sites are not updated.
public type Task = { id : Nat; var status : Text }; // WRONG
public type TaskStatus = { #open; #claimed : Principal; #done : { at : Time.Time } };
Variants are shared, so this reaches the frontend as a discriminated union too. Flag Text (or Nat, or Bool) whenever the code, a comment, or a doc lists the permitted values — == "..." comparisons, a switch on a Text with a case _, role names held as Text. Keep Text for genuinely open sets, and for strings an external system owns.
Two fields that are only valid together belong in one variant. A Bool plus an optional payload has four representable combinations and two meaningful ones.
// WRONG — { hasDeadline = true; deadline = null } compiles and means nothing
{ hasDeadline : Bool; deadline : ?Time.Time }
// RIGHT
{ deadline : { #none; #due : Time.Time } } // or simply `deadline : ?Time.Time`
If a ?T already says it, drop the Bool — do not introduce a variant that only restates ?T.
Use the type that matches the real count. A [T] that is always empty-or-one makes every caller write a loop or an if size == 1; a ?T tells them there is nothing to iterate.
Text — WarningResult<Ok, Text> forces every caller to string-match to tell one failure from another. Make Err a variant with the data each case needs. See the Result section of writing-motoko for the full rule, including when to return ?T and when to trap instead.
var fields leaking into the API — Blocker (it does not compile) or Warning (it compiles but over-shares)A record with a var field is not shared, so it cannot be returned from a public function. Where a project already has the internal/view split (Task internal, TaskView shared), check that:
Principal list, an audit trail, or another user's data is an information leak that compiles.case _ swallowing variants — WarningA catch-all arm is what turns "adding a variant tag is a compile error everywhere it matters" into "adding a variant tag silently takes the default branch". Keep case _ only where the arms are genuinely uniform and the set is expected to grow (e.g. mapping many tags to one label). Anywhere a new tag would need new behaviour, enumerate the tags.
type UserId = Principal is a documentation alias: any Principal still type-checks where a UserId is expected. That is fine, and usually the right trade. Just do not treat it as an enforced distinction — and if two different id types are genuinely being confused at call sites, a one-field record ({ userId : Principal }) or a variant tag (#user(Principal)) is what actually enforces it, at the cost of changing the Candid shape.
Under enhanced migration (EM) the actor's stable fields are declared type-only and every initial value comes from the migration chain. That separation is the point: it is what makes stable state impossible to initialize from something transient. Details in references/state-and-persistence.md.
No [canisters.<name>.migrations] chain in mops.toml means pre-EM persistence, where stable fields carry inline initializers — so a stable field can be initialized from a transient one:
transient let defaultQuota = 100;
let quota = defaultQuota; // stable; the initializer runs ONCE, on first install
Change defaultQuota to 250, deploy, and quota is still 100 — forever, no warning. Point at migrating-motoko-actors for the conversion; everything below assumes EM.
Under EM this is M0250 / M0014, so it will not compile — but it appears constantly in code copied from pre-EM examples and in half-finished refactors. The fix is never to add transient: move the initial value into the pending migration's NewActor.
transient misused — Warning both waysAsk of every actor and mixin field: if this is lost on upgrade, is the app still correct?
transient: caches, derived indexes, rate-limit counters, and anything rebuildable are being persisted. They then have to be migrated forever, and a stale cache survives the upgrade that was supposed to clear it.transient: durable domain data silently resets to its initializer on every upgrade. Look for transient on anything a user would expect to still be there after a deploy.var actor field shared with a mixin — Blockervar parameters pass by value, so the mixin mutates a copy and the actor's field never changes. Wrap it in a record and pass the record. Also flag a fresh record literal at the include site (include Api({ var n = state.n })) — each mixin then gets its own copy. See writing-motoko.
mixin block — BlockerEvery bare let/var at the top of a mixin is implicitly stable and traps at runtime with IC0503. State comes in as a parameter; constants go in a module; only transient is allowed inline.
mo:core/... — the chain replays forever, so a project import makes it wrong the moment that type changes. (caffeineai-lints catches this.)main.mo that no migration in the chain supplies (M0254 / M0267).An identity migration body is a Warning — the change was stable-compatible, so delete the file. Full table.
Layers, with dependencies pointing one way only: main.mo → mixins/ → lib/ → types.mo.
| File | Holds | Must not hold |
|---|---|---|
types.mo | Type declarations; small pure helpers on those types | State, endpoints, business rules |
lib/*.mo | Domain logic as stateless modules, state as parameters | Public endpoints, actor fields |
mixins/*.mo | Public endpoints: authorize, delegate, map to a view | Domain logic, stable state |
main.mo | State declarations and includes | Any public method, any logic |
migrations/ | Applied (frozen) history plus at most one pending module, one module per file | Project imports |
main.mo. Move it to a mixin..mo holding types, state, logic, and endpoints together.lib/.lib/ importing a mixin, or types.mo importing either.mixins/Bookings.mo), not by verb (mixins/Getters.mo).Most of this is caught in the last, bodies pass. The exception is M1, the authorization blocker — it is found during the API-boundary pass (step 5), not with the body smells below.
public shared function that does not check caller. Every state change and every read of another user's data needs authorization on the backend.Nat subtraction that is not provably non-negative at the operation. It traps.Result error variant.toView per type, in lib/ or types.mo.Least consequential of the four axes, and mops check --fix fixes most of it where the -W flags are on — so raise these only after the passes above, and only where the tooling cannot. writing-motoko has the rules; this is what to look for.
self — List.add(list, x), Principal.toText(p). M0236, auto-fixed.map.add(Text.compare, k, v)), or a type instantiation inference already resolved. M0237 / M0223, auto-fixed.func passed as a call argument — xs.filter(func(x : Nat) : Bool { x > 1 }). No diagnostic catches this, so it is the reviewer's job: the call already fixes both types, and the annotation duplicates them so they can drift. Write xs.filter(func x = x > 1). Instantiate the call (map<In, Out>) when M0098 demands it — never the lambda. One exception: : async () on an async callback is load-bearing on moc 1, so keep it while the code must build on both majors.class, or any object holding functions, used as actor state. Functions are not stable, so it fails with M0131 (declared stable but has non-stable type). Outside state a class still is not the idiom here — a module taking self plus dot notation gets the same call sites and stays stable.Design fixes in Motoko are not free: changing a type that is reachable from a stable actor field requires a migration, and the chain is one-way. Before proposing a type change, establish whether that type is stable state.
mops check --fix first, so the mechanical noise is gone.migrating-motoko-actors). For Text → variant, decide explicitly what an unrecognized string becomes rather than inventing an #unknown tag.mops check --fix until clean, then mops build once.Group findings by severity, blockers first. Per finding: file:line, the level, the concrete failure it permits, and the change. Suppress findings the compiler already reports.
Blocker src/backend/mixins/Tasks.mo:34 (M1)
`claimTask` never reads `caller` — any principal can claim any task.
Warning src/backend/types.mo:12 (T1)
`Task.status : Text` is a closed set, so the comparison at Tasks.mo:51
spells it "complete" and is never true. Stable state — needs a migration.
If nothing is wrong, say so plainly. Do not manufacture findings to fill a report.
writing-motoko — language mechanics, mo:core APIs, Result, dot notationmigrating-motoko-actors — writing the migration a type change requirestroubleshooting-motoko-migrations — compatibility diagnostics that do not match the source