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 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) | — | — | ✅&nbsp;(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) | — | — | ✅&nbsp;(init) | — | — | ~ | — | — |
39
+ | **madr-lint** | —¹ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ~&nbsp;(3/8&nbsp;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.3.0`.
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
- let firstList = null;
195
+ const segments = [];
196
+ let bridged = false;
185
197
  for (const child of ast.children) {
186
- if (child.type === "heading") {
187
- if (child.depth >= 2) break;
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 === "html") continue;
191
- if (child.type === "list") {
192
- firstList = child;
193
- break;
214
+ if (child.type === "list" && bridged) {
215
+ segments.push(child);
216
+ bridged = false;
217
+ continue;
194
218
  }
195
219
  break;
196
220
  }
197
- if (!firstList) return null;
221
+ if (segments.length === 0) return null;
198
222
  const values = {};
199
223
  const loc = {};
200
224
  const valueOffsets = {};
201
- for (const item of firstList.children) {
202
- const pair = extractListItemKV(item);
203
- if (pair && !(pair.key in values)) {
204
- values[pair.key] = pair.value;
205
- const start = item.position?.start;
206
- if (start) loc[pair.key] = { line: start.line, column: start.column };
207
- if (body !== void 0 && pair.valueOffset && body.slice(pair.valueOffset.start, pair.valueOffset.end) === pair.value) {
208
- valueOffsets[pair.key] = pair.valueOffset;
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
  }