Initial Commit
This commit is contained in:
104
openspec/archive/2026-05-25-enterprise-linter/proposal.md
Normal file
104
openspec/archive/2026-05-25-enterprise-linter/proposal.md
Normal file
@@ -0,0 +1,104 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user