Ignore directives

Sometimes a check is right and the code is right anyway. A complexity check flags a function that's deliberately complex because it's a parser. A no-console.log check flags console.log in a CLI binary that's supposed to print to stdout. A no-circular-imports check flags a circular import that exists for a documented reason.

Ignore directives are how you tell the framework "yes, I know — keep going." They're inline source comments scoped to a specific check and a specific location. The framework parses them, filters violations against them, and records what was suppressed.

What you'll understand after this:

- The two directive forms and their scoping rules.

- How directives interact with neighboring linter directives.

- Where they're applied in the pipeline (after the check runs, not before).

- When to use a directive vs. fixing the code vs. baselining.


The two forms

// @fitness-ignore-next-line <slug>      — suppress the next non-directive line
// @fitness-ignore-file <slug>           — suppress every violation in this file

Both expect a check slug as the second token (separated by space or tab). The slug must match the offending check's slug. Directives without a slug are ignored — there's no "ignore everything" form, by design.

The parser recognizes the directive after any of four comment openers, so the same syntax works in TypeScript/JavaScript, Markdown/HTML, and shell/YAML/Python sources — see COMMENT_OPENERS in directive-parsing.ts:

| Opener | Languages |

|---|---|

| // | TypeScript, JavaScript, Rust, Go, Java, C/C++ |

| // | Same set, plus CSS — the directive must appear on the line after / or inline before / |

| <!-- … --> | Markdown, HTML |

| # | Shell, YAML, Python, TOML, Dockerfile |

The doc examples below use //; substitute the correct opener for your file's language.

@fitness-ignore-next-line

Suppresses violations of the named check on the line immediately following the directive. Tolerates a stack of neighboring linter directives (up to three) before locking onto the actual target — so you can write:

// @fitness-ignore-next-line no-console-log
// eslint-disable-next-line no-console
// @ts-expect-error -- intentional
console.log(`[startup] PID ${process.pid}`);

…and the fitness directive lands on the console.log call, even though two unrelated linter directives sit between it and the line. The recognized neighbors are listed in KNOWN_DIRECTIVE_KEYWORDS (packages/fitness/engine/src/framework/directive-parsing.ts): eslint-disable-next-line, eslint-disable-line, @ts-expect-error, @ts-ignore, @ts-nocheck, prettier-ignore, biome-ignore, plus the fitness directives themselves.

@fitness-ignore-file

Suppresses every violation of the named check in the entire file. Must appear in the first 50 lines — directives buried at the bottom of a 5,000-line file aren't found.

// @fitness-ignore-file complex-function -- this is a hand-written parser; complexity is inherent
import { Lexer } from './lexer';
// ... 800 more lines of complexity ...

The trailing -- justification is a convention, not part of the parser. Anything after the slug is ignored by the directive parser but is visible in code review and to humans reading the file. Justifications are encouraged.

What's not supported


Where directives are applied

Directives are applied after a check runs, not before. The flow inside the framework:

1. Check runs, produces Signal[] for the file (no awareness of directives).
2. ignore-processing.ts walks the file content, builds the directive map:
   {
     fileIgnore: Set<slug>,
     lineIgnore: Map<slug, Set<lineNumber>>,
   }
3. The framework filters Signal[]: drop any signal whose slug
   is in fileIgnore OR whose line is in lineIgnore[slug].
4. The applied directives are recorded in DirectiveEntry[] and
   surface in the run summary as `ignoredCount`.

Why after, not before? Two reasons:

The parser implementation lives in packages/fitness/engine/src/framework/directive-parsing.ts. The aggregation that produces the per-run DirectiveEntry[] lives in packages/fitness/engine/src/framework/ignore-processing.ts and packages/fitness/engine/src/framework/directive-inventory.ts.


When to use a directive vs. baselining vs. fixing

Three options when a check fires. Pick by the answer to "is this code wrong?":

The code is wrong → fix it

The default. The check exists because the team agreed the pattern is bad. Fix the code; remove the violation.

The code is right and the check author didn't anticipate this case → directive

A specific, justified exception. The check is right in general; this site is the documented exception. Use a directive with a justification:

// @fitness-ignore-next-line no-console-log -- CLI startup banner is intentional
console.log(banner);

Directives are line-local. They're explicit. They're greppable. They survive review.

The code is right and there are too many sites to mark → baseline

The check fires on dozens of legitimate sites because the rule landed late or because the team's view changed. Use the gate baseline (--gate-save) instead. The baseline grandfathers existing violations and only fails on new ones.

opensip fit --gate-save                # capture today's reality into .runtime/datastore.sqlite
opensip fit --gate-compare             # CI gate from now on

The baseline lives in <project>/opensip-cli/.runtime/datastore.sqlite (gitignored). For CI to share a baseline across runs, upload that file as a workflow artifact on main-branch builds and download it before --gate-compare on PR builds — see 10-concepts/05-architecture-gate.md#ci-integration-patterns.

When NOT to directive

A directive becomes a problem when:

The dashboard's per-tool Catalog subtab and the CLI's --verbose detail view both surface directive counts; a check with hundreds of suppressions across the repo deserves a second look. (There is no separate top-level "Ignored" tab today — the suppressions are inlined into the same panels that show findings.)


How directives appear in output

The DirectiveEntry shape (packages/fitness/engine/src/framework/directive-inventory.ts) carries:

The CLI's --verbose output groups violations by check and the table renderer surfaces an ignored count per row (SignalTableRow.ignored in packages/output/src/format/signal-table.ts, derived from envelope.units[].ignoredCount). The dashboard reads the same session record. The contract-stable JSON output (the SignalEnvelope) carries the per-unit suppression count as units[].ignoredCount (fitness-only), so a CI consumer can read it off --json directly; the full appliedDirectives detail (which directive matched which line) still lives only in the internal session record on disk.

A directive that didn't match any violation (e.g. the targeted check no longer fires there) is also tracked internally. This is how you find stale suppressions: the directive exists in the source, and the framework reports zero violations matched it. A separate housekeeping pass can flag those for cleanup.


Where the example lands

For acme-api:

The CLI's run summary reports: "12 violations found, 5 ignored." CI gates on the 12; the 5 are visible but don't fail the build.


What's next