# Design — @suaveplan/linter ## Tier Server-tier — uses `node:fs` and `node:child_process`. Marked via `"engines.node"` and the absence of a `browser` field. The package never imports from `@suaveplan/*` runtime code other than `@suaveplan/error` and `@suaveplan/types`. ## Layout ```text packages/devtools/tooling/linter/ package.json v0.1.0 — subpath exports per feature tsconfig.lib.json vite.config.ts ESM-first, dts plugin vitest.config.ts forks pool, istanbul, 100/100/100/100 README.md tests/setup/polyfills.ts src/ index.ts Root barrel — explicit named re-exports bin.ts One-line CLI binary entry: process.exit(await runCli(...)) error-codes/ error-codes.ts defineCodes("LINTER", ...) + LinterError error-codes.md error-codes.test.ts index.ts telemetry/ telemetry.ts OTel counters + histogram + record helpers telemetry.md telemetry.test.ts index.ts types/ linter.types.ts Shared interfaces (no .md required) index.ts markdown-fixer/ markdown-fixer.ts fixMarkdown + processMarkdownFiles markdown-fixer.md markdown-fixer.test.ts index.ts biome-runner/ biome-runner.ts runBiome via CommandRunner seam biome-runner.md biome-runner.test.ts index.ts glob-resolver/ glob-resolver.ts resolveMarkdownTargets({ patterns, fs, glob }) glob-resolver.md glob-resolver.test.ts index.ts unified-lint/ unified-lint.ts runUnifiedLint orchestrator unified-lint.md unified-lint.test.ts index.ts cli/ cli.ts runCli(argv, opts?) — pure cli.md cli.test.ts index.ts ``` ## Public types ```ts export interface CommandRunner { run(input: CommandInput): Promise; } export interface CommandInput { command: string; args: ReadonlyArray; cwd?: string; env?: Readonly>; } export interface CommandResult { exitCode: number; stdout: string; stderr: string; } export interface FsAdapter { readFile(path: string): Promise; writeFile(path: string, data: string): Promise; exists(path: string): Promise; } export interface GlobAdapter { find(pattern: string, opts: GlobOptions): Promise>; } export interface GlobOptions { ignore: ReadonlyArray; cwd: string; } export interface MarkdownFixReport { file: string; changed: boolean; rulesApplied: ReadonlyArray; } export interface BiomeReport { mode: "check" | "check-write" | "format-write"; exitCode: number; stdout: string; stderr: string; success: boolean; durationMs: number; } export interface LintReport { biome?: BiomeReport; markdown: ReadonlyArray; success: boolean; durationMs: number; } ``` ## Error codes `defineCodes("LINTER", [...])`: - `INVALID_OPTIONS` — Zod validation fails at the public surface. - `FILE_NOT_FOUND` — single-file path passed to `fixMarkdown` / `resolveMarkdownTargets` does not exist. - `NO_TARGETS_FOUND` — glob expansion produced an empty list. - `READ_FAILED` — `fs.readFile` threw (e.g. permissions, EISDIR). - `WRITE_FAILED` — `fs.writeFile` threw. - `MARKDOWN_PARSE_FAILED` — `unified().process(...)` threw (malformed input). - `BIOME_UNAVAILABLE` — `biome --version` probe via `CommandRunner` returned non-zero. - `BIOME_FAILED` — biome run returned a non-zero exit AND we are in `check` mode (a non-zero exit in `check-write` mode is informational and surfaces via the report). - `RUNNER_FAILED` — `CommandRunner.run` rejected (process spawn failed before exit). - `UNKNOWN_FLAG` — CLI argv contains an unrecognised flag. Every code has at least one Verified-by scenario in the spec delta. ## Telemetry - Counter `linter_files_fixed_total{rule}` — incremented per rule application within `fixMarkdown`. - Counter `linter_files_processed_total{result}` — `result ∈ {"changed", "unchanged"}`. - Counter `linter_biome_runs_total{mode, outcome}` — `outcome ∈ {"success", "failure"}`. - Histogram `linter_run_duration_ms{phase}` — `phase ∈ {"markdown", "biome", "total"}`. All instruments are obtained via `metrics.getMeter("@suaveplan/linter")` — when no `MeterProvider` is registered the API returns no-op instruments so production callers who do not opt into telemetry pay zero cost. ## CommandRunner seam The default exported runner wraps `node:child_process.spawn` (NOT `execSync`) so it is cancellation-friendly and async-native: ```ts export const nodeCommandRunner: CommandRunner = { async run({ command, args, cwd, env }) { return await new Promise((resolve, reject) => { const child = spawn(command, args, { cwd, env: env ? { ...process.env, ...env } : process.env, stdio: ["ignore", "pipe", "pipe"], }); let stdout = ""; let stderr = ""; child.stdout?.on("data", (d) => { stdout += String(d); }); child.stderr?.on("data", (d) => { stderr += String(d); }); child.on("error", reject); child.on("close", (exitCode) => { resolve({ exitCode: exitCode ?? 1, stdout, stderr, }); }); }); }, }; ``` Tests pass a `CommandRunner` whose `run` is a real async function returning canned results — not a `vi.fn` mock. This honours rule 2 (no mocks) while keeping the unit-test runtime hermetic. ## FsAdapter and GlobAdapter defaults ```ts export const nodeFsAdapter: FsAdapter = { async readFile(path) { return fs.readFile(path, "utf-8"); }, async writeFile(path, data) { await fs.writeFile(path, data, "utf-8"); }, async exists(path) { try { await fs.access(path); return true; } catch { return false; } }, }; export const nodeGlobAdapter: GlobAdapter = { async find(pattern, { ignore, cwd }) { return glob(pattern, { ignore: [...ignore], cwd }); }, }; ``` Tests pass in-memory adapters (a `Map` for fs; a function returning a canned list for glob) — again, real fakes, not `vi.mock`. ## CLI argv parsing Trivial hand-rolled parser — no `commander` dependency for one binary with five flags. Returns `{ mode, fix, patterns, cwd, showHelp }`. Unknown flags raise `LinterError(LINTER.UNKNOWN_FLAG)` so the binary wrapper prints the message and exits non-zero rather than silently treating the flag as a positional pattern. ## Subpath exports Every feature folder gets its own subpath entry so consumers can import only what they need (`@suaveplan/linter/markdown-fixer`, `/biome-runner`, etc.) and pay zero cost for the features they don't use. The root barrel re-exports every public symbol so the default `@suaveplan/linter` import still works.