Files
skills/docs/devtools/tooling/linter/src/unified-lint/unified-lint.md
2026-05-29 08:20:56 +01:00

4.6 KiB

unified-lint

(no submodules)

Purpose

@suaveplan/linter/unified-lint is the orchestrator that composes the biome runner and the markdown fixer into a single pass. It probes biome availability via the injectable CommandRunner, decides whether to invoke biome in check or check-write mode (driven by the fix flag), runs the markdown fixer over the configured patterns, aggregates both lanes into a LintReport, and returns. The orchestrator NEVER calls process.exit; that translation is the CLI's responsibility, which keeps the orchestrator unit-testable and embeddable inside other tools (pre-commit hooks, IDE integrations, language servers).

Features

  • Bi-lane orchestration — Biome for JS/TS/JSON, markdown fixer for .md. Either lane can be skipped (markdownOnly: true skips biome; an unavailable biome binary skips the biome lane and leaves biome: undefined in the report).
  • LintReport aggregation — { biome, markdown, success, durationMs }. success reflects the biome lane's outcome (the markdown lane is structurally always a success once it returns; per-file failures throw a LinterError and short-circuit).
  • Catches BIOME_FAILED from runBiome in check-write mode and reflects it in the report rather than propagating, so write-mode is non-fatal but informational. In check mode a BIOME_FAILED error is propagated to the caller (matching the upstream "fail CI on lint errors" semantic).
  • OTel emission — linter_run_duration_ms{phase="markdown"} and {phase="total"} recorded per run.

Usage: Basic

import { runUnifiedLint } from "@suaveplan/linter/unified-lint";

const report = await runUnifiedLint({ fix: false });
if (!report.success) {
    process.exitCode = 1;
}

Usage: Advanced

Run a markdown-only pass with custom adapters from a test:

import { runUnifiedLint } from "@suaveplan/linter/unified-lint";
import type { CommandRunner, FsAdapter, GlobAdapter } from "@suaveplan/linter";

const store = new Map([["a.md", "# A\n# B\n"]]);
const fs: FsAdapter = {
    async readFile(p) {
        const v = store.get(p);
        if (v === undefined) throw new Error("ENOENT");
        return v;
    },
    async writeFile(p, data) {
        store.set(p, data);
    },
    async exists(p) {
        return store.has(p);
    },
};
const glob: GlobAdapter = {
    async find() {
        return [...store.keys()];
    },
};
const runner: CommandRunner = {
    async run() {
        return { exitCode: 0, stdout: "", stderr: "" };
    },
};

const report = await runUnifiedLint({
    markdownOnly: true,
    patterns: ["*.md"],
    runner,
    fs,
    glob,
});
// report.biome === undefined
// report.markdown[0].changed === true

API Reference

runUnifiedLint(options?): Promise<LintReport>

Options (all optional):

  • cwd?: string — defaults to ".".
  • fix?: boolean — when true, biome runs in "check-write" mode and the markdown fixer writes back changes.
  • markdownOnly?: boolean — skips the biome lane entirely.
  • patterns?: ReadonlyArray<string> — defaults to ["**/*.md"].
  • runner?: CommandRunner — defaults to nodeCommandRunner.
  • fs?: FsAdapter — defaults to nodeFsAdapter.
  • glob?: GlobAdapter — defaults to nodeGlobAdapter.
  • clock?: ClockLike — defaults to Date.now.

Returns a LintReport. Throws LinterError only when the markdown lane fails (any FILE_NOT_FOUND / READ_FAILED / WRITE_FAILED / MARKDOWN_PARSE_FAILED / NO_TARGETS_FOUND is propagated) or when runBiome raises in check mode.

Implementation Notes

  • The biome lane catches LinterError(LINTER.BIOME_FAILED) to surface the embedded report instead of bubbling. This makes the orchestrator's contract simpler — it always returns a LintReport unless the markdown lane fails (which is genuinely unrecoverable: a corrupt file or a missing target).
  • markdownOnly short-circuits BEFORE the probe so a user opting out of biome doesn't pay the cost of even probing for the binary. Conversely, an unavailable binary just yields biome: undefined — quiet skip, not an error.
  • The orchestrator's durationMs measures wall-clock from entry to exit, which includes both lanes' overhead. The histogram is double-tagged: per-lane phases via recordRunDuration("markdown", ...) and recordRunDuration("biome", ...) (the latter fired by runBiome) and the total via recordRunDuration("total", ...).
  • The injected Clock defaults to { now: () => Date.now() }. Tests pass a counter that increments deterministically so durations are stable.