loam.dev
v0.1.13 · 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 an enclosing Semantics(label: …) ancestor — WCAG 4.1.2 Name, Role, Value. 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: …).
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 into a 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 immediately before a declaration whose normalised text restates the declaration name (e.g. // build before void build()) or belongs to a fixed restatement list (“constructor”, “getter”, “setter”, “build method”). Also catches the <name> method/widget patterns. 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, // comments with informative text that does not match the name or fixed list, and comments not immediately adjacent to a declaration (blank line between).
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.


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 option

--format <format>    Output format (default: human).

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

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

Cyclomatic/cognitive complexity distribution view — shows Health-Score (0–100), Grade (A–F), and a descending Hotspot table (file:line, symbol, cyclomatic, cognitive).

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: three — 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), and slop-narrative-comment (// comments that restate the declaration name or use a fixed narrative phrase). 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. 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.


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.