Skip to content

Diagnostics

The cmotion CLI emits stable diagnostic codesCLI001, PAR100, NAM003 — that don’t get renumbered between releases. Every code is namespaced by the stage that emits it, carries a typed repair plan, and has a long-form explanation reachable from the command line:

Terminal window
cmo explain NAM003
cmo --json explain NAM003

Every diagnostic lives inside the same JSON envelope on --json. See the CLI README for the envelope contract; this page documents only the codes.

Each diagnostic carries a fixSafety label so agents and editors know how much trust to extend to the repair:

LabelMeaningWhat a tool should do
format-onlyonly formatting changesauto-apply
behavior-preservingpreserves runtime semanticsauto-apply
local-editconfined to a local scope or fileapply, but show diff
api-changingchanges a signature / exported name / call sitepropose only
requires-human-reviewambiguous, risky, or context-dependentpropose only
PrefixStageStatus
CLI*CLI argument parsing, file IOimplemented
PAR*Parser (tree-sitter)implemented
LWR*CST → AST loweringimplemented
NAM*Name resolutionimplemented
TYP*Type checkerpartial (TYP002 only)
UNT*Unit checkerpartial (UNT001, UNT002)
TIM*Timeline (negative duration, out-of-range keyframe)reserved
ANM*Animate / keyframe rulesreserved
COL*Color spacereserved
CMP*Composereserved
ASS*Asset (missing, wrong type)reserved
DET*Determinism partition violationsreserved
BKE*Backend lowering (CanvasKit / WGSL)reserved

The CLI was invoked with a subcommand it does not recognise. Run cmo help to see the list of available subcommands.

  • Repair: use-known-subcommand — replace with one of help, version, parse, check, fmt, explain.
  • Fix safety: requires-human-review.

A subcommand expected a positional argument that was not supplied. Most subcommands take exactly one .cm source file path.

  • Repair: supply-source-file (or supply-diagnostic-code for cmo explain).
  • Fix safety: requires-human-review.

A flag was passed that the subcommand does not accept. Global flags are --json and --no-color; subcommand-specific flags are listed in cmo <subcommand> --help.

  • Fix safety: requires-human-review.

The subcommand exists but its implementation is still a stub. The diagnostic’s repair.id names the stage that needs to ship. See the roadmap for the order in which stages land.

  • Fix safety: requires-human-review.

The path passed to the subcommand does not resolve to a readable file. Check the path, file permissions, and current working directory.

  • Repair: supply-valid-path.
  • Fix safety: requires-human-review.

cmo explain CODE was called with a code that has no long-form entry. This usually means the code is mistyped or the binary is older than the code you’re looking up.

  • Repair: register-diagnostic-code.
  • Fix safety: requires-human-review.

The tree-sitter parser could not be created or could not accept the generated cmotion language. This is an internal error — the runtime and grammar are out of sync, or the binary was built against the wrong runtime version.

  • Repair: report-bug.
  • Fix safety: requires-human-review.

The parser produced a tree containing ERROR or MISSING nodes. The span on the diagnostic points at the first invalid token; run cmo parse --json <file> to inspect the full CST and locate every error node.

  • Repair: fix-syntax.
  • Fix safety: requires-human-review.

The tree-sitter parse produced no ERROR/MISSING nodes, but the lowering pass (src/lower.zig) failed to map some node kind into the AST. This means lowering has a gap: the grammar emits a node kind that the AST builder doesn’t yet handle. The diagnostic’s message field names the Zig error variant that fired.

  • Repair: extend-lowering.
  • Fix safety: requires-human-review.

A name was referenced that resolves to nothing in the current scope chain (local lets, parameters, top-level declarations, imported names).

The check is suppressed when any use foo.* wildcard import is present, since cmotion does not yet have module manifests — we don’t know what names a wildcard brings into scope. Once manifests land, NAM003 will fire through wildcards too.

Repair is one of:

  • add a let name = ...; before the use site

  • bring the name into scope with use mod.name; or use mod as alias;

  • fix a typo

  • Repair: declare-or-import.

  • Fix safety: local-edit.

NAM004 — Forward reference within a block

Section titled “NAM004 — Forward reference within a block”

A name was referenced before its let declaration in the same block. Block lets are lexically scoped — they’re visible only to code that appears after their declaration. The diagnostic’s actual field names the line and column where the binding actually lives, so an agent can choose between two repairs: reorder so the declaration comes first, or hoist the declaration to an enclosing scope.

component title() -> Frame {
let a = b; // NAM004: 'b' is declared later in this block at line 3 column 7
let b = 1;
a
}

Distinct from NAM003: if the name is never declared anywhere in the block (or any enclosing scope), you get NAM003. Self-reference (let x = x + 1) is also NAM003 — the name doesn’t exist yet at that point even though it will.

  • Repair: reorder-block-let.
  • Fix safety: local-edit.

NAM005 — Duplicate top-level declaration

Section titled “NAM005 — Duplicate top-level declaration”

Two top-level declarations resolve to the same local name (for example two component title or a let answer = 1; next to a scene answer). The diagnostic’s actual field names the previous declaration’s line and column so an agent can see both sites without re-parsing.

  • Repair: rename-duplicate.
  • Fix safety: api-changing — renaming a top-level changes the exported name and any callers.

A signature lists the same parameter name twice. Parameter names must be unique within a single component, scene, filter, or lambda signature. The diagnostic’s actual field cross-references the prior parameter’s location.

  • Repair: rename-duplicate-param.
  • Fix safety: api-changing — the parameter name is part of the named-argument API.

TYP002 — Type mismatch on an annotated value

Section titled “TYP002 — Type mismatch on an annotated value”

An annotated let, param, or export was assigned a literal value whose category doesn’t match the annotation. The check is deliberately narrow today: it only fires when both sides are inferable.

Fires when the annotation is a simple_type whose name is a known category (String, Bool, Color, or one of the number-like types: Number, Int, Float, Duration, Time, Angle, Length, Pixels, Frequency, Tempo, Bars, Beats, Percent) and the value is a literal (number, string, bool, or color).

Skipped when the value is a call, identifier, or anything that needs real inference — the full typechecker takes over for those.

Examples:

let name: String = 42 // TYP002: expected 'String', got a number literal
let active: Bool = "yes" // TYP002: expected 'Bool', got a string literal
component title(bg: Color = 0) -> Frame // TYP002 on the default: expected 'Color', got a number literal

Unit-category mismatches within the number family (let x: Duration = 6deg) belong to UNT* codes, not TYP002.

  • Repair: align-value-with-type.
  • Fix safety: local-edit — usually only the value or annotation changes, no call sites move.

An annotated number-family let, param, or export was assigned a number literal whose unit suffix lives in the wrong category. The check cascades from TYP002: it runs only when both sides are number-side, the annotated type name pins a unit category, and the literal carries a unit.

Type → unit category mapping:

TypeAllowed units
Duration / Times, ms, us, ns
Angledeg, rad, turn
Length / Pixelspx
Percent%
Frequencyhz, khz
Tempobpm
Barsbars
Beatsbeats
Number / Int / Float(no unit)

Examples:

let timeout: Duration = 6deg // UNT001: Duration expects Time, got 6deg (Angle)
let count: Number = 5ms // UNT001: Number expects unitless, got 5ms (Time)
let timeout: Duration = 500ms // OK

If the literal has no unit at all (let x: Duration = 42), see UNT002 below.

  • Repair: align-unit-with-type.
  • Fix safety: local-edit.

An annotated number-family let, param, or export was assigned a unitless number literal where a unit is required (the annotation pins a non-Number unit category). The diagnostic’s repair.summary names the canonical unit for the category so an agent can fix it in one edit:

let timeout: Duration = 42 // UNT002: requires a Time unit (suggested: '42s')
let angle: Angle = 90 // UNT002: requires an Angle unit (suggested: '90deg')
let count: Number = 42 // OK — Number requires an unmarked literal

Canonical units used in the suggestion:

Annotation categorySuggested unit
Duration / Times
Angledeg
Length / Pixelspx
Percent%
Frequencyhz
Tempobpm
Barsbars
Beatsbeats
  • Repair: add-required-unit.
  • Fix safety: local-edit.

  • Codes never get renumbered. Once a code has shipped, its name is permanent. Retired codes are marked as such on this page but kept in the table.
  • Field shapes in the JSON envelope (code, message, path, line, column, length, expected, actual, help, fixSafety, repair, related) are versioned by the envelope’s schemaVersion. Additive fields are not a breaking change; removed fields bump the version.
  • cmo explain CODE is the authoritative long-form text. This page mirrors it; if the two ever drift, cmo explain wins.