madr-lint 0.3.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,53 @@
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
+
15
+ ## 0.4.0
16
+
17
+ ### Minor Changes
18
+
19
+ - [#87](https://github.com/knktkc/madr-lint/pull/87) [`7795ca7`](https://github.com/knktkc/madr-lint/commit/7795ca7c40634097806ad2bd7f82d4a3a02e465d) Thanks [@knktkc](https://github.com/knktkc)! - Autofix framework — `--fix`, `meta.fixable`, and the fixer API ([#28](https://github.com/knktkc/madr-lint/issues/28), [ADR-0008](https://github.com/knktkc/madr-lint/blob/main/docs/adr/0008-autofix-text-edits.md)). Fixes are raw-text offset edits, never AST serialization, so a fix changes only the span it targets and leaves every other byte untouched.
20
+
21
+ - **Rule API**: a rule declares `meta.fixable: 'code'` and attaches a lazy `fix: (fixer) => TextEdit | TextEdit[] | null` to `context.report(...)`. The `Fixer` (`replaceRange` / `insertAt` / `remove`) takes body (mdast) offsets and translates them to whole-file offsets past stripped frontmatter, so fixes are correct on files with YAML frontmatter. A fix from a rule that did not declare `meta.fixable` is a rule bug — the runner routes it through `core/internal-error`. Fix thunks are lazy: a normal lint constructs no fixer and invokes no thunk.
22
+ - **CLI**: `--fix` applies fixes in place (only files that change are written) and the exit code reflects the problems that **remain**; `--fix-dry-run` prints a per-file unified diff and writes nothing (exit code as if fixes were applied). Fixes compose with existing flags — `--quiet` / `--max-warnings` operate on the remaining diagnostics, and **suppressed** (`madr-lint-disable`) and **baselined** problems are never rewritten. `--update-baseline` combined with `--fix` / `--fix-dry-run` is a usage error (exit 2).
23
+ - **Applier**: collects fixes from reported diagnostics only (after suppression and baseline), sorts by offset, drops overlaps (first-by-position wins), applies in one pass, then re-lints to a fixpoint (max 10 passes). Per-file scope — cross-file fixes are tracked separately.
24
+ - **First real fix**: `madr/status-enum` now mechanically normalizes a v2 list-sourced status value that differs from an allowed value purely by case (e.g. `Accepted` → `accepted`). Frontmatter-sourced values and genuine typos get no fix.
25
+ - **Reporters**: `text` marks fixable diagnostics with a dim `🔧 fixable` tag; `json` gains a `fixable` boolean per result and a `summary.fixed` count when a fix pass ran; `github` / `sarif` are unchanged.
26
+ - **Programmatic API**: exports `applyEdits`, `makeFixer`, `collectFixes`, `fixFileContent`, `unifiedDiff`, `MAX_FIX_PASSES`, `frontmatterOffset`, and the `TextEdit` / `Fixer` / `FixFn` types.
27
+ - **Parser**: `ParsedFile.metadataValueLoc` (exposed to rules as `context.metadataValueLoc`) gives the body-coordinate `{ start, end }` offset range of a `metadata` value, for values sourced from the v2 leading list as a single contiguous text token — the offset source a fix's `fixer.replaceRange` targets.
28
+
29
+ **Breaking (type + cache):** `Diagnostic` gains a required `fixable: boolean` field (disclosed like the earlier `suggestion` / `docsUrl` additions), and the content-hash cache schema version is bumped to `3` (missing/mismatched ⇒ cold), so the first run after upgrading re-lints once instead of serving stale-shape cached diagnostics. Consumers constructing `Diagnostic` objects by hand must set `fixable`.
30
+
31
+ - [#88](https://github.com/knktkc/madr-lint/pull/88) [`32e1c04`](https://github.com/knktkc/madr-lint/commit/32e1c0475b9547236b32c94154ad023270d8b0e6) Thanks [@knktkc](https://github.com/knktkc)! - Make three rules fixable, including the first **cross-file** fix ([#29](https://github.com/knktkc/madr-lint/issues/29)). Building on the autofix framework ([#28](https://github.com/knktkc/madr-lint/issues/28)), fixes remain raw-text offset edits — only the targeted span changes.
32
+
33
+ - **`madr/date-iso8601`** now normalizes an **unambiguous** v2 body-list date to `YYYY-MM-DD`: year-first numeric with a single separator (`2026/7/3`, `2026.7.3`, `2026-7-3`) and English named-month forms (`3 Jul 2026`, `July 3, 2026`). Ambiguous day/month order (`03/07/2026`), two-digit years, impossible calendar dates (`2026/2/30`), non-English month names, and any **frontmatter**-sourced value are reported but never rewritten.
34
+ - **`madr/status-enum`** extends its case-only fix with a tiny synonym/misspelling map validated against the **configured** enum: curated typos (`superceded by …` → `superseded by …`, `depricated` → `deprecated`) fixed under any case setting, and prefix corrections that preserve the tail. Case-only corrections (`Accepted` → `accepted`, `Superseded By ADR-0042` → `superseded by ADR-0042`) apply under `caseSensitive: true` — with the default `caseSensitive: false` such values are valid and never flagged. A value that maps to two candidates, a synonym whose target is not configured, a genuine typo with no unique target, and frontmatter-sourced values get no fix.
35
+ - **`madr/supersedes-bidirectional`** gains the first **cross-file** fix: a `missingBackReference` inserts the reciprocal `<direction>: <expected>` line into the target ADR's **existing** YAML frontmatter, immediately before the closing `---`. The frontmatter is treated as opaque lines (no YAML reparse/reserialize), so key order, comments, and the file's newline style (LF/CRLF) are preserved. It declines when the target has no frontmatter (a block is never created), when the key already exists (value rewrite/append is out of scope), and for `unknownReference` (contextual). When two sources need a back-reference in the same target, one insertion lands per pass and the runner-up is reported.
36
+ - **Framework**: `context.report({ fix })` on a **project** rule is now honored (`meta.fixable: 'code'` gates it; a fix from a non-fixable project rule routes through `core/internal-error`, matching per-file behavior). `lintAndFix` runs a project-fix fixpoint on the fixed contents — collecting edits keyed by the target's `path`, applying them per file (the ADR-0008 per-file edit-set seam), and re-running the project pass to a fixpoint under the same 10-pass bound; suppressed and baselined problems are never rewritten.
37
+ - **Programmatic API**: exports `collectProjectFixes` and `applyEditsCounted` from `src/core/fix.ts`.
38
+
39
+ No breaking changes: `Diagnostic.fixable` was already required ([#28](https://github.com/knktkc/madr-lint/issues/28)); project-rule diagnostics that previously reported `fixable: false` now report `true` only when the rule attaches a fix.
40
+
41
+ - [#85](https://github.com/knktkc/madr-lint/pull/85) [`0e05d1c`](https://github.com/knktkc/madr-lint/commit/0e05d1cd338ae6adec99c066822c3c1edddac029) Thanks [@knktkc](https://github.com/knktkc)! - `madr-lint init` — scaffold a config file with detection heuristics ([#30](https://github.com/knktkc/madr-lint/issues/30)). Non-interactive by design (works in CI / piped stdin):
42
+
43
+ - **ADR directory**: first of `docs/adr`, `docs/decisions`, `doc/adr`, `adr`, `docs/architecture/decisions` whose top level contains at least one `NNNN-*.md` file; falls back to `docs/adr` (the linter's default) with a note when nothing qualifies.
44
+ - **MADR version**: samples up to 20 existing ADRs and lets the majority win — frontmatter with `decision-makers` counts as v4, other frontmatter as v3, a v2 metadata list as v2; empty/tie/no-metadata yields `auto` (omitted from the config, it is the default).
45
+ - **Config format**: `madr-lint.config.ts` when the project looks TypeScript-ish (`tsconfig.json`, or `typescript` among `package.json` dependencies), `.madrlintrc.json` otherwise.
46
+ - Refuses to overwrite an existing config file (exit 2); `--force` overwrites, `--dir <path>` overrides directory detection, `--json` emits a machine-readable summary of what was detected and written.
47
+ - The next-steps epilogue runs a cheap in-process lint of the detected directory and suggests `--update-baseline` when it finds violations, so legacy debt does not block adoption.
48
+
49
+ The plain `madr-lint [paths]` command is unchanged — `init` is dispatched only when it is the literal first argument, so paths, flags and exit codes behave exactly as before.
50
+
3
51
  ## 0.3.0
4
52
 
5
53
  ### 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
@@ -124,8 +131,8 @@ jobs:
124
131
  with: { path: docs/adr }
125
132
  ```
126
133
 
127
- > The floating `v0` tag is published alongside the v0.2.0 release. If it isn't
128
- > available yet, use `@main` (or pin an exact tag like `@v0.2.0`).
134
+ > The floating `v0` tag tracks the latest v0.x release; for stricter
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