madr-lint 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/LICENSE +21 -0
- package/README.md +155 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +1500 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +370 -0
- package/dist/index.js +975 -0
- package/dist/index.js.map +1 -0
- package/package.json +90 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import { AnySchemaObject } from 'ajv';
|
|
2
|
+
import { Root, Nodes } from 'mdast';
|
|
3
|
+
|
|
4
|
+
type MdastNodeType = Root['type'] | Nodes['type'];
|
|
5
|
+
type MdastNode = Root | Nodes;
|
|
6
|
+
/** Severity levels supported by every rule. */
|
|
7
|
+
type Severity = 'error' | 'warn';
|
|
8
|
+
/**
|
|
9
|
+
* User-facing severity declaration in config files.
|
|
10
|
+
* - bare string: enable/disable with default options
|
|
11
|
+
* - tuple: enable with explicit options
|
|
12
|
+
*/
|
|
13
|
+
type RuleSeverity = Severity | 'off' | readonly [Severity, Record<string, unknown>];
|
|
14
|
+
type MadrVersion = 'v2' | 'v3' | 'v4';
|
|
15
|
+
interface RuleMeta<TOptions = Record<string, unknown>> {
|
|
16
|
+
/** Rule identifier, e.g. 'madr/filename-format'. */
|
|
17
|
+
name: string;
|
|
18
|
+
/** Whether the rule operates per-file or across the project. */
|
|
19
|
+
type: 'perFile' | 'project';
|
|
20
|
+
/** Which MADR versions this rule applies to. */
|
|
21
|
+
versionCompat: readonly MadrVersion[];
|
|
22
|
+
docs: {
|
|
23
|
+
description: string;
|
|
24
|
+
url?: string;
|
|
25
|
+
recommended: boolean;
|
|
26
|
+
};
|
|
27
|
+
/** messageId -> template string with {{placeholders}}. */
|
|
28
|
+
messages: Record<string, string>;
|
|
29
|
+
/** Default options merged with user config. */
|
|
30
|
+
defaultOptions: TOptions;
|
|
31
|
+
/** AJV JSON Schema for options. Validated at runRule time. */
|
|
32
|
+
schema?: AnySchemaObject;
|
|
33
|
+
}
|
|
34
|
+
interface FileContext {
|
|
35
|
+
/** File path (typically basename or repo-relative). */
|
|
36
|
+
path: string;
|
|
37
|
+
/** Raw file content. */
|
|
38
|
+
content: string;
|
|
39
|
+
}
|
|
40
|
+
interface Diagnostic {
|
|
41
|
+
/** Rule that emitted this diagnostic. */
|
|
42
|
+
ruleName: string;
|
|
43
|
+
/** messageId from rule.meta.messages. */
|
|
44
|
+
messageId: string;
|
|
45
|
+
/** Severity resolved against the user config. */
|
|
46
|
+
severity: Severity;
|
|
47
|
+
/** Path of the offending file. */
|
|
48
|
+
path: string;
|
|
49
|
+
/** Optional source range. Filename rules omit this. */
|
|
50
|
+
loc?: {
|
|
51
|
+
line: number;
|
|
52
|
+
column: number;
|
|
53
|
+
};
|
|
54
|
+
/** Data for placeholder interpolation in messages[messageId]. */
|
|
55
|
+
data?: Record<string, unknown>;
|
|
56
|
+
}
|
|
57
|
+
interface RuleContext<TOptions = Record<string, unknown>> {
|
|
58
|
+
/** The file being linted. */
|
|
59
|
+
file: FileContext;
|
|
60
|
+
/**
|
|
61
|
+
* Parsed YAML frontmatter (v3/v4 only), or null if absent.
|
|
62
|
+
* Use this when the rule must validate strict YAML frontmatter shape.
|
|
63
|
+
*/
|
|
64
|
+
frontmatter: Record<string, unknown> | null;
|
|
65
|
+
/**
|
|
66
|
+
* Combined metadata: YAML frontmatter merged with v2 bold-list extracted
|
|
67
|
+
* from the body. Frontmatter wins on key conflict. null only when both
|
|
68
|
+
* are absent. See ADR-0006. Use this for format-agnostic field reads.
|
|
69
|
+
*/
|
|
70
|
+
metadata: Record<string, unknown> | null;
|
|
71
|
+
/** User-merged options for this rule (validated against rule.meta.schema). */
|
|
72
|
+
options: TOptions;
|
|
73
|
+
/** Emit a diagnostic. */
|
|
74
|
+
report(diagnostic: Omit<Diagnostic, 'ruleName' | 'severity' | 'path'>): void;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Visitor map dispatched during single-pass mdast traversal.
|
|
78
|
+
* Rules subscribe to specific node types; the runner walks the tree once
|
|
79
|
+
* and calls the matching enter/exit handler for each node.
|
|
80
|
+
*/
|
|
81
|
+
interface RuleListeners {
|
|
82
|
+
enter?: Partial<Record<MdastNodeType, (node: MdastNode) => void>>;
|
|
83
|
+
exit?: Partial<Record<MdastNodeType, (node: MdastNode) => void>>;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* A lint rule.
|
|
87
|
+
*
|
|
88
|
+
* - Filename / metadata-only rules: `create` reports diagnostics directly,
|
|
89
|
+
* then returns void.
|
|
90
|
+
* - AST-based rules: `create` returns RuleListeners; the runner invokes
|
|
91
|
+
* them during single-pass traversal of the file's mdast tree.
|
|
92
|
+
*/
|
|
93
|
+
interface Rule<TOptions = Record<string, unknown>> {
|
|
94
|
+
meta: RuleMeta<TOptions>;
|
|
95
|
+
create(context: RuleContext<TOptions>): RuleListeners | void;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* A pre-parsed file passed to project rules. Built once per
|
|
99
|
+
* `runRulesOnProject` invocation.
|
|
100
|
+
*/
|
|
101
|
+
interface ProjectFile {
|
|
102
|
+
/** File path relative to the project root. */
|
|
103
|
+
path: string;
|
|
104
|
+
/** Raw file content (unparsed). */
|
|
105
|
+
content: string;
|
|
106
|
+
/** Parsed YAML frontmatter (v3/v4 only), or null if absent. */
|
|
107
|
+
frontmatter: Record<string, unknown> | null;
|
|
108
|
+
/**
|
|
109
|
+
* Combined metadata: frontmatter merged with v2 bold-list extracted
|
|
110
|
+
* from the body. Frontmatter wins on key conflict. See ADR-0006.
|
|
111
|
+
*/
|
|
112
|
+
metadata: Record<string, unknown> | null;
|
|
113
|
+
/** mdast root of the body (frontmatter stripped). */
|
|
114
|
+
ast: Root;
|
|
115
|
+
}
|
|
116
|
+
interface ProjectRuleContext<TOptions = Record<string, unknown>> {
|
|
117
|
+
/** All files in the project (eager-parsed). */
|
|
118
|
+
files: readonly ProjectFile[];
|
|
119
|
+
/** User-merged options for this rule. */
|
|
120
|
+
options: TOptions;
|
|
121
|
+
/**
|
|
122
|
+
* Predicate: does a project-root-relative POSIX path exist as a regular
|
|
123
|
+
* file WITHIN the project root? Injected by the orchestrator so rules (e.g.
|
|
124
|
+
* no-broken-links) can verify link targets that are NOT in the linted `.md`
|
|
125
|
+
* set — non-Markdown assets or files outside the scanned paths. Returns
|
|
126
|
+
* false for directories and for any target that resolves at or above the
|
|
127
|
+
* project root (i.e. escapes it). Undefined when no filesystem is available
|
|
128
|
+
* (e.g. in-memory unit tests), in which case rules fall back to the `files`
|
|
129
|
+
* set alone.
|
|
130
|
+
*/
|
|
131
|
+
fileExists?: (resolvedPath: string) => boolean;
|
|
132
|
+
/**
|
|
133
|
+
* Emit a diagnostic. Unlike per-file rules, project rules MUST set
|
|
134
|
+
* `path` explicitly — the runner cannot infer which file the
|
|
135
|
+
* diagnostic relates to.
|
|
136
|
+
*/
|
|
137
|
+
report(diagnostic: Omit<Diagnostic, 'ruleName' | 'severity'>): void;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* A cross-file rule. Receives all parsed files at once and reports
|
|
141
|
+
* diagnostics with explicit `path` per file.
|
|
142
|
+
*
|
|
143
|
+
* Project rules use a different runner function (`runRulesOnProject`)
|
|
144
|
+
* and a distinct context shape from per-file rules. See ADR-0005 for
|
|
145
|
+
* the rationale behind keeping the two APIs separate.
|
|
146
|
+
*/
|
|
147
|
+
interface ProjectRule<TOptions = Record<string, unknown>> {
|
|
148
|
+
meta: RuleMeta<TOptions>;
|
|
149
|
+
check(context: ProjectRuleContext<TOptions>): void;
|
|
150
|
+
}
|
|
151
|
+
/** Either a per-file or a project rule. */
|
|
152
|
+
type AnyRule<TOptions = Record<string, unknown>> = Rule<TOptions> | ProjectRule<TOptions>;
|
|
153
|
+
/** Type guard distinguishing project rules from per-file rules. */
|
|
154
|
+
declare function isProjectRule(rule: AnyRule): rule is ProjectRule;
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The reserved rule name used for internal-error diagnostics. A real rule
|
|
158
|
+
* cannot register with this name (the registry should reject collisions).
|
|
159
|
+
*/
|
|
160
|
+
declare const INTERNAL_ERROR_RULE_NAME = "core/internal-error";
|
|
161
|
+
/**
|
|
162
|
+
* Thrown when a rule's merged options fail AJV validation against
|
|
163
|
+
* `rule.meta.schema`. Distinct from generic `Error` so callers can
|
|
164
|
+
* `instanceof` to skip rules with bad config and continue.
|
|
165
|
+
*/
|
|
166
|
+
declare class RuleOptionsError extends Error {
|
|
167
|
+
readonly ruleName: string;
|
|
168
|
+
readonly ajvErrors: string;
|
|
169
|
+
constructor(ruleName: string, ajvErrors: string);
|
|
170
|
+
}
|
|
171
|
+
interface RunRuleOptions {
|
|
172
|
+
/** Per-rule options to merge over rule.meta.defaultOptions. */
|
|
173
|
+
options?: Record<string, unknown>;
|
|
174
|
+
/**
|
|
175
|
+
* Options keyed by rule name, for multi-rule calls where each rule needs
|
|
176
|
+
* its own options (e.g. config tuples `['error', {...}]`). For a given rule,
|
|
177
|
+
* an entry here takes precedence over `options`; rules absent from the map
|
|
178
|
+
* fall back to `options` (if any), then `meta.defaultOptions`.
|
|
179
|
+
*/
|
|
180
|
+
optionsByRule?: Record<string, Record<string, unknown>>;
|
|
181
|
+
/** Severity to attach to emitted diagnostics. Defaults to 'error'. */
|
|
182
|
+
severity?: Severity;
|
|
183
|
+
/** Skip AJV options validation. Default false. */
|
|
184
|
+
skipValidation?: boolean;
|
|
185
|
+
/**
|
|
186
|
+
* Filesystem-existence predicate exposed to project rules as
|
|
187
|
+
* `context.fileExists`. Per-file runs ignore this. See
|
|
188
|
+
* ProjectRuleContext.fileExists.
|
|
189
|
+
*/
|
|
190
|
+
fileExists?: (resolvedPath: string) => boolean;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Run a single rule against a single file. Sugar over runRulesOnFile
|
|
194
|
+
* for the common one-rule case (used heavily by tests).
|
|
195
|
+
*/
|
|
196
|
+
declare function runRule<TOptions extends Record<string, unknown>>(rule: Rule<TOptions>, file: FileContext, runtime?: RunRuleOptions): Diagnostic[];
|
|
197
|
+
/**
|
|
198
|
+
* Run multiple rules against a single file with a single mdast traversal.
|
|
199
|
+
*
|
|
200
|
+
* Steps:
|
|
201
|
+
* 1. Validate each rule's merged options against its schema (AJV).
|
|
202
|
+
* Throws RuleOptionsError on failure (caller decides whether to skip).
|
|
203
|
+
* 2. Call each rule's create(). If a rule throws, the error is caught
|
|
204
|
+
* and reported as a `core/internal-error` diagnostic — other rules
|
|
205
|
+
* continue. The same isolation wraps every enter/exit handler so a
|
|
206
|
+
* single buggy rule cannot abort the whole lint run.
|
|
207
|
+
* 3. If at least one rule returned RuleListeners, call parseFile()
|
|
208
|
+
* ONCE (gray-matter + mdast-util-from-markdown together; there is
|
|
209
|
+
* no separate cheap "frontmatter only" path) and walk the tree,
|
|
210
|
+
* dispatching enter/exit handlers per node type.
|
|
211
|
+
*
|
|
212
|
+
* Frontmatter is exposed lazily via context.frontmatter — the getter
|
|
213
|
+
* triggers parseFile() on first access. Filename-style rules that never
|
|
214
|
+
* touch frontmatter and return void from create() pay zero parse cost.
|
|
215
|
+
*/
|
|
216
|
+
declare function runRulesOnFile(rules: readonly Rule[], file: FileContext, runtime?: RunRuleOptions): Diagnostic[];
|
|
217
|
+
/**
|
|
218
|
+
* Build a `ProjectFile` (path + content + parsed frontmatter + AST) from
|
|
219
|
+
* a raw file. Used by `runRulesOnProject` and by the lint orchestrator
|
|
220
|
+
* to share parsing between per-file and project passes when needed.
|
|
221
|
+
*/
|
|
222
|
+
declare function buildProjectFile(file: FileContext): ProjectFile;
|
|
223
|
+
/**
|
|
224
|
+
* Run multiple project rules across all parsed files. Each rule's
|
|
225
|
+
* `check()` is called once with the full file array. Throws are
|
|
226
|
+
* captured as `core/internal-error` diagnostics (attached to the
|
|
227
|
+
* first file's path, since project rules have no file context).
|
|
228
|
+
*
|
|
229
|
+
* See ADR-0005 for the API rationale.
|
|
230
|
+
*/
|
|
231
|
+
declare function runRulesOnProject(rules: readonly ProjectRule[], files: readonly ProjectFile[], runtime?: RunRuleOptions): Diagnostic[];
|
|
232
|
+
|
|
233
|
+
interface ParsedFile {
|
|
234
|
+
/** Parsed YAML frontmatter (v3/v4), or null if absent. */
|
|
235
|
+
frontmatter: Record<string, unknown> | null;
|
|
236
|
+
/**
|
|
237
|
+
* v2 body-list metadata extracted from the leading list, or null if absent.
|
|
238
|
+
* Covers both the bold-key (`- **Status**:`) and the canonical MADR v2.1.2
|
|
239
|
+
* plain-key (`* Status:`) shapes. See ADR-0006.
|
|
240
|
+
*/
|
|
241
|
+
listMetadata: Record<string, unknown> | null;
|
|
242
|
+
/**
|
|
243
|
+
* Combined metadata: frontmatter merged with bold-list. Frontmatter wins
|
|
244
|
+
* on key conflict. null only when both are absent. See ADR-0006.
|
|
245
|
+
*/
|
|
246
|
+
metadata: Record<string, unknown> | null;
|
|
247
|
+
/** mdast root of the body (frontmatter stripped). */
|
|
248
|
+
ast: Root;
|
|
249
|
+
/** Body content with frontmatter stripped. */
|
|
250
|
+
body: string;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Parse a Markdown file: extract YAML frontmatter via gray-matter, then
|
|
254
|
+
* feed the body into mdast-util-from-markdown directly (per ADR-0002,
|
|
255
|
+
* we skip the unified+remark pipeline overhead). Additionally, extract
|
|
256
|
+
* v2-style list metadata from the body's first list — both bold-key and
|
|
257
|
+
* canonical plain-key shapes (see ADR-0006).
|
|
258
|
+
*/
|
|
259
|
+
declare function parseFile(content: string): ParsedFile;
|
|
260
|
+
/**
|
|
261
|
+
* Extract v2-style list metadata from an mdast Root. The metadata block is
|
|
262
|
+
* the leading list — the first block after the H1 title (only headings may
|
|
263
|
+
* precede it; an intervening paragraph means the list is body content, not
|
|
264
|
+
* metadata). Each `listItem` is read as one `Key: value` pair in either of
|
|
265
|
+
* two shapes:
|
|
266
|
+
*
|
|
267
|
+
* - Bold key: `- **Status**: accepted` (some MADR v2 authors)
|
|
268
|
+
* - Plain key: `* Status: accepted` (official MADR v2.1.2 template)
|
|
269
|
+
*
|
|
270
|
+
* Key normalization: trim → lowercase → spaces become hyphens
|
|
271
|
+
* (matches v4 frontmatter convention, e.g. "Decision Makers" →
|
|
272
|
+
* "decision-makers"). On duplicate keys, first occurrence wins.
|
|
273
|
+
*
|
|
274
|
+
* Returns null unless the list carries a recognized MADR key, so ordinary
|
|
275
|
+
* leading prose lists are not mistaken for metadata. (A leading list keyed by
|
|
276
|
+
* a recognized field is still treated as metadata even if its value reads
|
|
277
|
+
* prose-like, e.g. `Status: under discussion` — that position + key is
|
|
278
|
+
* overwhelmingly metadata; this is an intentional, bounded tradeoff.)
|
|
279
|
+
*
|
|
280
|
+
* Because the result is gated on MADR field names, callers using this as a
|
|
281
|
+
* generic list reader will get null for purely non-MADR lists.
|
|
282
|
+
*/
|
|
283
|
+
declare function extractListMetadata(ast: Root): Record<string, unknown> | null;
|
|
284
|
+
|
|
285
|
+
interface DateIso8601Options extends Record<string, unknown> {
|
|
286
|
+
field: string;
|
|
287
|
+
}
|
|
288
|
+
declare const rule$7: Rule<DateIso8601Options>;
|
|
289
|
+
|
|
290
|
+
interface FilenameFormatOptions extends Record<string, unknown> {
|
|
291
|
+
pattern: string;
|
|
292
|
+
}
|
|
293
|
+
declare const rule$6: Rule<FilenameFormatOptions>;
|
|
294
|
+
|
|
295
|
+
interface NoBrokenLinksOptions extends Record<string, unknown> {
|
|
296
|
+
}
|
|
297
|
+
declare const rule$5: ProjectRule<NoBrokenLinksOptions>;
|
|
298
|
+
|
|
299
|
+
interface NoDuplicateNumberingOptions extends Record<string, unknown> {
|
|
300
|
+
}
|
|
301
|
+
declare const rule$4: ProjectRule<NoDuplicateNumberingOptions>;
|
|
302
|
+
|
|
303
|
+
interface NoNumberingGapOptions extends Record<string, unknown> {
|
|
304
|
+
}
|
|
305
|
+
declare const rule$3: ProjectRule<NoNumberingGapOptions>;
|
|
306
|
+
|
|
307
|
+
interface RequiredSectionsOptions extends Record<string, unknown> {
|
|
308
|
+
sections: string[];
|
|
309
|
+
matchMode: 'exact' | 'startsWith';
|
|
310
|
+
}
|
|
311
|
+
declare const rule$2: Rule<RequiredSectionsOptions>;
|
|
312
|
+
|
|
313
|
+
interface StatusEnumOptions extends Record<string, unknown> {
|
|
314
|
+
values: string[];
|
|
315
|
+
prefixValues: string[];
|
|
316
|
+
caseSensitive: boolean;
|
|
317
|
+
}
|
|
318
|
+
declare const rule$1: Rule<StatusEnumOptions>;
|
|
319
|
+
|
|
320
|
+
interface SupersedesBidirectionalOptions extends Record<string, unknown> {
|
|
321
|
+
}
|
|
322
|
+
declare const rule: ProjectRule<SupersedesBidirectionalOptions>;
|
|
323
|
+
|
|
324
|
+
declare namespace index {
|
|
325
|
+
export { rule$7 as dateIso8601, rule$6 as filenameFormat, rule$5 as noBrokenLinks, rule$4 as noDuplicateNumbering, rule$3 as noNumberingGap, rule$2 as requiredSections, rule$1 as statusEnum, rule as supersedesBidirectional };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
declare const recommended: Record<string, RuleSeverity>;
|
|
329
|
+
|
|
330
|
+
interface MadrLintConfig {
|
|
331
|
+
/** Preset configurations to extend (e.g. 'madr-lint:recommended'). */
|
|
332
|
+
extends?: string[];
|
|
333
|
+
/** MADR format version target. 'auto' = detect per-file. */
|
|
334
|
+
madrVersion?: MadrVersion | 'auto';
|
|
335
|
+
/** Directory containing ADR files. */
|
|
336
|
+
adrDir?: string;
|
|
337
|
+
/** Per-rule severity / options overrides. */
|
|
338
|
+
rules?: Record<string, RuleSeverity>;
|
|
339
|
+
/**
|
|
340
|
+
* Filename / path patterns to skip. Phase 1 supports exact basename,
|
|
341
|
+
* full relative path, path suffix, and trailing wildcard (e.g.
|
|
342
|
+
* `9999-*`). Full glob support is on the roadmap.
|
|
343
|
+
*/
|
|
344
|
+
ignorePatterns?: string[];
|
|
345
|
+
/** Enable per-file content-hash cache. Default: true. */
|
|
346
|
+
cache?: boolean;
|
|
347
|
+
/** Cache directory. Default: '.madr-lint/cache'. */
|
|
348
|
+
cacheLocation?: string;
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Helper for type-safe config files. Identity at runtime.
|
|
352
|
+
*
|
|
353
|
+
* Usage:
|
|
354
|
+
* ```typescript
|
|
355
|
+
* // madr-lint.config.ts
|
|
356
|
+
* import { defineConfig } from 'madr-lint';
|
|
357
|
+
* export default defineConfig({
|
|
358
|
+
* extends: ['madr-lint:recommended'],
|
|
359
|
+
* madrVersion: 'auto',
|
|
360
|
+
* adrDir: 'docs/adr',
|
|
361
|
+
* ignorePatterns: ['README.md', 'template.md'],
|
|
362
|
+
* rules: {
|
|
363
|
+
* 'madr/filename-format': ['error', { pattern: '^[0-9]{4}-.+\\.md$' }],
|
|
364
|
+
* },
|
|
365
|
+
* });
|
|
366
|
+
* ```
|
|
367
|
+
*/
|
|
368
|
+
declare function defineConfig(config: MadrLintConfig): MadrLintConfig;
|
|
369
|
+
|
|
370
|
+
export { type AnyRule, type Diagnostic, type FileContext, INTERNAL_ERROR_RULE_NAME, type MadrLintConfig, type MadrVersion, type MdastNode, type MdastNodeType, type ParsedFile, type ProjectFile, type ProjectRule, type ProjectRuleContext, type Rule, type RuleContext, type RuleListeners, type RuleMeta, RuleOptionsError, type RuleSeverity, type RunRuleOptions, type Severity, buildProjectFile, defineConfig, extractListMetadata, isProjectRule, parseFile, recommended, index as rules, runRule, runRulesOnFile, runRulesOnProject };
|