105 lines
6.5 KiB
Markdown
105 lines
6.5 KiB
Markdown
# Proposal — Migrate @suaveplan/linter to genesis
|
|
|
|
- **Design**: ./design.md
|
|
- **Spec delta**: ./specs/linter/spec.md
|
|
|
|
## Why
|
|
|
|
Upstream `@suaveplan/linter` (`/mnt/s/web/suaveplan/packages/server/linter/`) ships a thin
|
|
markdown auto-fixer plus an `execSync`-based Biome / markdownlint orchestrator. The genesis
|
|
`enterprise/linter.md` audit calls out three structural gaps: (a) no error-code registry —
|
|
every failure path either `console.error`s or calls `process.exit(1)` — so consumers cannot
|
|
discriminate, (b) no OpenTelemetry surface for what is the second-most invoked CI step in
|
|
the monorepo (every package runs lint + format on every commit), and (c) the orchestrator
|
|
is hard-wired to spawn subprocesses with no seam for tests or for programmatic embedding
|
|
into pre-commit hooks. The fixed `remark` recipe is correct as far as it goes, but
|
|
violations are written to disk silently with no machine-readable summary, so CI cannot
|
|
gate on "files modified" vs "files clean".
|
|
|
|
Genesis cleans up the contract: every failure raises a typed `LinterError` with a stable
|
|
`LINTER.*` code, every run emits structured `LintReport` results, the orchestrator and the
|
|
shell-out path both take an injectable `CommandRunner` (defaults to `node:child_process`)
|
|
so unit tests use a real fake runner and a single integration test exercises the real
|
|
`bunx biome --version` probe, and every public surface is wrapped in a Zod schema.
|
|
|
|
## What
|
|
|
|
Land `@suaveplan/linter@0.1.0` at `packages/devtools/tooling/linter/` with these feature
|
|
modules (one folder per feature, sibling `.md` per implementation file):
|
|
|
|
- `markdown-fixer/` — `fixMarkdown(filePath, opts?)` / `processMarkdownFiles(opts)` —
|
|
remark-based pipeline fixing MD012 / MD022 / MD025 / MD029 / MD031 / MD032 / MD035 /
|
|
MD036 / MD040 / MD044. Returns a `MarkdownFixReport` (per-file changed flag + rule list).
|
|
- `biome-runner/` — `runBiome({ cwd, mode, runner })` — shells to `bunx biome check` via
|
|
an injectable `CommandRunner` seam (default = `node:child_process` `spawn`). Modes:
|
|
`"check"`, `"check-write"`, `"format-write"`. Returns a `BiomeReport` with parsed
|
|
diagnostic count, exit code, and stdout/stderr.
|
|
- `glob-resolver/` — `resolveMarkdownTargets({ patterns, fs, glob })` — resolves a list of
|
|
patterns (or a single file path) to a sorted, deduplicated list of `.md` paths via
|
|
injectable `FsAdapter` + `GlobAdapter`.
|
|
- `unified-lint/` — `runUnifiedLint(opts)` — orchestrator. Probes biome availability via
|
|
the injectable runner, runs the markdown fixer + biome in fix or check mode, aggregates
|
|
into a `LintReport`. Never calls `process.exit` — returns the report. The CLI is the
|
|
only call site that translates `report.success` into an exit code.
|
|
- `cli/` — `runCli(argv, opts?)` — pure function over argv → `Promise<{ code: number }>`.
|
|
Parses flags (`--fix`/`--write`, `--unified`, `--markdown-only`, `--help`/`-h`,
|
|
`--cwd`), invokes the orchestrator, returns the exit code. The shipped binary
|
|
(`suaveplan-lint`) is a one-line wrapper that calls `runCli(process.argv.slice(2))`
|
|
then `process.exit(code)`.
|
|
- `error-codes/` — `LINTER_CODES` via `defineCodes("LINTER", […])` + `LinterError`.
|
|
- `telemetry/` — OTel counters + histogram for fixes applied, biome runs, and
|
|
orchestrator durations.
|
|
- `types/` — shared `LintReport` / `MarkdownFixReport` / `BiomeReport` /
|
|
`CommandRunner` / `FsAdapter` / `GlobAdapter` types.
|
|
|
|
## Parity Checklist
|
|
|
|
| Capability | Upstream | Migrated | Notes |
|
|
|---|---|---|---|
|
|
| `fixMarkdown(filePath)` (remark pipeline) | ✅ | ✅ | Preserved; now returns `MarkdownFixReport` not `boolean`. |
|
|
| `processMarkdownFiles(args)` glob walker | ✅ | ✅ | Split out into `glob-resolver` + `markdown-fixer`; injectable `fs` / `glob`. |
|
|
| `unifiedLint(args)` orchestrator | ✅ | ⬆️ | Enhanced — injectable `CommandRunner`, returns `LintReport`, no `process.exit`. |
|
|
| MD025 (multiple H1s) fixer | ✅ | ✅ | |
|
|
| MD036 (emphasis-as-heading) fixer | ✅ | ✅ | Plus extended `NEVER/ALWAYS/CRITICAL/IMPORTANT/MANDATORY` heuristics. |
|
|
| MD032 / MD022 / MD031 (blank lines) | ✅ | ✅ | |
|
|
| MD044 (proper-name capitalisation) | ✅ | ✅ | |
|
|
| MD029 (ordered list numbering) | ✅ | ✅ | |
|
|
| MD035 (horizontal-rule style) | ✅ | ✅ | |
|
|
| MD040 (code-block language inference) | ✅ | ✅ | |
|
|
| MD012 (multiple blank lines) | ✅ | ✅ | |
|
|
| Trailing-newline normalisation | ✅ | ✅ | |
|
|
| `**text**` → ` **`text`** ` post-process | ✅ | ✅ | |
|
|
| CLI binary `suaveplan-lint` | ✅ | ✅ | Now a one-line wrapper over the pure `runCli`. |
|
|
| `--help` flag | ✅ | ✅ | |
|
|
| `--fix` / `--write` flag | ✅ | ✅ | |
|
|
| `--unified` / `--markdown-only` flag | ✅ | ✅ | |
|
|
| Error-code registry | 🚫 | ⬆️ | Added — required by genesis. |
|
|
| OTel self-telemetry | 🚫 | ⬆️ | Added — required by genesis. |
|
|
| Zod-validated options | 🚫 | ⬆️ | Added — required by genesis. |
|
|
| Subpath exports per feature | 🚫 | ⬆️ | Added — every feature is its own subpath. |
|
|
| `execSync` Biome / markdownlint shell-out | ✅ | ⬆️ | Replaced with injectable `CommandRunner` (default = real `spawn`); markdownlint shell-out dropped (remark fixer covers the rule set + a real `markdownlint-cli2` is already in `devDependencies` at the repo root for the `bunx markdownlint-cli2 --fix` lane in §13b — re-running it inside `unifiedLint` would double-walk the tree). |
|
|
|
|
## Impact
|
|
|
|
### Affected dependents
|
|
|
|
Run `bunx turbo ls --filter=...@suaveplan/linter` to enumerate. Net-new package on
|
|
genesis — no current dependents. The repo-root `bunx markdownlint-cli2 --fix` lane
|
|
referenced in `openspec/AGENTS.md §13b` is unchanged.
|
|
|
|
- No dependents (verified by `bunx turbo ls --filter=...@suaveplan/linter`).
|
|
|
|
## Risks
|
|
|
|
- **Biome programmatic vs subprocess.** Biome v2 exposes `@biomejs/js-api` but the binary
|
|
resolution behaviour differs across Bun / Node. We stay on `bunx biome check` via the
|
|
`CommandRunner` seam so the surface matches what consumers run by hand. Tests use a
|
|
fake `CommandRunner` that returns canned exit codes and stdout.
|
|
- **remark plugin order matters.** The blank-line fixer must run after the emphasis-as-
|
|
heading fixer (otherwise the rewritten heading isn't surrounded by blanks). Pinned by
|
|
the existing pipeline order — preserved verbatim in migration.
|
|
- **CLI tests must not exit.** The previous package called `process.exit` from library
|
|
code, which made testing impossible. `runCli` returns `{ code }` and only the binary
|
|
wrapper exits. Verified by a spec scenario that calls `runCli(['--help'])` and asserts
|
|
the resolved code without the test harness exiting.
|