Architecture
The lint pipeline
crackdown is built on the unified ecosystem. Every lint pass follows the same five-step pipeline:
Markdown source │ ▼┌─────────────────────────────────────────────────────────┐│ 1. PARSE (remark-parse + micromark + remark-gfm) ││ Markdown text → mdast (Markdown Abstract Syntax ││ Tree). Position information is preserved on every ││ node, enabling precise line:column reporting. │└──────────────────────────┬──────────────────────────────┘ │ mdast (Root node) ▼┌─────────────────────────────────────────────────────────┐│ 2. TRANSFORM (remark-lint + plugins) ││ Each lint rule is a unified transformer. It ││ traverses the mdast via unist-util-visit and calls ││ file.message() to record violations. Rules run ││ in the order they were registered. │└──────────────────────────┬──────────────────────────────┘ │ VFile with messages[] ▼┌─────────────────────────────────────────────────────────┐│ 3. COLLECT (lintString / lintFile) ││ VFile.messages are mapped to LintViolation objects: ││ { ruleId, message, line, column, severity }. ││ config.rules severity overrides are applied here. │└──────────────────────────┬──────────────────────────────┘ │ LintResult ▼┌─────────────────────────────────────────────────────────┐│ 4. FIX (optional — lintStringFix / --fix) ││ config.fixers are applied sequentially as pure ││ string-to-string transformers. The pipeline re-runs ││ steps 1–3 on the fixed content to surface any ││ remaining violations. │└──────────────────────────┬──────────────────────────────┘ │ FixResult ▼┌─────────────────────────────────────────────────────────┐│ 5. REPORT (reporters or programmatic API) ││ Pretty terminal reporter, JSON reporter, or raw ││ LintResult / FixResult objects for library use. ││ LSP server publishes textDocument/publishDiagnostics│└─────────────────────────────────────────────────────────┘Package layout
crackdown/ packages/ core/ @crackdown/core │ ├─ lint.ts lintString, lintFile │ ├─ fix.ts lintStringFix, lintFileFix │ ├─ config.ts loadConfig (jiti-based crackdown.config.ts loader) │ ├─ api.ts lint() — multi-file programmatic API │ └─ rules/ │ ├─ md009.ts trailing spaces │ └─ md010.ts hard tabs │ cli/ @crackdown/cli │ ├─ cli.ts crackdown lint / crackdown lsp / crackdown migrate │ ├─ migrate.ts crackdown migrate (markdownlint → crackdown.config.ts) │ └─ reporters/ │ ├─ pretty.ts terminal output │ └─ json.ts JSON output │ plugin-mermaid/ @crackdown/plugin-mermaid │ └─ index.ts remarkLintMermaid (@mermaid-js/parser) │ compat-markdownlint/ @crackdown/compat-markdownlint │ ├─ compat.ts loadMarkdownlintConfig │ └─ rules/ │ ├─ md001.ts heading-increment │ ├─ md013.ts line-length │ ├─ md022.ts blanks-around-headings │ └─ md041.ts first-line-heading │ lsp/ @crackdown/lsp │ ├─ convert.ts LintViolation → LSP Diagnostic │ ├─ validate.ts validateMarkdown │ └─ server.ts createServer (TextDocuments, debounce, codeAction) │ vscode/ @crackdown/vscode └─ extension.ts VS Code extension (LanguageClient → @crackdown/lsp)Config loading
crackdown.config.ts is loaded at runtime using jiti, which evaluates TypeScript without a compilation step. Config discovery walks up the directory tree from the linted file’s location until it finds a crackdown.config.ts or reaches the filesystem root.
Mermaid validation
The @crackdown/plugin-mermaid plugin takes a different approach from the text-based rules: it passes the raw content of each mermaid code block to @mermaid-js/parser, which uses a Langium-based grammar for accurate syntax validation.
Diagram type is detected from the first non-empty token (e.g. flowchart, pie, gitGraph). Types not supported by the v1 parser (e.g. sequenceDiagram, classDiagram) are skipped gracefully — no false positives.
LSP architecture
Editor (VS Code, Neovim, Zed…) │ LSP protocol (JSON-RPC over stdio) ▼ @crackdown/lsp server ├─ TextDocuments manager ← tracks open files ├─ Config cache ← loadConfig() per workspace root ├─ fs.watch watcher ← hot-reload crackdown.config.ts ├─ Debounce timer (300ms) ← onDidChange validation └─ validateMarkdown() ← @crackdown/core lintString │ Diagnostic[] ▼ textDocument/publishDiagnostics → inline squiggles in editor