rototo
DocsReference
Reference

Diagnostics

When you run rototo lint, what comes back is a list of diagnostics - each one a specific thing that's wrong, or worth a second look, in your package. This page explains how to read a diagnostic, how the built-in checks are organized, and how your own custom checks fit in alongside them.

The thing to hold onto: lint isn't a smoke test that just says "looks fine." It actually understands rototo's model - that a variable's type matches its values, that a qualifier references a real qualifier, that a catalog entry fits its schema - and each kind of problem has its own stable name you can point at.

What a diagnostic looks like

Every diagnostic has the same parts, whether it came from a built-in check or one you wrote:

In your terminal that's printed as readable text. With --json you get the structured version, covered at the end of the page.

Rule names: who's talking

The first part of a rule name tells you where the check came from - its authority.

Built-in rototo checks always start with rototo/, followed by a flat, hyphenated name:

rototo/variable-unknown-value
rototo/qualifier-when-missing
rototo/catalog-entry-schema-mismatch

It's always exactly one level - rototo/something, never rototo/variables/something. That flatness is on purpose, so a rule name is one stable string you can grep for.

Your own custom Lua checks use the same shape, but with an authority that's yours - anything except rototo, which is reserved:

consumer-experience/checkout-heading-required
payments/max-token-budget

Both the authority and the rule name have to be lowercase letters, digits, and hyphens. That keeps every diagnostic in the system addressable the same way, no matter who wrote the rule.

Seeing the whole catalog

You don't have to memorize the rules - you can ask for the list:

# every built-in rule rototo ships
rototo show --lint-rules

# from inside a package: built-in rules *plus* your custom ones
rototo show --lint-rules

# machine-readable, for tooling
rototo show --lint-rules --json

Run it with no package and you get the global built-in catalog. Run it from inside a package and rototo adds that package's custom rules to the list, so you see exactly what this package will be checked against. The human view is a table of rule | entity | severity | title; the JSON view carries the same fields plus each rule's help.

That command is the source of truth - it can't drift the way a hand-written list in these docs would.

How the built-in checks are grouped

There are a few dozen built-in rules, and they line up with the parts of a package. You don't need them all in your head; this is the map so you know roughly where a finding is coming from.

A good rule of thumb: if the rule name starts with the thing you just edited, the finding is about that thing.

Errors versus warnings

There are only two severities, and the line between them is simple.

An error means the package can't be trusted to run - a value doesn't match its type, a qualifier points at one that doesn't exist, an entry breaks its schema. Package::load in the SDK rejects a package with lint errors, so these genuinely block a release.

A warning is something you probably want to know but that won't break anything: a qualifier nothing references, a rule that can never fire, a custom lint file that registered no rules. Warnings are how lint nudges you toward a cleaner package without standing in your way.

Your own checks, in Lua

Some rules are specific to your world and rototo can't guess them - "standard-tier users can't get more than five projects," "a checkout heading can't be empty." Those live in lint/*.lua, and they produce diagnostics that sit right next to the built-in ones.

A lint file defines a register function and registers one or more rules inside it:

function register(lint)
  lint:rule({
    id = "consumer-experience/checkout-heading-required",
    title = "Checkout heading is missing",
    help = "Set heading to visible checkout copy.",
    target = "/catalogs/checkout-redesign/entries",
    severity = "error",
    handler = "check_heading",
  })
end

function check_heading(package, entry)
  if is_checkout_value(entry.value) and entry.value.heading == "" then
    return {{ message = "checkout value " .. entry.key .. " must include heading", path = "/value/heading" }}
  end
  return {}
end

What each field in lint:rule({...}) does:

The handler gets the package and the current target, and returns a list of problems. Each problem needs a message, and can optionally point at the exact spot with a path or field. Returning an empty list (or nil) means "nothing wrong here."

A couple of guardrails worth knowing, since they shape what your Lua can do: the handlers run in a locked-down sandbox - just the table, string, utf8, and math libraries, no file or network access - with limits on memory, work, and a two-second timeout per handler. Custom lint is for inspecting the package, not reaching outside it.

The JSON shape

rototo lint --json emits each diagnostic as an object. Here's the full shape, with the fields you'll actually use up top:

{
  "rule": "rototo/variable-value-type-mismatch",
  "severity": "error",
  "message": "value 3 does not match declared type bool",
  "help": "Use a value matching the variable's declared type.",
  "location": {
    "path": "variables/checkout-redesign.toml",
    "range": {
      "start": { "line": 8, "character": 8 },
      "end": { "line": 8, "character": 9 }
    }
  },
  "stage": "value",
  "target": {
    "entity": { "kind": "variable", "id": "checkout-redesign" },
    "field": { "kind": "resolveDefault" }
  },
  "related": [
    { "location": { "path": "...", "range": { } }, "message": "declared here" }
  ]
}

The fields:

Severity and stage both serialize lowercase. The rule string is identical whether it's built-in or custom, so one piece of tooling can read findings from both.