Files
2026-05-27 23:29:53 +01:00

4.1 KiB

error-codes

(no submodules)

Purpose

@suaveplan/linter/error-codes owns the stable error-code registry and the typed LinterError class that every other module in the package throws. The registry is expressed through defineCodes("LINTER", [...]) from @suaveplan/error so each code carries the LINTER. namespace prefix and survives across module boundaries, worker threads, RPC envelopes, and serialised error transports without losing its identity. Consumers of the linter — pre-commit hooks, CI gates, IDE integrations — discriminate on error.code to make routing decisions (retry, suppress, surface to the user) without parsing free-form messages, so the registry is a public contract, not an implementation detail.

Features

  • Closed code set declared once via defineCodes — typo-safe, no string literals at call sites.
  • LinterError extends SuaveplanError<LinterCode> — interoperable with every observability adapter shipped in @suaveplan/error (OTel span events, redaction, i18n hooks).
  • Optional details payload preserved on the instance and on the underlying SuaveplanError.context field so structured diagnostics travel with the error.
  • Optional cause forwarded to the native Error.cause chain — preserves the underlying failure (e.g. an EACCES from fs.writeFile) for debugging.

Usage: Basic

import { LINTER_CODES, LinterError } from "@suaveplan/linter/error-codes";

throw new LinterError(LINTER_CODES.NO_TARGETS_FOUND, "no .md files matched", {
    details: { pattern: "docs/**/*.md" },
});

Usage: Advanced

Route errors by code in a CI gate:

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

try {
    const report = await runUnifiedLint({ fix: false });
    if (!report.success) {
        process.exitCode = 1;
    }
} catch (err) {
    if (err instanceof LinterError) {
        switch (err.code) {
            case "LINTER.BIOME_UNAVAILABLE":
                console.warn("biome not installed — skipping JS/TS lane");
                break;
            case "LINTER.NO_TARGETS_FOUND":
                process.exitCode = 0; // empty repo, not a failure
                break;
            default:
                console.error(err.code, err.message, err.details);
                process.exitCode = 2;
        }
    } else {
        throw err;
    }
}

API Reference

LINTER_CODES

A frozen object whose values are the registered LINTER.<REASON> strings. Members: INVALID_OPTIONS, FILE_NOT_FOUND, NO_TARGETS_FOUND, READ_FAILED, WRITE_FAILED, MARKDOWN_PARSE_FAILED, BIOME_UNAVAILABLE, BIOME_FAILED, RUNNER_FAILED, UNKNOWN_FLAG.

type LinterCode

Union of every value in LINTER_CODES.

class LinterError extends SuaveplanError<LinterCode>

Constructor: new LinterError(code, message, options?).

  • code: LinterCode — one of the registered codes.
  • message: string — human-readable summary.
  • options.cause?: unknown — forwarded to Error.cause.
  • options.details?: Record<string, unknown> — stored as both instance.details AND instance.context (via SuaveplanError).

instance.name is set to "LinterError".

Implementation Notes

  • The registry is enumerated by tests (Object.values(LINTER_CODES).sort()) to guarantee the public set matches the spec delta exactly. Adding a code without updating the test fails the gate.
  • details defaults to {} rather than undefined so consumers can safely spread or read fields without a null-check; the spec scenario "defaults details to empty record when omitted" pins this contract.
  • SuaveplanError accepts cause only when defined — passing undefined would set cause: undefined on the chain, which differs from "no cause." The constructor guards this with the options?.cause === undefined ? {} : { cause: options.cause } ternary.
  • The class extends SuaveplanError<LinterCode> rather than SuaveplanError so the code getter is narrowed to the linter's registered union at the type level — IDE autocompletion lists exactly the ten codes.