Initial Commit
This commit is contained in:
229
openspec/archive/2026-05-25-enterprise-linter/design.md
Normal file
229
openspec/archive/2026-05-25-enterprise-linter/design.md
Normal file
@@ -0,0 +1,229 @@
|
||||
# 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<CommandResult>;
|
||||
}
|
||||
export interface CommandInput {
|
||||
command: string;
|
||||
args: ReadonlyArray<string>;
|
||||
cwd?: string;
|
||||
env?: Readonly<Record<string, string>>;
|
||||
}
|
||||
export interface CommandResult {
|
||||
exitCode: number;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
}
|
||||
|
||||
export interface FsAdapter {
|
||||
readFile(path: string): Promise<string>;
|
||||
writeFile(path: string, data: string): Promise<void>;
|
||||
exists(path: string): Promise<boolean>;
|
||||
}
|
||||
|
||||
export interface GlobAdapter {
|
||||
find(pattern: string, opts: GlobOptions): Promise<ReadonlyArray<string>>;
|
||||
}
|
||||
export interface GlobOptions {
|
||||
ignore: ReadonlyArray<string>;
|
||||
cwd: string;
|
||||
}
|
||||
|
||||
export interface MarkdownFixReport {
|
||||
file: string;
|
||||
changed: boolean;
|
||||
rulesApplied: ReadonlyArray<string>;
|
||||
}
|
||||
|
||||
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<MarkdownFixReport>;
|
||||
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<CommandResult>((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<string, string>` 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.
|
||||
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.
|
||||
@@ -0,0 +1,436 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec Delta: linter
|
||||
|
||||
**Package**: `@suaveplan/linter`
|
||||
**Status**: PROPOSED
|
||||
|
||||
> Every requirement below carries a `**Verified by:**` line at authoring time. No exceptions.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: LINTER_CODES registry MUST enumerate every typed failure path
|
||||
|
||||
The package MUST export `LINTER_CODES = defineCodes("LINTER", [...])` exposing exactly the
|
||||
following codes: `INVALID_OPTIONS`, `FILE_NOT_FOUND`, `NO_TARGETS_FOUND`, `READ_FAILED`,
|
||||
`WRITE_FAILED`, `MARKDOWN_PARSE_FAILED`, `BIOME_UNAVAILABLE`, `BIOME_FAILED`,
|
||||
`RUNNER_FAILED`, `UNKNOWN_FLAG`. Every code MUST be reachable from a call site exercised
|
||||
by a test in `src/`. The exported `LinterError` MUST extend `SuaveplanError<LinterCode>`
|
||||
and carry a `details: Record<string, unknown>` field for structured diagnostics.
|
||||
|
||||
#### Scenario: registry exposes the full code set
|
||||
|
||||
- **WHEN** the consumer imports `LINTER_CODES` and reads `Object.values(LINTER_CODES)`
|
||||
- **THEN** the result contains exactly the ten declared codes (no more, no less)
|
||||
|
||||
#### Scenario: LinterError carries code and details
|
||||
|
||||
- **WHEN** `new LinterError(LINTER_CODES.INVALID_OPTIONS, "bad", { details: { field: "x" } })`
|
||||
- **THEN** the instance has `code === "LINTER.INVALID_OPTIONS"`,
|
||||
`details.field === "x"`, and `instanceof SuaveplanError === true`
|
||||
|
||||
**Verified by:** `src/error-codes/error-codes.test.ts::"registry exposes the full code set"` AND `src/error-codes/error-codes.test.ts::"LinterError carries code and details payload"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: fixMarkdown applies the full remark rule set and returns a MarkdownFixReport
|
||||
|
||||
`fixMarkdown(filePath: string, opts?: { fs?: FsAdapter }): Promise<MarkdownFixReport>`
|
||||
MUST read the file via the injectable `FsAdapter` (defaulting to `nodeFsAdapter`), run
|
||||
the remark pipeline (MD025, MD036 + enhanced patterns, MD029, MD035, MD040, MD032 /
|
||||
MD022 / MD031, MD044, MD012, trailing-newline normalisation, bold-without-backticks
|
||||
post-process), write the result back only when content changed, and return
|
||||
`{ file, changed, rulesApplied }` where `rulesApplied` lists the rule IDs that altered
|
||||
the AST. A missing file MUST raise `LinterError(LINTER.FILE_NOT_FOUND)`.
|
||||
|
||||
#### Scenario: collapses multiple H1s into H2s after the first
|
||||
|
||||
- **WHEN** `fixMarkdown('test.md')` runs on content `# A\n\n# B\n\n# C`
|
||||
- **THEN** the written content contains `# A`, `## B`, `## C`
|
||||
AND the returned report has `changed === true` AND `"MD025"` ∈ `rulesApplied`
|
||||
|
||||
#### Scenario: rewrites step-style strong paragraph to H3
|
||||
|
||||
- **WHEN** `fixMarkdown` runs on `**Step 1: PLAN**\n\nbody`
|
||||
- **THEN** the written content contains `### Step 1: PLAN`
|
||||
AND `"MD036"` ∈ `rulesApplied`
|
||||
|
||||
#### Scenario: infers bash language for code blocks
|
||||
|
||||
- **WHEN** `fixMarkdown` runs on a fenced block with no language whose body contains `npm install`
|
||||
- **THEN** the written content contains ` ```bash ` AND `"MD040"` ∈ `rulesApplied`
|
||||
|
||||
#### Scenario: infers typescript language for code blocks
|
||||
|
||||
- **WHEN** `fixMarkdown` runs on a fenced block containing `interface User { name: string }`
|
||||
- **THEN** the written content contains ` ```typescript `
|
||||
|
||||
#### Scenario: infers python language for code blocks
|
||||
|
||||
- **WHEN** `fixMarkdown` runs on a fenced block containing `def main():\n pass`
|
||||
- **THEN** the written content contains ` ```python `
|
||||
|
||||
#### Scenario: infers markdown language for code blocks
|
||||
|
||||
- **WHEN** `fixMarkdown` runs on a fenced block whose body is `# Title\n## Sub`
|
||||
- **THEN** the written content contains ` ```markdown `
|
||||
|
||||
#### Scenario: infers html language for code blocks
|
||||
|
||||
- **WHEN** `fixMarkdown` runs on a fenced block whose body is `<div>x</div>`
|
||||
- **THEN** the written content contains ` ```html `
|
||||
|
||||
#### Scenario: infers json language for code blocks
|
||||
|
||||
- **WHEN** `fixMarkdown` runs on a fenced block whose body is `{ "a": 1 }`
|
||||
- **THEN** the written content contains ` ```json `
|
||||
|
||||
#### Scenario: defaults to text language for unrecognised content
|
||||
|
||||
- **WHEN** `fixMarkdown` runs on a fenced block whose body is opaque (`zzz qqq`)
|
||||
- **THEN** the written content contains ` ```text `
|
||||
|
||||
#### Scenario: collapses multiple blank lines
|
||||
|
||||
- **WHEN** `fixMarkdown` runs on content with `\n\n\n\n` between paragraphs
|
||||
- **THEN** the written content contains no `\n\n\n` AND `"MD012"` ∈ `rulesApplied`
|
||||
|
||||
#### Scenario: capitalises proper names
|
||||
|
||||
- **WHEN** `fixMarkdown` runs on content `This is a json api over http.`
|
||||
- **THEN** the written content contains `JSON`, `API`, `HTTP`
|
||||
AND `"MD044"` ∈ `rulesApplied`
|
||||
|
||||
#### Scenario: ensures trailing single newline
|
||||
|
||||
- **WHEN** `fixMarkdown` runs on content with three trailing newlines
|
||||
- **THEN** the written content ends with exactly one `\n`
|
||||
|
||||
#### Scenario: rewrites bold heading-style line with backticks
|
||||
|
||||
- **WHEN** `fixMarkdown` runs on a paragraph line `**API**`
|
||||
- **THEN** the written content contains ` **`API`** `
|
||||
|
||||
#### Scenario: returns changed=false when content is already canonical
|
||||
|
||||
- **WHEN** `fixMarkdown` runs on already-canonical content
|
||||
- **THEN** the report has `changed === false` AND no write occurs (verified via the injectable fs)
|
||||
|
||||
#### Scenario: raises FILE_NOT_FOUND when the path does not exist
|
||||
|
||||
- **WHEN** `fixMarkdown('missing.md')` is called with an in-memory `FsAdapter` whose `exists` returns `false`
|
||||
- **THEN** a `LinterError` is thrown with `code === "LINTER.FILE_NOT_FOUND"`
|
||||
|
||||
#### Scenario: raises READ_FAILED when the FsAdapter throws on read
|
||||
|
||||
- **WHEN** `fixMarkdown` is called with a `FsAdapter.readFile` that rejects
|
||||
- **THEN** a `LinterError` is thrown with `code === "LINTER.READ_FAILED"` AND the original error as `cause`
|
||||
|
||||
#### Scenario: raises WRITE_FAILED when the FsAdapter throws on write
|
||||
|
||||
- **WHEN** `fixMarkdown` is called with a `FsAdapter.writeFile` that rejects AND content changed
|
||||
- **THEN** a `LinterError` is thrown with `code === "LINTER.WRITE_FAILED"`
|
||||
|
||||
#### Scenario: raises MARKDOWN_PARSE_FAILED when remark rejects the input
|
||||
|
||||
- **WHEN** `fixMarkdown` is called with a `FsAdapter` whose `readFile` returns input that causes `unified().process` to throw (forced via a poisoned processor seam)
|
||||
- **THEN** a `LinterError` is thrown with `code === "LINTER.MARKDOWN_PARSE_FAILED"`
|
||||
|
||||
**Verified by:** `src/markdown-fixer/markdown-fixer.test.ts::"collapses multiple H1s into H2s after the first"` AND `src/markdown-fixer/markdown-fixer.test.ts::"rewrites step-style strong paragraph to H3"` AND `src/markdown-fixer/markdown-fixer.test.ts::"infers bash language for code blocks"` AND `src/markdown-fixer/markdown-fixer.test.ts::"infers typescript language for code blocks"` AND `src/markdown-fixer/markdown-fixer.test.ts::"infers python language for code blocks"` AND `src/markdown-fixer/markdown-fixer.test.ts::"infers markdown language for code blocks"` AND `src/markdown-fixer/markdown-fixer.test.ts::"infers html language for code blocks"` AND `src/markdown-fixer/markdown-fixer.test.ts::"infers json language for code blocks"` AND `src/markdown-fixer/markdown-fixer.test.ts::"defaults to text language for unrecognised content"` AND `src/markdown-fixer/markdown-fixer.test.ts::"collapses multiple blank lines"` AND `src/markdown-fixer/markdown-fixer.test.ts::"capitalises proper names"` AND `src/markdown-fixer/markdown-fixer.test.ts::"ensures trailing single newline"` AND `src/markdown-fixer/markdown-fixer.test.ts::"rewrites bold heading-style line with backticks"` AND `src/markdown-fixer/markdown-fixer.test.ts::"returns changed=false when content is already canonical"` AND `src/markdown-fixer/markdown-fixer.test.ts::"raises FILE_NOT_FOUND when the path does not exist"` AND `src/markdown-fixer/markdown-fixer.test.ts::"raises READ_FAILED when the FsAdapter throws on read"` AND `src/markdown-fixer/markdown-fixer.test.ts::"raises WRITE_FAILED when the FsAdapter throws on write"` AND `src/markdown-fixer/markdown-fixer.test.ts::"raises MARKDOWN_PARSE_FAILED when remark rejects the input"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: processMarkdownFiles walks a pattern and applies fixMarkdown to each match
|
||||
|
||||
`processMarkdownFiles({ patterns, fs?, glob? }): Promise<MarkdownFixReport[]>` MUST resolve
|
||||
the patterns via `resolveMarkdownTargets`, apply `fixMarkdown` to each match in
|
||||
deterministic sorted order, and return one report per file. Empty pattern lists default
|
||||
to `["**/*.md"]`. An empty result set MUST raise
|
||||
`LinterError(LINTER.NO_TARGETS_FOUND)`.
|
||||
|
||||
#### Scenario: walks a glob and returns per-file reports
|
||||
|
||||
- **WHEN** `processMarkdownFiles({ patterns: ["**/*.md"], fs, glob })` runs over an in-memory tree with `a.md` (changed) and `b.md` (unchanged)
|
||||
- **THEN** the result has length 2 AND `result[0].file === 'a.md'` AND `result[1].file === 'b.md'`
|
||||
AND `result[0].changed === true` AND `result[1].changed === false`
|
||||
|
||||
#### Scenario: defaults to **/*.md when patterns is empty
|
||||
|
||||
- **WHEN** `processMarkdownFiles({ patterns: [], fs, glob })` is called
|
||||
- **THEN** the glob adapter is invoked with pattern `"**/*.md"`
|
||||
|
||||
#### Scenario: raises NO_TARGETS_FOUND when nothing matches
|
||||
|
||||
- **WHEN** `processMarkdownFiles({ patterns: ["nope/*.md"], fs, glob })` returns an empty list
|
||||
- **THEN** a `LinterError` is thrown with `code === "LINTER.NO_TARGETS_FOUND"`
|
||||
|
||||
**Verified by:** `src/markdown-fixer/markdown-fixer.test.ts::"walks a glob and returns per-file reports"` AND `src/markdown-fixer/markdown-fixer.test.ts::"defaults to **/*.md when patterns is empty"` AND `src/markdown-fixer/markdown-fixer.test.ts::"raises NO_TARGETS_FOUND when nothing matches"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: runBiome invokes Biome via an injectable CommandRunner and returns a BiomeReport
|
||||
|
||||
`runBiome({ cwd, mode, runner })` MUST shell to `bunx biome` with arguments derived from
|
||||
`mode` (`"check"` → `["biome", "check", "."]`, `"check-write"` →
|
||||
`["biome", "check", "--write", "--unsafe", "--max-diagnostics", "500", "."]`,
|
||||
`"format-write"` →
|
||||
`["biome", "format", "--write", "--max-diagnostics", "500", "."]`)
|
||||
via the injectable `CommandRunner` (default = `nodeCommandRunner`). It MUST measure
|
||||
duration via the injectable `Clock` (default: `Date.now`), populate `BiomeReport.success`
|
||||
as `exitCode === 0`, and return the full report. In `"check"` mode, an exit code other
|
||||
than 0 raises `LinterError(LINTER.BIOME_FAILED)` carrying the report under `details`.
|
||||
Invalid `mode` values raise `LinterError(LINTER.INVALID_OPTIONS)`.
|
||||
|
||||
#### Scenario: passes the correct args for each mode
|
||||
|
||||
- **WHEN** `runBiome({ cwd: '.', mode: 'check', runner })` is called with a recording fake runner
|
||||
- **THEN** the runner recorded `command === 'bunx'` AND `args` starts with `['biome', 'check', '.']`
|
||||
|
||||
#### Scenario: check-write mode forwards --write and --unsafe
|
||||
|
||||
- **WHEN** `runBiome({ cwd: '.', mode: 'check-write', runner })` is called
|
||||
- **THEN** the recorded args include `'--write'` AND `'--unsafe'`
|
||||
|
||||
#### Scenario: format-write mode runs biome format
|
||||
|
||||
- **WHEN** `runBiome({ cwd: '.', mode: 'format-write', runner })` is called
|
||||
- **THEN** the recorded args[1] equals `'format'` AND include `'--write'`
|
||||
|
||||
#### Scenario: report carries exit code and stdout
|
||||
|
||||
- **WHEN** the fake runner returns `{ exitCode: 0, stdout: 'ok', stderr: '' }`
|
||||
- **THEN** the report has `success === true`, `exitCode === 0`, `stdout === 'ok'`,
|
||||
AND `durationMs >= 0`
|
||||
|
||||
#### Scenario: raises BIOME_FAILED in check mode on non-zero exit
|
||||
|
||||
- **WHEN** `runBiome({ cwd: '.', mode: 'check', runner })` is called with a runner that returns `exitCode: 1`
|
||||
- **THEN** a `LinterError` is thrown with `code === "LINTER.BIOME_FAILED"`
|
||||
AND `error.details.report.exitCode === 1`
|
||||
|
||||
#### Scenario: check-write mode returns non-zero report without throwing
|
||||
|
||||
- **WHEN** `runBiome({ cwd: '.', mode: 'check-write', runner })` is called with `exitCode: 1`
|
||||
- **THEN** the call resolves to a report with `success === false`
|
||||
|
||||
#### Scenario: raises RUNNER_FAILED when the runner rejects
|
||||
|
||||
- **WHEN** the runner's `run` rejects with an error
|
||||
- **THEN** a `LinterError` is thrown with `code === "LINTER.RUNNER_FAILED"` AND the underlying error as `cause`
|
||||
|
||||
#### Scenario: raises INVALID_OPTIONS on a bad mode
|
||||
|
||||
- **WHEN** `runBiome({ cwd: '.', mode: 'nope' as 'check', runner })` is called
|
||||
- **THEN** a `LinterError` is thrown with `code === "LINTER.INVALID_OPTIONS"`
|
||||
|
||||
**Verified by:** `src/biome-runner/biome-runner.test.ts::"passes the correct args for check mode"` AND `src/biome-runner/biome-runner.test.ts::"check-write mode forwards --write and --unsafe"` AND `src/biome-runner/biome-runner.test.ts::"format-write mode runs biome format"` AND `src/biome-runner/biome-runner.test.ts::"report carries exit code and stdout"` AND `src/biome-runner/biome-runner.test.ts::"raises BIOME_FAILED in check mode on non-zero exit"` AND `src/biome-runner/biome-runner.test.ts::"check-write mode returns non-zero report without throwing"` AND `src/biome-runner/biome-runner.test.ts::"raises RUNNER_FAILED when the runner rejects"` AND `src/biome-runner/biome-runner.test.ts::"raises INVALID_OPTIONS on a bad mode"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: probeBiomeAvailable verifies biome is installed via an injectable runner
|
||||
|
||||
`probeBiomeAvailable({ runner }): Promise<boolean>` MUST invoke
|
||||
`bunx biome --version` via the runner and return `true` iff `exitCode === 0`. A runner
|
||||
rejection MUST resolve to `false` (probe is non-fatal). The unified-lint orchestrator
|
||||
MUST use this probe and skip the Biome lane when it returns `false` (raising
|
||||
`LinterError(LINTER.BIOME_UNAVAILABLE)` is reserved for callers who opt into strict
|
||||
mode).
|
||||
|
||||
#### Scenario: returns true when biome --version succeeds
|
||||
|
||||
- **WHEN** `probeBiomeAvailable({ runner })` is called with a runner returning `exitCode: 0`
|
||||
- **THEN** the result is `true`
|
||||
|
||||
#### Scenario: returns false on non-zero exit
|
||||
|
||||
- **WHEN** the runner returns `exitCode: 127`
|
||||
- **THEN** the result is `false`
|
||||
|
||||
#### Scenario: returns false when the runner rejects
|
||||
|
||||
- **WHEN** the runner's `run` rejects
|
||||
- **THEN** the result is `false`
|
||||
|
||||
**Verified by:** `src/biome-runner/biome-runner.test.ts::"probe returns true when biome --version succeeds"` AND `src/biome-runner/biome-runner.test.ts::"probe returns false on non-zero exit"` AND `src/biome-runner/biome-runner.test.ts::"probe returns false when the runner rejects"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: resolveMarkdownTargets expands patterns deterministically with ignore defaults
|
||||
|
||||
`resolveMarkdownTargets({ patterns, fs, glob, cwd? }): Promise<string[]>` MUST treat a
|
||||
pattern equal to an existing `.md` file as a single-file pass-through, MUST apply the
|
||||
default ignore list `["node_modules/**", "**/node_modules/**", "dist/**", "**/dist/**"]`
|
||||
on every glob invocation, MUST return the unique sorted list of matches across all
|
||||
patterns, and MUST raise `LinterError(LINTER.FILE_NOT_FOUND)` when a pattern looks like a
|
||||
file path (ends in `.md` AND has no glob magic chars `* ? [ {`) but does not exist.
|
||||
|
||||
#### Scenario: single-file pattern bypasses the glob
|
||||
|
||||
- **WHEN** `resolveMarkdownTargets({ patterns: ['README.md'], fs, glob })` is called and `fs.exists('README.md')` resolves true
|
||||
- **THEN** the result is `['README.md']` AND the glob adapter was NOT called
|
||||
|
||||
#### Scenario: applies the default ignore list
|
||||
|
||||
- **WHEN** `resolveMarkdownTargets({ patterns: ['**/*.md'], fs, glob })` is called with a recording glob
|
||||
- **THEN** the recorded `opts.ignore` contains `'node_modules/**'` AND `'dist/**'`
|
||||
|
||||
#### Scenario: deduplicates across patterns
|
||||
|
||||
- **WHEN** two patterns both match the same file
|
||||
- **THEN** the file appears exactly once in the sorted result
|
||||
|
||||
#### Scenario: raises FILE_NOT_FOUND for a missing file path
|
||||
|
||||
- **WHEN** `resolveMarkdownTargets({ patterns: ['missing.md'], fs, glob })` is called and `fs.exists('missing.md')` resolves false
|
||||
- **THEN** a `LinterError` is thrown with `code === "LINTER.FILE_NOT_FOUND"`
|
||||
|
||||
**Verified by:** `src/glob-resolver/glob-resolver.test.ts::"single-file pattern bypasses the glob"` AND `src/glob-resolver/glob-resolver.test.ts::"applies the default ignore list"` AND `src/glob-resolver/glob-resolver.test.ts::"deduplicates across patterns"` AND `src/glob-resolver/glob-resolver.test.ts::"raises FILE_NOT_FOUND for a missing file path"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: runUnifiedLint orchestrates markdown + biome and returns a LintReport
|
||||
|
||||
`runUnifiedLint(opts): Promise<LintReport>` MUST: (1) probe biome availability via the
|
||||
injectable runner; (2) when available, run `runBiome` in the mode derived from
|
||||
`opts.fix` (`"check-write"` when `fix === true`, otherwise `"check"`); (3) when
|
||||
`opts.markdownOnly !== true`, run `processMarkdownFiles`; (4) aggregate timings via the
|
||||
injectable `Clock`; (5) return `{ biome, markdown, success, durationMs }` where
|
||||
`success === (biome?.success ?? true)` AND every markdown report had a successful
|
||||
read/write. The function MUST NOT call `process.exit`. When biome is unavailable, the
|
||||
report has `biome === undefined`.
|
||||
|
||||
#### Scenario: in fix mode runs biome check-write and markdown processor
|
||||
|
||||
- **WHEN** `runUnifiedLint({ fix: true, cwd: '.', runner, fs, glob })` is called with a runner where the probe + check-write both succeed
|
||||
- **THEN** the result has `biome?.mode === 'check-write'` AND `markdown.length > 0`
|
||||
AND `success === true`
|
||||
|
||||
#### Scenario: in check mode runs biome check and processes no writes
|
||||
|
||||
- **WHEN** `runUnifiedLint({ fix: false, cwd: '.', runner, fs, glob })` is called
|
||||
- **THEN** `biome?.mode === 'check'`
|
||||
|
||||
#### Scenario: markdownOnly skips biome entirely
|
||||
|
||||
- **WHEN** `runUnifiedLint({ markdownOnly: true, runner, fs, glob })` is called
|
||||
- **THEN** the report has `biome === undefined`
|
||||
|
||||
#### Scenario: biome unavailable yields biome:undefined and success based on markdown
|
||||
|
||||
- **WHEN** the probe runner returns `exitCode: 127`
|
||||
- **THEN** the report has `biome === undefined` AND `success` reflects the markdown lane only
|
||||
|
||||
#### Scenario: orchestrator never calls process.exit
|
||||
|
||||
- **WHEN** any of the above scenarios run
|
||||
- **THEN** the function returns a resolved promise (the test asserts on the value, not on the process)
|
||||
|
||||
**Verified by:** `src/unified-lint/unified-lint.test.ts::"in fix mode runs biome check-write and markdown processor"` AND `src/unified-lint/unified-lint.test.ts::"in check mode runs biome check and processes no writes"` AND `src/unified-lint/unified-lint.test.ts::"markdownOnly skips biome entirely"` AND `src/unified-lint/unified-lint.test.ts::"biome unavailable yields biome undefined and success based on markdown"` AND `src/unified-lint/unified-lint.test.ts::"orchestrator never calls process exit"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: runCli parses argv and resolves an exit code without exiting the process
|
||||
|
||||
`runCli(argv: ReadonlyArray<string>, opts?): Promise<{ code: number }>` MUST: (1) print
|
||||
help text and resolve `{ code: 0 }` for `--help` or `-h`; (2) treat any flag not in
|
||||
`--help`, `-h`, `--fix`, `--write`, `--unified`, `--markdown-only`, `--cwd` as an
|
||||
unknown flag and raise `LinterError(LINTER.UNKNOWN_FLAG)`; (3) translate `--fix`
|
||||
or `--write` to `fix: true`; (4) collect positional args as patterns; (5) invoke
|
||||
`runUnifiedLint` with the parsed options; (6) resolve `{ code: 0 }` on
|
||||
`report.success === true`, otherwise `{ code: 1 }`. The function MUST accept an
|
||||
injectable `out: (line: string) => void` for tests.
|
||||
|
||||
#### Scenario: --help prints usage and resolves code 0
|
||||
|
||||
- **WHEN** `runCli(['--help'], { out, runner, fs, glob })` is called
|
||||
- **THEN** `out` was called with a string containing `"Usage:"` AND the result is `{ code: 0 }`
|
||||
|
||||
#### Scenario: -h is an alias for --help
|
||||
|
||||
- **WHEN** `runCli(['-h'], { out, runner, fs, glob })` is called
|
||||
- **THEN** the result is `{ code: 0 }`
|
||||
|
||||
#### Scenario: --fix sets fix mode
|
||||
|
||||
- **WHEN** `runCli(['--fix'], { out, runner, fs, glob })` is called with a runner whose probe + check-write succeed
|
||||
- **THEN** the recorded runner args include `'--write'` AND the result is `{ code: 0 }`
|
||||
|
||||
#### Scenario: --write is an alias for --fix
|
||||
|
||||
- **WHEN** `runCli(['--write'], { out, runner, fs, glob })` is called
|
||||
- **THEN** the recorded runner args include `'--write'`
|
||||
|
||||
#### Scenario: --markdown-only skips biome
|
||||
|
||||
- **WHEN** `runCli(['--markdown-only'], { out, runner, fs, glob })` is called
|
||||
- **THEN** the runner was NOT invoked
|
||||
|
||||
#### Scenario: positional pattern is forwarded to the resolver
|
||||
|
||||
- **WHEN** `runCli(['docs/*.md'], { out, runner, fs, glob })` is called with a glob adapter that captures `pattern`
|
||||
- **THEN** the glob adapter was invoked with pattern `'docs/*.md'`
|
||||
|
||||
#### Scenario: unknown flag raises UNKNOWN_FLAG
|
||||
|
||||
- **WHEN** `runCli(['--nope'], { out, runner, fs, glob })` is called
|
||||
- **THEN** a `LinterError` is thrown with `code === "LINTER.UNKNOWN_FLAG"`
|
||||
|
||||
#### Scenario: failing report yields exit code 1
|
||||
|
||||
- **WHEN** `runCli([], { out, runner, fs, glob })` is called with a runner whose biome check returns `exitCode: 1` (i.e. `runBiome` throws BIOME_FAILED, the CLI catches it)
|
||||
- **THEN** the result is `{ code: 1 }` AND `out` was called with a string containing the failure summary
|
||||
|
||||
**Verified by:** `src/cli/cli.test.ts::"--help prints usage and resolves code 0"` AND `src/cli/cli.test.ts::"-h is an alias for --help"` AND `src/cli/cli.test.ts::"--fix sets fix mode"` AND `src/cli/cli.test.ts::"--write is an alias for --fix"` AND `src/cli/cli.test.ts::"--markdown-only skips biome"` AND `src/cli/cli.test.ts::"positional pattern is forwarded to the resolver"` AND `src/cli/cli.test.ts::"unknown flag raises UNKNOWN_FLAG"` AND `src/cli/cli.test.ts::"failing report yields exit code 1"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: telemetry instruments use the @suaveplan/linter meter and record helpers
|
||||
|
||||
The package MUST expose `filesFixedCounter`, `filesProcessedCounter`,
|
||||
`biomeRunsCounter`, and `runDurationHistogram` obtained via
|
||||
`metrics.getMeter("@suaveplan/linter")`, plus `recordFileFixed(rule)`,
|
||||
`recordFileProcessed(result)`, `recordBiomeRun(mode, outcome)`, and
|
||||
`recordRunDuration(phase, ms)` helpers. Calls to the helpers MUST be no-ops when no
|
||||
`MeterProvider` is registered. `fixMarkdown`, `runBiome`, and `runUnifiedLint` MUST emit
|
||||
the appropriate instruments during normal operation.
|
||||
|
||||
#### Scenario: helpers do not throw with no provider registered
|
||||
|
||||
- **WHEN** `recordFileFixed('MD025')`, `recordFileProcessed('changed')`, `recordBiomeRun('check', 'success')`, `recordRunDuration('total', 5)` are called
|
||||
- **THEN** no exception is thrown AND the calls return `undefined`
|
||||
|
||||
#### Scenario: counter and histogram are wired and emit
|
||||
|
||||
- **WHEN** an instrumented `MeterProvider` is registered AND `fixMarkdown` runs successfully on a changed file
|
||||
- **THEN** `linter_files_processed_total{result="changed"}` increments by 1
|
||||
AND at least one `linter_files_fixed_total{rule=...}` series increments
|
||||
|
||||
**Verified by:** `src/telemetry/telemetry.test.ts::"record helpers do not throw with no provider registered"` AND `src/telemetry/telemetry.test.ts::"counter and histogram are wired and emit through real MeterProvider"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: package surfaces subpath exports for every feature module
|
||||
|
||||
`package.json` MUST declare subpath exports for each of `./markdown-fixer`,
|
||||
`./biome-runner`, `./glob-resolver`, `./unified-lint`, `./cli`, `./error-codes`,
|
||||
`./telemetry`, in addition to the root `"."` entry. Each subpath MUST point to a
|
||||
matching `dist/<feature>/index.{js,d.ts}` bundle so consumers tree-shake at the feature
|
||||
boundary. The package MUST set `"sideEffects": false` and MUST register a
|
||||
`suaveplan-lint` bin pointing at `dist/bin.js`.
|
||||
|
||||
#### Scenario: every feature folder has a matching subpath export
|
||||
|
||||
- **WHEN** the test enumerates `Object.keys(packageJson.exports)` and the `src/<feature>/` folders
|
||||
- **THEN** every feature folder name appears as a `./<feature>` key in `exports`
|
||||
|
||||
#### Scenario: sideEffects is false
|
||||
|
||||
- **WHEN** the test reads `packageJson.sideEffects`
|
||||
- **THEN** the value is `false`
|
||||
|
||||
#### Scenario: bin entry is registered
|
||||
|
||||
- **WHEN** the test reads `packageJson.bin['suaveplan-lint']`
|
||||
- **THEN** the value points at `./dist/bin.js`
|
||||
|
||||
**Verified by:** `tests/package-shape.test.ts::"every feature folder has a matching subpath export"` AND `tests/package-shape.test.ts::"sideEffects is false"` AND `tests/package-shape.test.ts::"bin entry is registered"`
|
||||
30
openspec/archive/2026-05-25-enterprise-linter/tasks.md
Normal file
30
openspec/archive/2026-05-25-enterprise-linter/tasks.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# Tasks — Migrate @suaveplan/linter
|
||||
|
||||
1. [x] Author proposal.md, design.md, spec delta.
|
||||
2. [x] Add `markdown` catalog to root `package.json`.
|
||||
3. [x] Scaffold package at `packages/devtools/tooling/linter/` with `package.json`
|
||||
(v0.1.0, subpath exports per feature), `tsconfig.lib.json`, `vite.config.ts`,
|
||||
`vitest.config.ts`, `README.md`, `tests/setup/polyfills.ts`.
|
||||
4. [x] Implement `src/error-codes/error-codes.ts` with
|
||||
`defineCodes("LINTER", [...])` and `LinterError`.
|
||||
5. [x] Implement `src/telemetry/telemetry.ts` with the 3 counters + 1 histogram and
|
||||
helpers.
|
||||
6. [x] Implement `src/types/linter.types.ts` with the shared interfaces.
|
||||
7. [x] Implement `src/markdown-fixer/markdown-fixer.ts` (remark pipeline + processor).
|
||||
8. [x] Implement `src/biome-runner/biome-runner.ts` (CommandRunner seam, `runBiome`).
|
||||
9. [x] Implement `src/glob-resolver/glob-resolver.ts`
|
||||
(`resolveMarkdownTargets` with injectable fs + glob).
|
||||
10. [x] Implement `src/unified-lint/unified-lint.ts` (orchestrator).
|
||||
11. [x] Implement `src/cli/cli.ts` (pure `runCli(argv, opts?)`).
|
||||
12. [x] Implement `src/bin.ts` — one-line binary entry.
|
||||
13. [x] Write sibling `.md` doc per implementation file (six required sections, ≥200
|
||||
words, real-import basic + advanced examples).
|
||||
14. [x] Write `.test.ts` files exercising every spec scenario; import test-runner
|
||||
primitives from `@suaveplan/testing/runner`.
|
||||
15. [x] Run `bunx turbo lint typecheck test build --filter=@suaveplan/linter` — green.
|
||||
16. [x] Run `bunx turbo test:coverage --filter=@suaveplan/linter` — 100/100/100/100.
|
||||
17. [x] Refresh MANIFEST.md via `bun scripts/folder-reorg/generate-manifest.ts`.
|
||||
18. [x] Flip STATUS.md row from `–` to `✅` with one-line description.
|
||||
19. [x] Fold `specs/linter/spec.md` into `openspec/specs/linter/spec.md`.
|
||||
20. [x] Archive `openspec/changes/enterprise-linter/` →
|
||||
`openspec/archive/2026-05-25-enterprise-linter/`.
|
||||
Reference in New Issue
Block a user