first commit
This commit is contained in:
122
docs/devtools/tooling/linter/src/unified-lint/unified-lint.md
Normal file
122
docs/devtools/tooling/linter/src/unified-lint/unified-lint.md
Normal file
@@ -0,0 +1,122 @@
|
||||
# `unified-lint`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## 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
|
||||
|
||||
```ts
|
||||
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:
|
||||
|
||||
```ts
|
||||
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.
|
||||
Reference in New Issue
Block a user