madr-lint 0.4.0 → 0.4.1
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 +12 -0
- package/README.md +18 -11
- package/dist/cli.js +42 -16
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +19 -12
- package/dist/index.js +42 -16
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#114](https://github.com/knktkc/madr-lint/pull/114) [`e7d3a30`](https://github.com/knktkc/madr-lint/commit/e7d3a30fc84e0cbc4f2f76bcdc50a7681a38b8fc) Thanks [@knktkc](https://github.com/knktkc)! - Fix v2 metadata being silently dropped after an HTML comment ([#73](https://github.com/knktkc/madr-lint/issues/73)). A suppression comment placed _between_ MADR v2 metadata-list items splits the Markdown list in two (CommonMark), and the v2 bridge previously read only the first segment — so a field below the comment vanished from `context.metadata`, turning a line-suppressible `invalidDate` into an unsuppressible file-level `missingDate`. Suppressing was strictly worse than not suppressing.
|
|
8
|
+
|
|
9
|
+
- **The v2 metadata block is now the leading run of lists joined across HTML comments.** A comment renders as nothing, so it can no longer end the block. A key below a comment now behaves exactly like one above it: it keeps its own body line, so `<!-- madr-lint-disable-next-line madr/date-iso8601 -->` between items reaches the field below it, and it carries the same value offsets, so — with no directive above it — `--fix` rewrites that field in place (`madr/status-enum`, `madr/date-iso8601`). Not both: `--fix` never rewrites a suppressed problem. Blank lines around the comment do not matter — `disable-next-line` already targets the next non-blank line.
|
|
10
|
+
- **Only comments bridge.** Visible HTML (`<div>`, `<details>`, or a node like `<!-- a --> visible` that is not purely a comment), a paragraph, a code fence, a thematic break, a blockquote, and a heading of any depth all still end the metadata block — it must lead. Two lists with no comment between them (a bullet-marker change, `* Status:` then `- Date:`) are still two lists, and the second is still not metadata.
|
|
11
|
+
- **Public API**: `extractListMetadata` now reads list segments separated only by HTML comments. The signature is unchanged and the change is additive — merging can only add keys (duplicate keys still resolve first-wins), so it never returns fewer than before. Two consequences worth knowing: a document whose recognized MADR key sits only in a later segment now returns metadata where it previously returned `null` (which also lets `madr-lint init` cast a v2 version vote for it), and a prose list placed after the metadata block with only a comment between them, before the next heading of any depth, can contribute an extra key.
|
|
12
|
+
- **Upgrading with a baseline**: a v2 field hidden by this bug was reported as a file-level `missingDate` / `missingStatus`; it is now reported as `invalidDate` / `invalidStatus` on the field's own line. If you baselined the old entry, re-run `madr-lint --update-baseline` — the stored entry no longer absorbs the new diagnostic, and the file goes red on an otherwise unchanged tree.
|
|
13
|
+
- **Not fixed here**: a directive written as a multi-line `<!--` … `-->` block still resolves to its own second line, so keep `disable-next-line` on a single line. `<div>`-style HTML on the line directly above a metadata item swallows that line into the HTML block, so the item never reaches the AST at all.
|
|
14
|
+
|
|
3
15
|
## 0.4.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -31,12 +31,12 @@ A fast, configurable linter for [MADR](https://adr.github.io/madr/) (Markdown Ar
|
|
|
31
31
|
|
|
32
32
|
[MADR](https://github.com/adr/madr) ships no official linter, and the general-purpose tools each cover only part of what an ADR collection needs:
|
|
33
33
|
|
|
34
|
-
| Tool | Markdown style | Inter-doc links | ADR numbering | Status enum | Date format | Supersedes graph | v2 bold-list |
|
|
35
|
-
|
|
36
|
-
| [markdownlint-cli2](https://github.com/DavidAnson/markdownlint-cli2) | ✅ | — | — | — | — | — | n/a |
|
|
37
|
-
| [lychee](https://github.com/lycheeverse/lychee) | — | ✅ | — | — | — | — | n/a |
|
|
38
|
-
| [adrs (rust)](https://crates.io/crates/adrs) | — | — | ✅ (init) | — | — | ~ | — |
|
|
39
|
-
| **madr-lint** | —¹ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
34
|
+
| Tool | Markdown style | Inter-doc links | ADR numbering | Status enum | Date format | Supersedes graph | v2 bold-list | Autofix |
|
|
35
|
+
|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
|
|
36
|
+
| [markdownlint-cli2](https://github.com/DavidAnson/markdownlint-cli2) | ✅ | — | — | — | — | — | n/a | ✅ |
|
|
37
|
+
| [lychee](https://github.com/lycheeverse/lychee) | — | ✅ | — | — | — | — | n/a | — |
|
|
38
|
+
| [adrs (rust)](https://crates.io/crates/adrs) | — | — | ✅ (init) | — | — | ~ | — | — |
|
|
39
|
+
| **madr-lint** | —¹ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ~ (3/8 rules) |
|
|
40
40
|
|
|
41
41
|
¹ Deliberately *not* Markdown style — pair it with `markdownlint-cli2` if you want both. `madr-lint` owns the ADR-specific semantics.
|
|
42
42
|
|
|
@@ -44,6 +44,7 @@ A fast, configurable linter for [MADR](https://adr.github.io/madr/) (Markdown Ar
|
|
|
44
44
|
- **ESLint-style rules** — named `madr/*` rules with `error` / `warn` / `off` and per-rule options validated by a JSON Schema.
|
|
45
45
|
- **Per-file & cross-file** — fast single-pass checks plus project rules for unique numbering, the supersedes graph, and link rot.
|
|
46
46
|
- **CLI, library & CI** — `text` / `json` / `sarif` / `github` reporters, a programmatic API, and a drop-in GitHub Actions step.
|
|
47
|
+
- **Autofix** — `--fix` / `--fix-dry-run` mechanically repair the violations that have a safe, unambiguous correction (3 of 8 rules today); everything else still reports with a `suggestion`.
|
|
47
48
|
- **Gradual adoption** — [inline suppression comments](https://knktkc.github.io/madr-lint/guides/suppressing-rules/) for one-off exceptions and a [baseline file](https://knktkc.github.io/madr-lint/guides/adopting-existing-repo/) to snapshot legacy debt so only new violations fail the build.
|
|
48
49
|
|
|
49
50
|
## Install
|
|
@@ -57,9 +58,15 @@ Node.js 22+. ESM-only. Ships TypeScript types.
|
|
|
57
58
|
## Quick start
|
|
58
59
|
|
|
59
60
|
```bash
|
|
61
|
+
# Scaffold a config — detects your ADR directory and MADR version
|
|
62
|
+
npx madr-lint init
|
|
63
|
+
|
|
60
64
|
# Lint the configured adrDir (default: docs/adr)
|
|
61
65
|
npx madr-lint
|
|
62
66
|
|
|
67
|
+
# Auto-repair what's mechanically fixable, then re-lint
|
|
68
|
+
npx madr-lint --fix
|
|
69
|
+
|
|
63
70
|
# Explicit paths (files or directories; directories are searched recursively)
|
|
64
71
|
npx madr-lint docs/adr libs/x/adr
|
|
65
72
|
|
|
@@ -92,17 +99,17 @@ Rule values are a severity (`'error' | 'warn' | 'off'`) or a `[severity, options
|
|
|
92
99
|
|
|
93
100
|
## Rules
|
|
94
101
|
|
|
95
|
-
8 rules — 7 enabled by `recommended`, 1 opt-in. Each page documents its options, examples, and MADR-version compatibility.
|
|
102
|
+
8 rules — 7 enabled by `recommended`, 1 opt-in. 🔧 marks the 3 rules `--fix` can repair. Each page documents its options, examples, and MADR-version compatibility.
|
|
96
103
|
|
|
97
104
|
| Rule | Type | Default | Checks |
|
|
98
105
|
|---|---|:---:|---|
|
|
99
106
|
| [`madr/required-sections`](https://knktkc.github.io/madr-lint/rules/required-sections/) | per-file | `error` | Required heading sections are present |
|
|
100
|
-
| [`madr/status-enum`](https://knktkc.github.io/madr-lint/rules/status-enum/) | per-file | `error` | `status` is one of the allowed values |
|
|
101
|
-
| [`madr/date-iso8601`](https://knktkc.github.io/madr-lint/rules/date-iso8601/) | per-file | `error` | `date` is a valid ISO-8601 calendar date |
|
|
107
|
+
| [`madr/status-enum`](https://knktkc.github.io/madr-lint/rules/status-enum/) 🔧 | per-file | `error` | `status` is one of the allowed values |
|
|
108
|
+
| [`madr/date-iso8601`](https://knktkc.github.io/madr-lint/rules/date-iso8601/) 🔧 | per-file | `error` | `date` is a valid ISO-8601 calendar date |
|
|
102
109
|
| [`madr/filename-format`](https://knktkc.github.io/madr-lint/rules/filename-format/) | per-file | `error` | Filename matches the ADR convention |
|
|
103
110
|
| [`madr/no-broken-links`](https://knktkc.github.io/madr-lint/rules/no-broken-links/) | project | `error` | Relative links resolve to existing files |
|
|
104
111
|
| [`madr/no-duplicate-numbering`](https://knktkc.github.io/madr-lint/rules/no-duplicate-numbering/) | project | `error` | ADR numbers are unique |
|
|
105
|
-
| [`madr/supersedes-bidirectional`](https://knktkc.github.io/madr-lint/rules/supersedes-bidirectional/) | project | `error` | `supersedes` / `superseded-by` agree |
|
|
112
|
+
| [`madr/supersedes-bidirectional`](https://knktkc.github.io/madr-lint/rules/supersedes-bidirectional/) 🔧 | project | `error` | `supersedes` / `superseded-by` agree |
|
|
106
113
|
| [`madr/no-numbering-gap`](https://knktkc.github.io/madr-lint/rules/no-numbering-gap/) | project | `off` | ADR numbers are contiguous (opt-in) |
|
|
107
114
|
|
|
108
115
|
## Use in CI
|
|
@@ -125,7 +132,7 @@ jobs:
|
|
|
125
132
|
```
|
|
126
133
|
|
|
127
134
|
> The floating `v0` tag tracks the latest v0.x release; for stricter
|
|
128
|
-
> reproducibility, pin an exact tag like `@v0.
|
|
135
|
+
> reproducibility, pin an exact tag like `@v0.4.0`.
|
|
129
136
|
|
|
130
137
|
Or run it via `npx` in any CI provider:
|
|
131
138
|
|
package/dist/cli.js
CHANGED
|
@@ -180,32 +180,58 @@ var RECOGNIZED_METADATA_KEYS = /* @__PURE__ */ new Set([
|
|
|
180
180
|
"consulted",
|
|
181
181
|
"informed"
|
|
182
182
|
]);
|
|
183
|
+
function isHtmlComment(value) {
|
|
184
|
+
let rest = value.trim();
|
|
185
|
+
if (rest.length === 0) return false;
|
|
186
|
+
while (rest.length > 0) {
|
|
187
|
+
if (!rest.startsWith("<!--")) return false;
|
|
188
|
+
const end = rest.indexOf("-->", 2);
|
|
189
|
+
if (end === -1) return false;
|
|
190
|
+
rest = rest.slice(end + 3).trim();
|
|
191
|
+
}
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
183
194
|
function extractListMetadataWithLoc(ast, body) {
|
|
184
|
-
|
|
195
|
+
const segments = [];
|
|
196
|
+
let bridged = false;
|
|
185
197
|
for (const child of ast.children) {
|
|
186
|
-
if (
|
|
187
|
-
if (child.
|
|
198
|
+
if (segments.length === 0) {
|
|
199
|
+
if (child.type === "heading") {
|
|
200
|
+
if (child.depth >= 2) break;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (child.type === "html") continue;
|
|
204
|
+
if (child.type === "list") {
|
|
205
|
+
segments.push(child);
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
if (child.type === "html" && isHtmlComment(child.value)) {
|
|
211
|
+
bridged = true;
|
|
188
212
|
continue;
|
|
189
213
|
}
|
|
190
|
-
if (child.type === "
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
214
|
+
if (child.type === "list" && bridged) {
|
|
215
|
+
segments.push(child);
|
|
216
|
+
bridged = false;
|
|
217
|
+
continue;
|
|
194
218
|
}
|
|
195
219
|
break;
|
|
196
220
|
}
|
|
197
|
-
if (
|
|
221
|
+
if (segments.length === 0) return null;
|
|
198
222
|
const values = {};
|
|
199
223
|
const loc = {};
|
|
200
224
|
const valueOffsets = {};
|
|
201
|
-
for (const
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
225
|
+
for (const list of segments) {
|
|
226
|
+
for (const item of list.children) {
|
|
227
|
+
const pair = extractListItemKV(item);
|
|
228
|
+
if (pair && !(pair.key in values)) {
|
|
229
|
+
values[pair.key] = pair.value;
|
|
230
|
+
const start = item.position?.start;
|
|
231
|
+
if (start) loc[pair.key] = { line: start.line, column: start.column };
|
|
232
|
+
if (body !== void 0 && pair.valueOffset && body.slice(pair.valueOffset.start, pair.valueOffset.end) === pair.value) {
|
|
233
|
+
valueOffsets[pair.key] = pair.valueOffset;
|
|
234
|
+
}
|
|
209
235
|
}
|
|
210
236
|
}
|
|
211
237
|
}
|