loam.dev
v0.1.15 · previewEN

Dieser Leitfaden wird auf Englisch gepflegt — die Fachbegriffe sind im gesamten Tool einheitlich englisch.

loam.dev Developer & Tool Guide

A practical reference for using loam.dev — the Dart/Flutter codebase intelligence CLI — covering core concepts, CLI commands, output formats, and how loam.dev handles code generators automatically.

Naming. The product is loam.dev; loam (no .dev) is exclusively the CLI command and the pub.dev package name. The capitalised form without .dev is anti-vocabulary — always write loam.dev in prose.


Core concepts

Finding

A Finding is a single analysis result:

{ ruleId, severity, location, message, fingerprint }

Every Finding has a stable Fingerprint — a position-robust hash used for baseline diffing. ruleId identifies which Rule produced it; severity is one of error, warning, or info.

Vocabulary note: “Issue” refers to project tracker tickets, not analysis results. Always use “Finding” in the context of loam.dev output.

Severity

Three levels, in descending order: error · warning · info.

Rule

A Rule is one analysis unit behind the shared Rule interface. Each capability (unused public API, slop detection, cycle detection …) is a separate Rule. Adding a new Rule never changes the analysis pipeline — only the rule list.

Rules are either:

Currently active rules:

Rule IDWhat it finds
a11y-form-field-labelFlutter TextField and TextFormField without a visible or accessible label — neither decoration: InputDecoration(labelText: …) nor InputDecoration(hintText: …) (accepted as fallback) nor an enclosing Semantics(label: …) ancestor. WCAG 1.3.1 Info and Relationships / 3.3.2 Labels or Instructions / 4.1.2 Name, Role, Value. Uses the resolved element model; local classes named TextField are never flagged. Conservative: a decoration: that is not a literal InputDecoration(…) creation is left alone. AST-only: out of scope are runtime contrast, focus order, touch-target sizes, and dynamic decoration values.
a11y-icon-button-labelFlutter IconButton without a tooltip: argument and without an enclosing Semantics(label: …), and GestureDetector/InkWell whose direct child: argument is a pure Icon and that likewise lacks an enclosing Semantics(label: …) — WCAG 4.1.2 Name, Role, Value. Uses the resolved element model; local classes named IconButton are never flagged. AST-only: out of scope are composite icon children, dynamic tooltip values, and custom accessible-name mechanisms.
a11y-image-labelFlutter Image widgets (Image(), Image.asset, Image.network, Image.file, Image.memory) without semanticLabel or excludeFromSemantics: true — WCAG 1.1.1 Non-text Content. Uses the resolved element model to identify Flutter’s Image, not regex. AST-only: out of scope are runtime contrast, focus order, and touch-target sizes.
a11y-interactive-semanticsGeneric and custom (non-Flutter) interactive widgets that carry an onTap, onPressed, or onLongPress named argument but lack both an enclosing Semantics(label: …) ancestor and a non-empty label: … / semanticLabel: … / tooltip: … argument on the call site itself — WCAG 4.1.2 Name, Role, Value. The call-site property check covers widgets that encapsulate their own accessibility (self-wrapping in Semantics(...), or delegating the value to an already-excluded Flutter widget) without needing to inspect the target class’s definition. Flutter built-in interactive widgets covered by sibling rules (IconButton, GestureDetector, InkWell, TextField, TextFormField) are excluded. Uses the resolved element model (library URI check); local classes with the same name as excluded Flutter widgets are never skipped. AST-only: out of scope are interactive behaviour via inherited callbacks, custom gesture recognisers that bypass the named parameters, and accessible-name mechanisms other than Semantics(label: …) and the three recognised call-site properties.
circular-dependenciesCircular import/export chains between first-party lib/ libraries — one finding per strongly connected component, naming all files in the loop.
code-duplicatesStructurally identical code blocks over the resolved AST (token-normalised, Rabin-Karp clustered); detects Type-1 (exact) and Type-2 (renamed identifiers/literals) duplicates project-wide. One finding per cluster listing all locations, with a stable fingerprint that survives line shifts.
complexity-hotspotsCyclomatic and cognitive complexity per executable (function, method, constructor, accessor); flags hotspots above documented, conservative thresholds. Aggregated by loam health (together with project-wide finding load) into a composite Health-Score (0–100) and Grade (A–F).
slop-empty-catchcatch blocks whose body is empty ({}) or contains only comments — both silently swallow exceptions. Severity: warning. Category: slop. Extends the built-in empty_catches lint (literal-empty only) to also flag the comment-only variant that AI agents commonly produce. Conservative allowlist: any rethrow, throw, or logging call in the body suppresses the finding (all produce at least one statement). Generated files are skipped automatically. What it deliberately does NOT catch: bodies with any non-comment statement (rethrow, throw, logging call, or any other executable code).
slop-unjustified-ignore// ignore: and // ignore_for_file: directives with no written justification — either inline (text after the lint names, separated by or similar) or on the immediately preceding comment line. Severity: info. Category: slop. Generated files (.g.dart, .freezed.dart, etc.) are skipped automatically. What it deliberately does NOT catch: directives that have any non-empty text after the lint-name list (inline reason), directives whose preceding line contains any // comment (accepted as a reason), and directives in generated files.
slop-narrative-comment// comments that add no information beyond what the code already shows. Declaration-adjacent: immediately before a declaration, normalised text restates the declaration name (e.g. // build before void build()) or belongs to a fixed restatement list (“constructor”, “getter”, “setter”, “build method”) or the <name> method/widget patterns. Context-free (anywhere in the file): decorative banner/divider comments (// ====, // ****** SECTION ******), bare structural category labels (“main logic”, “helper function”, “error handling”, …), emoji-only decoration, redundant end-of-block markers (// end if, // End processOrder), and vague TODOs with no concrete action, condition, or reference (a TODO with real substance, e.g. // TODO: handle null case when userId is missing (see #123), is never flagged). Severity: info. Category: slop. Generated files (.g.dart, .freezed.dart, etc.) are skipped automatically. What it deliberately does NOT catch: /// Dart-doc comments (never flagged), block comments, plain hyphen dividers (// ---------------) — a common, legitimate section-separator convention, not AI-slop — and // comments with informative text that does not match any of the fixed categories above (no fuzzy heuristic quality scoring).
slop-workflow-narration-commentSequential “workflow narration”: two or more // Step 1: …, // Step 2: …, // First, …, // Next, …, // Then, …, // Finally, … comments, each immediately preceding a statement in the same function/method/constructor body, that together narrate the body step-by-step even though the statements are already self-explanatory. A single such comment on its own is not flagged — only a sequence (2+) is. Severity: info. Category: slop. Generated files are skipped automatically. What it deliberately does NOT catch: /// Dart-doc comments, block comments, a lone workflow-style comment with no sequence, and comments not immediately adjacent to the statement they narrate.
unused-public-exportsPublic API members (classes, methods, getters/setters, fields, enums, typedefs) with no references anywhere in the project — on the resolved element model, not regex.

The remaining rules in the planned target surface (boundary violations, more AI-slop rules, hardcoded secrets) are still to come (🚧).

Baseline

A Baseline is a frozen snapshot of accepted Findings written to baseline.json. The Gate uses it as a reference: only new Findings (not in the baseline) count against the gate decision. A Baseline is the bridge between a full audit and ongoing CI hygiene.

Gate

The Gate is loam.dev’s pass/fail decision with an exit code, designed for CI.

Two modes:

ModeBehaviourWhen to use
Ratchet (default)Only NEW Findings fail. Kept (frozen) and fixed Findings are transparent. The score can only improve.All projects with existing code (established repos).
Absolute (--absolute)All current Findings are evaluated against a fixed threshold (default 0). Baseline is ignored.Greenfield projects, or pipelines requiring zero findings.

Ratchet is the default. This means an existing codebase never goes red on day one — you baseline the current state and only new regressions fail CI.

Reporter

A Reporter converts Findings into output. It is a pure renderer: it has no influence on the gate decision or exit code (Invariant 4).

Available formats: see Output formats below.

Recommendation

A Recommendation is a curated, agent-addressed suggestion for how to avoid an entire class of Finding going forward — distinct from a Finding, which describes one concrete occurrence at a file/line. After a run with Findings, loam.dev appends exactly one Recommendation per distinct fired ruleId (deduplicated, even if that rule fired dozens of times), sorted lexicographically for determinism. The block is not shown on a clean run (no Findings ⇒ no empty-noise block).

Recommendations are curated, not LLM-generated — a hand-written, versioned guidance corpus (guidance@ver, currently guidance@v2) keyed by ruleId, with no LLM call in the scan/gate path (Invariant 2). The text is addressed to the AI agent consuming loam.dev’s output, and explicitly asks the agent to propose the relevant recommendations to its user for their persistent instructions (e.g. CLAUDE.md) — loam.dev itself never writes to user instructions.

In human/markdown/html output the block renders as readable prose with the agent instruction spelled out; in json it is a structured recommendations array of { "ruleId": "...", "guidance": "..." } entries, omitted entirely (not emitted empty) when there are no recommendations.


Reproducibility

loam.dev is deterministic by design:

Same code + same ruleset@veridentical Findings.

The ruleset@ver is stored in baseline.json and in SARIF/JSON output alongside the tool version. When the ruleset version changes (a rule is added or its logic is updated), the gate warns and suggests a baseline refresh. There is no silent rule-version drift.

For the LLM layer (planned): sha(code)+prompt@ver keys the VerdictCache, so even the LLM path is deterministic per code snapshot.


Configuration & discovery

loam is Zero-Config: without a loam.yaml it works out of the box. To customise, scaffold one with loam init (writes a fully-commented loam.yaml to the project root; it never overwrites an existing file).

Discovery. loam looks for loam.yaml by walking up from the analysed project root to — and including — the enclosing git repository root (the directory holding .git), exactly like Dart’s own analysis_options.yaml. So a config at your repo root applies even when a sub-package is analysed. The walk is deliberately bounded at the git root: a loam.yaml above the repository (e.g. in $HOME) is never picked up, which keeps discovery repo-relative and reproducible across machines and CI. With no git root, only <projectRoot>/loam.yaml is read.

Layering (monorepos). Every loam.yaml on the way is merged farthest→nearest, so the repo root provides shared defaults and a nearer (per-package) config builds on top of it. Merge rules — nearer wins:

KeyMerge behaviour
rulesmerged per ruleId; the nearer value overrides
ignoreconcatenated (farther first), de-duplicated, order preserved
source_dirsscalar override; a layer that omits it inherits the farther value
update_check, a11yscalar override; omitted → inherit

ignore globs are always matched relative to the analysed project root, regardless of which layer declared them — a repo-root ignore: ["**/*.g.dart"] therefore applies relative to each analysed package.


CLI commands

Install via Homebrew (recommended on Apple Silicon macOS / Linux):

brew install silvio-l/loam/loam

Or via pub.dev (all platforms):

dart pub global activate loam

Global options

--format <format>    Output format (default: human).
--no-progress         Suppress the live progress bar.

Available formats: human · sarif · json · markdown · html.

Live progress. scan, gate, baseline, slop, a11y, and health all show the same live loading/analysis progress bar in an interactive terminal. It is automatically disabled outside a TTY and under CI (non-empty CI env var) — never interferes with piped or machine-readable output. Silence it explicitly with --no-progress or the LOAM_NO_PROGRESS environment variable; progress never appears in structured output (sarif/json/ markdown/html) regardless.

loam scan

Full audit: runs all active rules across the whole project, baseline-independent. Use this to see every Finding regardless of what is in the baseline.

loam scan                              # current directory
loam scan -p /path/to/project          # explicit project root
loam scan --format json                # machine-readable output

Exit code 1 when any Findings are present; 0 when clean.

This is the Vollaudit command — start here to assess a new project before freezing a baseline.

loam baseline

Show, write, or update the baseline (baseline.json).

loam baseline                          # show current baseline findings
loam baseline --write                  # freeze current findings as baseline
loam baseline --update                 # refresh an existing baseline

Baseline onboarding flow for an existing project:

  1. loam scan — see everything
  2. Fix or accept what you can
  3. loam baseline --write — freeze the accepted state
  4. loam gate in CI from now on

loam gate

CI gate: evaluates current Findings against the baseline.

loam gate                              # ratchet mode (default): only new findings fail
loam gate --absolute                   # absolute mode: all findings evaluated, threshold 0
loam gate --format sarif               # SARIF output (CI code-scanning)

Exit code 1 when the gate fails; 0 when it passes.

Prints a terse summary line:

loam gate: N neu, M eingefroren, K gefixt — grün.

loam health

Composite Health-Score (0–100) and Grade (A–F) combining finding load (severity-weighted, size-normalised) and complexity hotspots (weighed absolutely, not diluted by function count), plus a descending Hotspot table (file:line, symbol, cyclomatic, cognitive). The terminal output breaks the score down into its Findings and Complexity contributions.

loam health                    # analyse current directory
loam health /path/to/project   # positional project root
loam health -p /path/to/proj   # --project-root override

Exit code: always 0 on a successful run (even with a low score — gating stays loam gate). Exit 64 on a usage error (e.g. two positional arguments).

The health score is a diagnostic view: it always measures, regardless of whether complexity-hotspots is disabled in loam.yaml. The codegen exclusion (generated files excluded by the collector) stays active.

loam slop

AI-slop audit: runs slop-category rules only across the whole project.

loam slop                          # analyse current directory
loam slop /path/to/project         # positional project root
loam slop -p /path/to/proj         # --project-root override
loam slop --format json            # machine-readable output

Exit code 1 when any slop Findings are present; 0 when clean.

The slop category is run-isolated from drift and accessibility categories — loam slop only ever executes slop-category rules.

loam scan includes slop rules default-on. Currently active slop rules: four — slop-empty-catch (empty and comment-only catch bodies that silently swallow exceptions), slop-unjustified-ignore (// ignore: and // ignore_for_file: without a written justification), slop-narrative-comment (// comments that restate the declaration name or use a fixed narrative phrase, plus banner comments, empty category labels, emoji-only decoration, end-of-block markers, and vague TODOs), and slop-workflow-narration-comment (sequential Step 1:/ First,/Next,/Then,/Finally, comments narrating a function body step-by-step). Further slop rules (dead guards, …) are planned.

loam a11y

Accessibility audit: runs accessibility-category rules across the whole project.

loam a11y                          # analyse current directory
loam a11y /path/to/project         # positional project root
loam a11y -p /path/to/proj         # --project-root override
loam a11y --format json            # machine-readable output

Exit code 1 when any accessibility Findings are present; 0 when clean.

Accessibility rules target WCAG-relevant Flutter-widget patterns (missing semantic labels, missing tooltips on icon buttons, missing labels on form fields, …). The a11y category is run-isolated from drift and slop categories — loam a11y only ever executes accessibility-category rules; drift and slop rules are not touched.

loam scan includes the accessibility category default-on. To exclude it:

loam scan --no-a11y                # skip all accessibility-category rules

Or add a11y: false to loam.yaml for a repo-wide default. Precedence: --no-a11y (CLI) > a11y: false (loam.yaml) > default (on).

Four accessibility rules are currently active: a11y-form-field-label (WCAG 1.3.1 / 3.3.2 / 4.1.2), which flags Flutter TextField and TextFormField widgets without a decoration: InputDecoration(labelText: …) (or hintText fallback) and without an enclosing Semantics(label: …) ancestor; a11y-image-label (WCAG 1.1.1 Non-text Content), which flags Flutter Image widgets without semanticLabel or excludeFromSemantics: true; a11y-icon-button-label (WCAG 4.1.2 Name, Role, Value), which flags IconButton without tooltip and GestureDetector/InkWell with a pure Icon child when no Semantics(label: …) ancestor provides an accessible name; and a11y-interactive-semantics (WCAG 4.1.2 Name, Role, Value), which flags generic and custom (non-Flutter) widgets that carry an onTap, onPressed, or onLongPress callback but have no Semantics(label: …) ancestor and no label, semanticLabel, or tooltip argument with a non-empty value on the call site itself (the latter covers custom widgets that encapsulate their own accessibility, e.g. by wrapping themselves in Semantics(label: …) internally or delegating the value to an already-excluded Flutter widget). See the “Currently active rules” table above.

loam init

Scaffold a loam.yaml configuration in the current project.

loam init                    # writes loam.yaml in the current directory
loam init -p /path/to/proj   # specify a different project root

If loam.yaml already exists, the command refuses to overwrite it and exits with code 1 — no silent data loss. Delete or edit the file manually to replace it.

The generated loam.yaml includes commented examples for the rules:, ignore: and source_dirs: sections. The file is valid and loadable as-is (all examples are YAML comments, so no rule toggles are active until you uncomment them).

source_dirs: — production-source scope for the complexity scan

complexity-hotspots and loam health measure the universal property of function complexity, so they need to know which directories hold hand-written, shipping code. By default they measure lib/ and bin/. Override per project:

source_dirs:
  - lib
  - bin
  # - tool      # widen to include dev scripts
  # - test      # widen to include tests (usually noisy — intentionally complex)

Rules of the road:

loam fix (coming soon)

Apply mechanical fixes for Findings that have a safe auto-fix. Not yet implemented.


Output formats

All formats are selected with the global --format flag and work with every command that produces Finding output.

FormatStatusUse case
humanAvailableDefault. Human-readable terminal output with colour (when TTY).
sarifAvailableSARIF 2.1 JSON for CI code-scanning tools (GitHub, GitLab, …).
jsonAvailableMachine-readable JSON for agent/tooling integration.
markdownAvailableMarkdown report for PR comments, docs embedding, LLM pipelines.
htmlAvailableSelf-contained HTML report (loam-report.html by default; override with --output), opened automatically in the browser on interactive runs. --no-open skips it; auto-off when piped or under CI.

The Reporter is a pure renderer — format choice never affects exit codes or gate decisions.

Fix-Prompt (html report). The HTML report lets you select Findings and copy a versioned Fix-Prompt (prompt@v3) for an AI coding agent. The prompt names the analysed project via a stable, checkout-independent identifier (no absolute path), scopes the agent to that project’s own source tree, and structures its instructions along four principles: think first (state assumptions, ask instead of guessing), simplicity first (smallest fix, no speculative abstraction), surgical changes (touch only what the Finding requires, preserve existing style), and goal-driven (one verifiable success criterion per Finding). The Dart template and the report’s inline JS build byte-identical prompts (single source of truth, Invariant 5).


Automatic codegen-input suppression

Why your generated-code inputs are not reported

loam.dev does not report public members of code-generator input classes as unused. This is automatic — no comments, no configuration required.

Why? Code generators like Drift, freezed, Riverpod, and json_serializable read your class at build time and produce a companion *.g.dart or *.freezed.dart file. The generated file uses every member you declared, but the Dart element model sees no static reference from your hand-written code to those members. Without suppression, loam.dev would report every column in a Drift Table class as unused — a false positive.

How detection works

Detection is semantic (element model, not source text). Classification uses “first match wins” over three signals, in this order:

1. Base-type registry (base_type:<name>)

If the class (or any supertype in its chain) is one of the known Drift base types, it is classified as a codegen input:

Matched typeExample
TableDrift table definitions
DataClassDrift data classes
ViewDrift view definitions

The check walks the full supertype chain via the element model — it is not a string match on source text (Invariant 1).

2. Annotation registry (annotation:<name>)

If the class carries one of these annotations:

AnnotationEcosystem
@DriftDatabaseDrift
@DataClassNameDrift
@riverpodRiverpod (lowercase constant)
@RiverpodRiverpod (class annotation)
@freezedfreezed
@JsonSerializablejson_serializable

3. Structural fallback (fallback:part_generated)

If both conditions hold:

  1. The library declares a part '*.g.dart' or part '*.freezed.dart' directive.
  2. The class itself binds a generated counterpart — it extends _$X or with _$X (the _$-prefixed convention used by freezed and Riverpod class-based notifiers).

The part directive alone is deliberately not sufficient. Plain hand-written classes that merely share a file with a generated notifier must remain candidates (no over-suppression). The class must also bind the _$-symbol directly.

What stays reported

Plain classes, utilities, and any class that matches none of the three signals above remain candidates for the unused-public-exports rule. The suppression is targeted — it only applies where the generator actually consumes the members.


User-driven suppression

Three complementary mechanisms let you suppress Findings intentionally. All three act before the baseline and gate — suppressed Findings are invisible to the gate and do not appear in output or baseline.json.

1. Rule toggles (loam.yamlrules: map)

Disable a rule project-wide in loam.yaml:

rules:
  unused-public-exports: false   # rule is disabled; its findings are never produced

2. Path suppression (loam.yamlignore: glob list)

Exclude whole files or directories from the audit with project-relative glob patterns:

ignore:
  - "lib/generated/**"   # all files under lib/generated/
  - "**/*.g.dart"        # every .g.dart file anywhere in the project

3. Inline suppression (// loam-ignore: directive)

Suppress a single Finding for one specific rule at one specific location by placing a // loam-ignore: comment on the same line as the flagged code or on the immediately preceding line:

// loam-ignore: unused-public-exports – exported via barrel file, not direct ref
class MyPublicApi { … }

class AnotherClass { … }  // loam-ignore: unused-public-exports – plugin entry point

Format: // loam-ignore: <ruleId> – <reason>

Scaffold via loam init

loam init writes a commented loam.yaml scaffold that illustrates all three mechanisms. If loam.yaml already exists, the command refuses to overwrite it (exit code 1) — no silent data loss.