motoko-core-code-improvements
Purpose & Scope
Use this skill after test pass status to raise readability and consistency without changing behavior.
This skill focuses on mechanical, semantics‑preserving improvements:
- Aggregate imports into sections (1) mo:core/... (2) other mo:*/... from mops or similar third‑party sources (3) local project modules; sort each section alphabetically per file
- Prefer dot‑notation where available in
mo:core - Clean up truly unused
importlines while respecting implicit needs created by dot‑notation - Remove redundant
returnin single‑expression functions - Use direct string‑to‑Blob assignment for constant ASCII strings where appropriate
Safety first:
- Run each improvement category independently; commit after each to isolate diffs
- Prefer scripted, reviewable changes; use audit checks provided below
- Rebuild after every category; run tests if present
AI Quick Checklist (Do Not Skip)
- Preconditions
- Project compiles on
mo:core(see Migration Skill). Keepmo:basearound only if still referenced; otherwise remove base dependency already. - Ensure consistent Motoko and dfx versions per migration skill (moc ≥ 1.3.0, dfx ≥ 0.31).
- Order of improvements (recommended)
- A. Remove
returnin single‑expression functions - B. Convert to dot‑notation where available — see Motoko Dot‑Notation Migration Skill (
skills/dot-notation-migration/SKILL.md) - C. Ensure necessary
mo:coreimports for dot‑notation — see Motoko Dot‑Notation Migration Skill (import mapping) - D. Clean up unused imports (be conservative re: dot‑notation)
- E. Shorten local (sibling) import paths (drop the
./prefix where applicable) - F. Aggregate imports into three sections and sort each section alphabetically per file: (1)
mo:core/..., (2) othermo:*/...from mops/third‑party, (3) local project modules - G. Use direct string‑to‑Blob assignment for constant ASCII strings where appropriate
- Verify after each step
- Build all canisters or packages
- Grep/audit with provided commands
- Keep diffs minimal and readable
Acceptance Criteria
- No compiler errors or warnings introduced by the changes (esp. missing imports)
- No behavior changes; public interfaces unchanged unless stylistic
- Imports are aggregated into three sections — (1)
mo:core/..., (2) othermo:*/...from mops/third‑party, (3) local project modules — and each section is alphabetized; no truly unusedimports remain - Dot‑notation is consistently used where directly supported
- Constant Text strings are assigned directly to
Blobwithout redundantText.encodeUtf8calls
A) Remove return in single‑expression functions
Pattern
// Before
func f(x : T) : U { return <expr>; };
// After
func f(x : T) : U { <expr> };
Notes
- Only apply when the function body consists of a single
return <expr>;statement. - Do not transform multi‑statement bodies or bodies that include
try,label,switch, orawaitleading to different control flow. - A function with multiple
returnstatements (e.g., early returns inswitchcases likereturn null) must NOT have any returns removed. - Single non-terminal
return(early/conditional exit): if a function has exactly onereturnbut it is not the final statement of the body (e.g., an early return inside aniforswitchcase, followed by a fall-through final expression), the function has two distinct return paths. Do NOT remove the earlyreturn. Instead, add an explicitreturnto the final expression as well, so the function has tworeturnkeywords total — one per exit path. This makes both control-flow exits explicit and consistent.// Before — one explicit return, one implicit fall-through return func lookup(k : Key) : ?V { if (cache.contains(k)) { return cache.get(k) }; table.find(k) }; // After — both exit paths use `return` func lookup(k : Key) : ?V { if (cache.contains(k)) { return cache.get(k) }; return table.find(k); }; return switch (...) { ... }at the end of a function is OK. Each case block ends with the expression being returned (noreturnkeyword inside the cases). This is the preferred style when all cases produce values normally.// OK — all cases produce values, no traps or throws return switch (decode(data)) { case (#ok key) { (key.x, key.y) }; case (#err msg) { #err(msg) }; };- Exception: if any case has a return-equivalent statement (
Runtime.traporthrow) that represents a genuine function exit, pullreturninside the case blocks. Removereturnbeforeswitch, then addreturnonly in the case blocks that produce values. Cases withRuntime.traporthrowdo not needreturn. This makes it clear which branches return and which abort:// Avoid — trap is hidden inside return switch return switch (decode(data)) { case (#ok key) { (key.x, key.y) }; case (#err msg) { Runtime.trap(msg) }; }; // Prefer — return only in value-producing cases switch (decode(data)) { case (#ok key) { return (key.x, key.y) }; case (#err msg) { Runtime.trap(msg) }; }; - Unreachable traps are NOT return-equivalent. If
Runtime.trap("unreachable")is used, or a comment or message indicates the branch is only reachable through a bug, that trap is not a normal function exit — it's a defensive assertion. In that case,return switchis fine andreturnshould NOT be pulled inside:// OK — the trap just guards an impossible case return switch (Jacobi.fromNat(x, y, 1, curve)) { case (null) Runtime.trap("unreachable"); case (?point) point; };
Automation (example)
- Grep candidates:
grep -rn "func \\w\\+(.*) *:.*{ *return .*; *};" . --include="*.mo" | grep -v \.mops - Review matches; then apply with your editor or a scripted replacement.
Script Safety Requirements (learned from real migration)
- A simple line-based return counter is NOT sufficient. You must track function boundaries using brace-depth parsing at the character level.
- Nested functions: When a function body contains nested
funcdeclarations, skip the nested function's body entirely — only count returns at the direct (outermost) function scope. - Accurate function boundary detection: Use character-level scanning that handles string literals (skip
"..."including\"escapes), comments (//line comments and/* ... */block comments), and tracks brace depth to find the true closing}of each function. - Counting rule: Count every
returnanywhere inside the outer function body, including inside nested control-flow blocks such asswitch,if,for, andwhile, but excluding anyreturninside nestedfuncbodies. A function is safe to rewrite only if this total count is exactly 1 and that singlereturnis the terminal direct-body statement (only whitespace, comments, and an optional;may follow it before the closing}). If the singlereturnis an early/conditional exit, the function has an additional implicit return path via its fall-through final expression — leave both alone (the script will not remove it; manually add an explicitreturnto the fall-through expression per the style note above).
Battle-tested Python script
Save as remove_returns.py in the project root, run with python3 remove_returns.py, then delete the script.
#!/usr/bin/env python3
"""
Remove terminal `return` from Motoko functions that have exactly one return
statement in their direct body (excluding nested functions).
Usage:
1. Set src_dirs to match your project layout.
2. Run: python3 remove_returns.py
3. Run tests: npx mops test
4. Delete this script after confirming all tests pass.
Safety:
- Tracks function boundaries via character-level brace-depth parsing.
- Skips string literals ("..." with \" escapes) and comments (// and /* */).
- Detects nested `func` declarations and skips their bodies entirely.
- Only removes the `return` keyword (+ trailing space) from functions with
exactly 1 return at any depth within the direct body.
"""
import re
import glob
import os
# ── Configuration ──────────────────────────────────────────────────────
# Directories to process (relative to script location or cwd).
SRC_DIRS = ["src", "test", "bench"]
# ──────────────────────────────────────────────────────────────────────
def find_func_bodies(text):
"""
Yield (body_start, body_end) for every top-level and nested function
found in `text`. body_start is the index of the opening '{' of the
function body; body_end is the index of the matching closing '}'.
Handles:
- String literals (skips content inside "...")
- Line comments (// ...)
- Block comments (/* ... */)
- Nested braces
"""
i = 0
n = len(text)
while i < n:
# Skip string literals
if text[i] == '"':
i += 1
while i < n and text[i] != '"':
if text[i] == '\\':
i += 1 # skip escaped char
i += 1
i += 1 # skip closing "
continue
# Skip line comments
if text[i] == '/' and i + 1 < n and text[i + 1] == '/':
i += 2
while i < n and text[i] != '\n':
i += 1
continue
# Skip block comments
if text[i] == '/' and i + 1 < n and text[i + 1] == '*':
i += 2
while i < n and not (text[i] == '*' and i + 1 < n and text[i + 1] == '/'):
i += 1
i += 2 # skip */
continue
# Look for 'func' keyword at a word boundary
if text[i:i+4] == 'func' and (i == 0 or not text[i-1].isalnum() and text[i-1] != '_'):
after = text[i+4:i+5] if i + 4 < n else ''
if after == '' or not (after.isalnum() or after == '_'):
# Found a func keyword. Scan forward to find the opening '{'.
j = i + 4
while j < n:
if text[j] == '"':
j += 1
while j < n and text[j] != '"':
if text[j] == '\\':
j += 1
j += 1
j += 1
continue
if text[j] == '{':
# Found the opening brace of the function body.
brace_start = j
depth = 1
j += 1
while j < n and depth > 0:
if text[j] == '"':
j += 1
while j < n and text[j] != '"':
if text[j] == '\\':
j += 1
j += 1
j += 1
continue
if text[j] == '/' and j + 1 < n and text[j + 1] == '/':
j += 2
while j < n and text[j] != '\n':
j += 1
continue
if text[j] == '/' and j + 1 < n and text[j + 1] == '*':
j += 2
while j < n and not (text[j] == '*' and j + 1 < n and text[j + 1] == '/'):
j += 1
j += 2
continue
if text[j] == '{':
depth += 1
elif text[j] == '}':
depth -= 1
j += 1
brace_end = j - 1 # index of closing '}'
yield (brace_start, brace_end)
i = j
break
if text[j] == '=' or text[j] == ';':
# func ... = expr; (no body) or forward decl
i = j + 1
break
j += 1
else:
i = j
continue
i += 1
def count_returns_in_direct_body(text, body_start, body_end):
"""
Count `return` statements that are directly inside this function body
(not inside nested functions). Returns list of (return_keyword_start,
return_keyword_end) positions.
"""
body = text[body_start + 1 : body_end] # content between { and }
offset = body_start + 1
# First, find all nested func bodies within this body so we can skip them.
nested_ranges = []
for ns, ne in find_func_bodies(body):
# Adjust to absolute positions
nested_ranges.append((ns + offset, ne + offset))
def is_inside_nested(pos):
for ns, ne in nested_ranges:
if ns <= pos <= ne:
return True
return False
# Now scan for `return` keywords in the body, skipping nested funcs.
returns = []
i = 0
while i < len(body):
# Skip strings
if body[i] == '"':
i += 1
while i < len(body) and body[i] != '"':
if body[i] == '\\':
i += 1
i += 1
i += 1
continue
# Skip line comments
if body[i] == '/' and i + 1 < len(body) and body[i + 1] == '/':
i += 2
while i < len(body) and body[i] != '\n':
i += 1
continue
# Skip block comments
if body[i] == '/' and i + 1 < len(body) and body[i + 1] == '*':
i += 2
while i < len(body) and not (body[i] == '*' and i + 1 < len(body) and body[i + 1] == '/'):
i += 1
i += 2
continue
# Check for 'return' keyword
if body[i:i+6] == 'return' and (i == 0 or not body[i-1].isalnum() and body[i-1] != '_'):
after = body[i+6:i+7] if i + 6 < len(body) else ''
if after == '' or not (after.isalnum() or after == '_'):
abs_pos = i + offset
if not is_inside_nested(abs_pos):
returns.append((abs_pos, abs_pos + 6))
i += 6
continue
i += 1
return returns
def process_file(filepath):
with open(filepath, 'r') as f:
text = f.read()
original = text
removals = 0
# Collect all function bodies
func_bodies = list(find_func_bodies(text))
# For each function, check if it has exactly 1 return in its direct body
# Process in reverse order to preserve indices when editing
edits = [] # list of (start, end) of "return " to remove
for body_start, body_end in func_bodies:
returns = count_returns_in_direct_body(text, body_start, body_end)
if len(returns) == 1:
ret_start, ret_end = returns[0]
# Verify this return is the terminal statement of the direct body:
# scan past the return's expression (tracking brackets/strings/comments)
# to its terminating ';' (or body_end), then ensure only whitespace,
# comments, and optional semicolons remain before body_end.
n = len(text)
j = ret_end
depth = 0
stmt_end = body_end # position after the return statement
while j < body_end:
c = text[j]
if c == '"':
j += 1
while j < body_end and text[j] != '"':
if text[j] == '\\':
j += 1
j += 1
j += 1
continue
if c == '/' and j + 1 < body_end and text[j + 1] == '/':
j += 2
while j < body_end and text[j] != '\n':
j += 1
continue
if c == '/' and j + 1 < body_end and text[j + 1] == '*':
j += 2
while j < body_end and not (text[j] == '*' and j + 1 < body_end and text[j + 1] == '/'):
j += 1
j += 2
continue
if c in '({[':
depth += 1
elif c in ')}]':
depth -= 1
elif c == ';' and depth == 0:
stmt_end = j + 1
break
elif c == '\n' and depth == 0:
# Statement terminated by newline (no semicolon)
stmt_end = j
break
j += 1
else:
stmt_end = body_end
# Now skip whitespace, comments, and stray semicolons after the return
k = stmt_end
terminal = True
while k < body_end:
c = text[k]
if c.isspace() or c == ';':
k += 1
continue
if c == '/' and k + 1 < body_end and text[k + 1] == '/':
k += 2
while k < body_end and text[k] != '\n':
k += 1
continue
if c == '/' and k + 1 < body_end and text[k + 1] == '*':
k += 2
while k < body_end and not (text[k] == '*' and k + 1 < body_end and text[k + 1] == '/'):
k += 1
k += 2
continue
# Found other code after the return — not terminal.
terminal = False
break
if not terminal:
continue
# Remove "return " (keyword + trailing space)
if ret_end < len(text) and text[ret_end] == ' ':
edits.append((ret_start, ret_end + 1))
else:
edits.append((ret_start, ret_end))
# Apply edits in reverse order to preserve positions
edits.sort(key=lambda x: x[0], reverse=True)
for start, end in edits:
text = text[:start] + text[end:]
removals += 1
if text != original:
with open(filepath, 'w') as f:
f.write(text)
return removals
def main():
total = 0
files_modified = 0
for src_dir in SRC_DIRS:
for filepath in sorted(glob.glob(os.path.join(src_dir, "**/*.mo"), recursive=True)):
r = process_file(filepath)
if r > 0:
print(f" {filepath}: {r} returns removed")
total += r
files_modified += 1
print(f"\nTotal: {total} returns removed in {files_modified} files")
if __name__ == '__main__':
main()
B) Dot‑notation conversion
For all Motoko dot‑notation rules, automation scripts, and pitfalls, see the dedicated skill:
- skills/dot-notation-migration/SKILL.md
This file intentionally does not duplicate those instructions. Apply dot‑notation changes using the dedicated skill, then continue here with import cleanup (Section D) and import ordering (Section F).
C) Dot‑notation import requirements
For import mapping and rules related to dot‑notation, use the dedicated skill:
- skills/dot-notation-migration/SKILL.md
This file intentionally does not duplicate the import mapping. After applying dot‑notation changes per that skill, proceed with Section D (unused import cleanup) and Section F (import ordering).
D) Clean up unused imports (safely)
Goal
- Remove imports that are truly unused after prior refactors, but do not remove modules implicitly required by dot‑notation.
Reality check
- Editor tooling (VSCode Motoko extension) correctly marks unused imports, including dot‑notation awareness. CLI detection can be trickier.
Common false positives (imports that LOOK unused but are REQUIRED)
Blob— needed when.toArray(),.size(),.isEmpty(),.hash()are called onBlobvalues (e.g.,Sha256.fromArray(...).toArray(),hmac.sum().toArray())Array— needed when.flatten(),.foldLeft(),.sliceToArray(),.map(),.filter()etc. are called on[T]values (e.g.,[arr1, arr2].flatten())Nat— needed when.toText()is called onNatvalues from.size()(e.g.,arr.size().toText())VarArray— needed when.toArray()is called on[var T]values- Rule: If ANY dot-notation method is called on a value of that module's type, the import is required even though the module name never appears explicitly in the code.
Approaches
-
Editor‑guided
- Open the workspace in VSCode. For each
*.mofile, accept quick‑fix to remove imports marked as unused. Review diffs.
- Open the workspace in VSCode. For each
-
Compiler/LSP‑assisted batch
- Use the Motoko language server via the VSCode extension to surface all diagnostics; apply code actions in batches where supported.
-
Script‑assisted conservative removal
- Write a simple script that:
- Parses each
import ... "mo:core/XYZ"; - Searches file for either
XYZ.or any of the known dot‑patterns mapped toXYZ(see Dot‑Notation Migration Skill import mapping) - If neither is found, flag the line as removable
- Parses each
- Manually review flagged lines before deletion
- Write a simple script that:
Audit helpers
- After cleanup, search for "import" lines whose module name never appears and no mapped dot‑pattern is present.
- Build the project. If a required module was removed, dot‑calls will fail at compile time — restore import and refine rules.
E) Shorten local (sibling) import paths
-
Use bare module names for local imports when possible:
"Bech32"instead of"./Bech32". Both resolve correctly, but bare names are more concise and idiomatic.// Good import Bech32 "Bech32"; import Script "Script"; import Types "Types"; // Acceptable (cross-directory) import ByteUtils "../ByteUtils"; import Curves "../ec/Curves"; // Avoid (unnecessary ./ prefix for siblings) import Bech32 "./Bech32"; -
For cross-directory imports, relative paths with
../are required and acceptable.
F) Aggregate and alphabetize imports by section
Why
- Consistent ordering reduces merge conflicts and speeds reviews. Clear grouping improves scanning and avoids mixing external modules with local ones.
Sections (in this order, each separated by a single blank line)
- mo:core imports
- All imports whose path starts with "mo:core/..." (including
mo:core/Types).
- All imports whose path starts with "mo:core/..." (including
- Other mo:* third‑party imports (mops or similar)
- Any
mo:...imports that are notmo:core/...(e.g.,mo:uuid/UUID,mo:sha2/SHA256, etc.).
- Any
- Local project modules
- Bare module name imports like
"Bech32","Common"(preferred), or relative path imports like"../ByteUtils","./Script". - Prefer bare module names without
./prefix for sibling imports (e.g.,"Bech32"instead of"./Bech32"). Both work, but bare names are cleaner. - Sort local project module imports by the local name they are imported as, not by the module name in the imported path.
- Bare module name imports like
Sorting rules (apply within each section independently)
- Sort alphabetically by the local name modules are imported as
- Preserve import style (module vs. named type imports).
- Keep multiple named‑type imports from the same path on a single line as‑is.
- Optionally keep a comment header above each section (Core, Third‑party, Local) if your repo style prefers.
Example
// Before (mixed)
import Runtime "mo:core/Runtime";
import { type Result } "mo:core/Types";
import SHA256 "mo:sha2/SHA256";
import Map "mo:core/Map";
import BitVec "mo:bitvec/BitVec";
import Utils "../lib/Utils";
import Logger "./Logger";
// After (aggregated and sorted per section)
//// Core
import Map "mo:core/Map";
import Runtime "mo:core/Runtime";
import { type Result } "mo:core/Types";
//// Third‑party (mops)
import BitVec "mo:bitvec/BitVec";
import SHA256 "mo:sha2/SHA256";
//// Local
import Logger "Logger";
import Utils "../lib/Utils";
Lightweight automation idea (per file)
- Collect all import lines at the file top.
- Partition into the three sections by path prefix.
- Sort each partition alphabetically by the local name they are imported as
- Re‑emit sections in the order Core → Third‑party → Local, with a blank line between sections.
- Keep any non‑import comments at their relative positions unless they clearly belong to a section header.
G) Direct string‑to‑Blob assignment for constants
Pattern
// Before
let b : Blob = Text.encodeUtf8("hello");
// After
let b : Blob = "hello";
Why
- For constant Text strings, the Motoko compiler allows direct assignment to the
Blobtype. - The result is identical to
Text.encodeUtf8, but the code is cleaner and avoids an explicit function call.
Examples
// Good: direct assignment
let blobs = [
"strategy",
Text.encodeUtf8(Nat.toText(slot)),
];
// Avoid: redundant encoding for constant string
let blobs = [
Text.encodeUtf8("strategy"),
Text.encodeUtf8(Nat.toText(slot)),
];
Practical Automation Recipes (opt‑in)
These are optional starting points. Prefer editor‑integrated refactors when available. Always review diffs.
- Find one‑line return functions
rg -n --glob '!**/.mops/**' --glob '**/*.mo' "func [A-Za-z_][A-Za-z0-9_]*\(.*\) *:.*\{ *return .*; *};"
- Dot‑notation conversion & candidate detection
- See the dedicated skill for full automation and grep recipes:
- skills/dot-notation-migration/SKILL.md
- Flag possibly unused core imports (conservative)
# Rough heuristic: list imports, then search for name or dot‑patterns
rg -n --glob '!**/.mops/**' --glob '**/*.mo' '^import .*"mo:core/([A-Za-z/]+)";' -o -r '$1'
# For each file, ensure presence of module references OR mapped dot‑patterns before removal
- Aggregate + sort imports into sections (editor macro)
- Select all
importlines at the top of the file → group into three sections (Core, Third‑party mo:*, Local) → sort each group alphabetically by path → insert blank lines between sections → keep import styles as‑is.
Agent Strategy (for AI assistants)
- Confirm the project builds on
mo:corebefore starting improvements. - Work file‑by‑file. For each file:
- A. Remove single‑expression
returnforms - B. Apply dot‑notation per skills/dot-notation-migration/SKILL.md
- C. Ensure required imports for any introduced dot‑notation (see import mapping in skills/dot-notation-migration/SKILL.md)
- D. Remove truly unused imports (respect the dot‑notation import mapping from the dedicated skill)
- E. Shorten local (sibling) import paths (remove
./prefix where applicable) - F. Aggregate imports into the three sections and sort each section alphabetically (Core → Third‑party mo:* → Local)
- G. Replace
Text.encodeUtf8("<literal>")with"<literal>"where the target type isBlob.
- A. Remove single‑expression
- After each file: compile; if failure due to missing import, restore and mark mapping
- After each category across repo: run a full build and optionally tests
- Produce a short report of changes and any edge cases deferred for manual review
H) Convert Array.fromVarArray(x) to x.toArray()
Pattern
// Before
Array.fromVarArray(buf)
Array.fromVarArray<Nat8>(buf)
// After
buf.toArray()
Notes
Array.fromVarArrayis a factory function (first param is NOTself), so the dot-notation migration script does NOT convert it automatically. This is a separate conversion.VarArray.toArray()is the dot-notation equivalent — it's defined on[var T]values.- After conversion, check if
Arrayimport can be removed (it may still be needed forArray.tabulate,Array.flattendot-notation on[T], etc.) - Strip optional type params:
Array.fromVarArray<Nat8>(buf)→buf.toArray()(the type is inferred from the var array).
I) Array.tabulate type annotations are usually required
- Do NOT remove type annotations from
Array.tabulate<T>(...)calls. - The Motoko compiler often cannot infer the element type, especially when the callback uses
fromNat, arithmetic, or other expressions that could return multiple numeric types. - Removing annotations caused 13 of 24 test files to fail in a real project with errors like
expression of type [Any] cannot produce expected type [Nat8]. - Keep them:
Array.tabulate<Nat8>(n, func i { ... }).
Edge Cases & Gotchas
- For all dot‑notation behavior, method availability, factories vs methods, and mutability notes, see:
- skills/dot-notation-migration/SKILL.md
- When aggregating imports, keep named type imports from
mo:core/Typeswithin themo:coregroup; see Section F for ordering rules. - Local imports: prefer bare module names (
"Bech32") over relative paths ("./Bech32") for sibling files. Both resolve correctly but bare names are more concise. - Import paths like
"../src/Bech32"from withinsrc/are incorrect — use"Bech32"for siblings or"../SubDir/Module"for cross-directory references.
Verification & Sign‑off
- Build all canisters successfully after changes.
- Run static audits:
- No
importlines flagged unused by editor or heuristic scripts (after accounting for dot‑notation needs). - Spot check: arrays, maps, sets, text operations use dot‑notation where natural.
- No
- Diffs remain mechanical; no public API or behavioral changes.
Appendix: Dot‑notation reference
For the complete, maintained dot‑notation catalog, automation scripts, and import mapping, see:
- skills/dot-notation-migration/SKILL.md
More from research-ag/motoko-skills
motoko-general-style-guidelines
Load when working with contents in *.mo files
25motoko-benchmarks-generation
How to write benchmarks in Motoko using bench‑helper. Covers project setup (mops.toml), bench file layout in bench/*.bench.mo, the Bench.Schema rows/cols model, and safe patterns for encode/decode, hashing, crypto, and allocation benches.
25motoko-performance-optimizations
General performance optimization techniques for Motoko. Reducing allocations, efficient Text building, fixed-width arithmetic, block processing, async patterns, and more. Load when you need to improve hot paths or reduce overhead without changing behavior.
24motoko-dot-notation-migration
Use new dot-notation syntax in projects with mo:core dependency
24motoko-base-to-core-migration
Complete, AI-ready playbook to migrate Motoko projects from mo:base to mo:core — phases, renames, data structure changes, agent strategy, verification scripts, upgrade tests, and production rollout.
21motoko-doc-strings
Add `///` doc strings to public objects in Motoko modules so that `mo-doc` produces useful HTML/Markdown documentation. Covers placement rules, formatting, code examples, common pitfalls, and a verification workflow.
16