madr-lint 0.4.1 → 0.4.2
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 +27 -0
- package/dist/cli.js +36 -19
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +7 -5
- package/dist/index.js +28 -17
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#127](https://github.com/knktkc/madr-lint/pull/127) [`ac0883e`](https://github.com/knktkc/madr-lint/commit/ac0883e1b9c015cf328ea2d4e806c564cc42489f) Thanks [@knktkc](https://github.com/knktkc)! - Bypass gray-matter's content-keyed parse cache ([#122](https://github.com/knktkc/madr-lint/issues/122)). gray-matter 4.0.3 memoizes a parse only when it is called with no options, in a plain object keyed by the *whole document* (`matter.cache[content]`). Both parser call sites now pass a shared, frozen options object, which turns that memoization off.
|
|
8
|
+
|
|
9
|
+
- **A document whose entire content is any `Object.prototype` member name (`constructor`, `toString`, `valueOf`, `__proto__`, …) no longer crashes.** Every one of those twelve lookups hit an inherited member, which gray-matter accepted as a cache hit and returned as a file with `content: undefined` — so `parseFile` threw `TypeError: Cannot convert undefined or null to object` and `frontmatterOffset` threw `TypeError: Cannot read properties of undefined (reading 'length')`. The throw escaped `lintFiles`, so `madr-lint docs/adr` on a directory holding such a file exited with a stack trace instead of reporting; it now reports the file's violations like any other. Only those exact bodies were affected — a trailing newline made the key an ordinary miss.
|
|
10
|
+
- **madr-lint no longer keeps every parsed document in memory.** The cache retained each document's full content for the process lifetime, as the key, the stripped body, and an `orig` Buffer. Measured over 1,000 distinct 8.4 KB documents: 16.9 MB retained before (8.9 MB heap + 8.0 MB buffers, about 2.1x the corpus), 0 MB now. Per-file lint throughput is unchanged (-1% to +3% across the rule benches, i.e. noise). `--fix` now performs a real second parse in `frontmatterOffset` where it previously got a memo hit — about +3 µs per file per pass, measured on this repo's own ADRs; correctness is unaffected.
|
|
11
|
+
|
|
12
|
+
- [#134](https://github.com/knktkc/madr-lint/pull/134) [`7512968`](https://github.com/knktkc/madr-lint/commit/7512968fc1420e232bd60cb977b5f7cb091be5aa) Thanks [@knktkc](https://github.com/knktkc)! - Read the fields below a bullet-marker change in a MADR v2 metadata block ([#115](https://github.com/knktkc/madr-lint/issues/115)). `* Status: accepted` followed by `- Date: 2026/07/06` is two CommonMark lists with no node between them, so the HTML-comment bridge added for [#73](https://github.com/knktkc/madr-lint/issues/73) could not reach the second one and every field below the marker change vanished from metadata.
|
|
13
|
+
|
|
14
|
+
- **A marker change no longer drops fields.** An adjacent list now joins the metadata block when *every* one of its items reads as a `Key: value` pair, so the `Date` above is parsed, keeps its own line number, is targetable by `madr-lint-disable-next-line`, and is repaired by `--fix` — instead of degrading a line-suppressible `invalidDate` into an unsuppressible file-level `missingDate`. The gate is shape, not marker: an ordered list (`1. Date: …`) after an unordered one merges on the same terms; a list with any non-`Key: value` item is prose and still ends the block, as do a paragraph, a code fence, a thematic break, a blockquote, a heading of any depth, and visible HTML.
|
|
15
|
+
- **`extractListMetadata` (public export) returns more keys for some documents.** The accepted cost of a shape-only gate is over-merging: a genuine prose list whose every bullet happens to read `Key: value` (`- Option A: do nothing`) is now merged into the block, so its keys appear in `listMetadata` / `metadata`. Merging can only add keys — duplicates still resolve first-wins, so an earlier field always shadows a later one and no document loses a key it had. The recognized-key gate (`status`, `date`, `deciders`, `decision-makers`, `consulted`, `informed`) is unchanged as a mechanism, but its outcome can change: a document whose only recognized key sits in the adjacent list now returns metadata where it previously returned `null` (which also lets `madr-lint init` cast a v2 version vote for it). Absorbed junk keys on their own never flip that gate.
|
|
16
|
+
- **Upgrading with a baseline**: a v2 field below a marker change 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.
|
|
17
|
+
|
|
18
|
+
- [#135](https://github.com/knktkc/madr-lint/pull/135) [`eba7822`](https://github.com/knktkc/madr-lint/commit/eba78224ea23a018b6d1102c5de4af1a114903ca) Thanks [@knktkc](https://github.com/knktkc)! - Resolve `madr-lint-disable-next-line` from the comment's last line ([#116](https://github.com/knktkc/madr-lint/issues/116)). A directive spelled as a multi-line `<!--` … `-->` block parsed correctly, but the suppression layer counted its target from the html node's **start** line, so it landed on the comment's own interior instead of the content below `-->`. Directives now carry the node's end line and count from there.
|
|
19
|
+
|
|
20
|
+
- **A multi-line directive now reaches the line it points at.** Previously the diagnostic below `-->` was reported despite the directive — for the issue's document (a `disable-next-line madr/date-iso8601` split across three lines between two MADR v2 metadata items), `invalidDate` on the `* Date:` line was reported, not suppressed. A blank line between `-->` and the content is skipped, exactly as it is for a one-line directive.
|
|
21
|
+
- **Single-line directives are unchanged**: a one-line comment's end line *is* its start line, so nothing about the existing spelling moves. `disable` / `enable` ranges still open and close at the comment's **start** line, which is what preserves their pre-existing behavior. That choice is observable: an inline multi-line comment shares its start line with the content beside it — `[gone](./nope.md) <!--` … `-->` spans three lines while `madr/no-broken-links` reports the link on the first — so counting a range from the end line would stop such a `disable` from covering its own line.
|
|
22
|
+
- **The docs no longer ask you to keep a directive on one line.** The suppression guide (en/ja) and the `adopt-madr-lint` / `new-adr` skills previously called the multi-line spelling a miss; they now document it as working.
|
|
23
|
+
|
|
24
|
+
- [#123](https://github.com/knktkc/madr-lint/pull/123) [`657e3ef`](https://github.com/knktkc/madr-lint/commit/657e3ef7ab641fc8055796d22c8303a2b931b4b0) Thanks [@knktkc](https://github.com/knktkc)! - Make filename- and document-derived record keys prototype-safe ([#56](https://github.com/knktkc/madr-lint/issues/56)). Both the cache manifest and the MADR v2 metadata parser accumulated untrusted keys on plain objects, where a key that names an `Object.prototype` member is read and written *through* the prototype instead of stored as data.
|
|
25
|
+
|
|
26
|
+
- **Cache manifest keys are now prototype-safe.** `manifest.files` is a null-prototype map on both the cold path and rehydration, so a file whose relative path is `__proto__` caches, serializes, and cache-hits like any other. Previously the write went through the inherited `__proto__` setter — the entry never reached `manifest.json` and every run was a cold miss. No user-facing bug is fixed: the key is unreachable from the CLI (`findAdrFiles` drops every non-`.md` target, and a `__proto__.md` file keys as `__proto__.md`) and from the published API (`lintFiles` is not exported from `madr-lint`). This is hardening for the internal `lintFiles` seam and any future entry point that passes explicit paths. It was never a global pollution vector either: plain assignment swapped that one record's prototype rather than mutating `Object.prototype`. `saveManifest` output is byte-identical for ordinary keys, so existing manifests stay valid — no `CACHE_SCHEMA_VERSION` bump.
|
|
27
|
+
- **A v2 body-list key named `constructor` is now read.** The leading metadata block de-duplicates with `Object.hasOwn` instead of `in`, so `* Constructor: x` lands in `metadata.constructor` (with its body position and value offset) rather than being silently skipped because `'constructor' in {}` is true through inheritance. `constructor` is the only reachable case: key normalization lowercases, so `toString` arrives as `tostring` (not a member), and `__proto__` never matches the key pattern, which requires a leading letter. No rule reads these fields — the fix is for library consumers of `extractListMetadata` and `ParsedFile`, which previously got `Object` back for such a key. The returned records keep an ordinary prototype, so `hasOwnProperty` still works on them.
|
|
28
|
+
- **A frontmatter key named `__proto__` survives the v2 merge.** When a file carries both YAML frontmatter and a v2 body list, the merge step that drops explicit `null`/`undefined` now builds its record on a null prototype, so `__proto__: x` reaches `metadata.__proto__` as data instead of being swallowed by the inherited setter. The merged `metadata` is still a plain object.
|
|
29
|
+
|
|
3
30
|
## 0.4.1
|
|
4
31
|
|
|
5
32
|
### Patch Changes
|
package/dist/cli.js
CHANGED
|
@@ -130,11 +130,19 @@ import { join as join2, resolve as resolve2 } from "path";
|
|
|
130
130
|
import { fromMarkdown } from "mdast-util-from-markdown";
|
|
131
131
|
import { toString } from "mdast-util-to-string";
|
|
132
132
|
import grayMatter from "gray-matter";
|
|
133
|
+
|
|
134
|
+
// src/core/null-proto-map.ts
|
|
135
|
+
function nullProtoMap() {
|
|
136
|
+
return /* @__PURE__ */ Object.create(null);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// src/core/parser.ts
|
|
140
|
+
var GRAY_MATTER_OPTIONS = Object.freeze({});
|
|
133
141
|
function frontmatterOffset(content) {
|
|
134
|
-
return content.length - grayMatter(content).content.length;
|
|
142
|
+
return content.length - grayMatter(content, GRAY_MATTER_OPTIONS).content.length;
|
|
135
143
|
}
|
|
136
144
|
function parseFile(content) {
|
|
137
|
-
const matter = grayMatter(content);
|
|
145
|
+
const matter = grayMatter(content, GRAY_MATTER_OPTIONS);
|
|
138
146
|
const data = matter.data;
|
|
139
147
|
const frontmatter = Object.keys(data).length > 0 ? data : null;
|
|
140
148
|
const ast = fromMarkdown(matter.content);
|
|
@@ -144,7 +152,7 @@ function parseFile(content) {
|
|
|
144
152
|
let metadataLoc = listResult && Object.keys(listResult.loc).length > 0 ? { ...listResult.loc } : null;
|
|
145
153
|
let metadataValueLoc = listResult && Object.keys(listResult.valueOffsets).length > 0 ? { ...listResult.valueOffsets } : null;
|
|
146
154
|
if (frontmatter && listMetadata) {
|
|
147
|
-
const frontmatterDefined =
|
|
155
|
+
const frontmatterDefined = nullProtoMap();
|
|
148
156
|
for (const [k, v] of Object.entries(frontmatter)) {
|
|
149
157
|
if (v !== null && v !== void 0) frontmatterDefined[k] = v;
|
|
150
158
|
}
|
|
@@ -211,7 +219,7 @@ function extractListMetadataWithLoc(ast, body) {
|
|
|
211
219
|
bridged = true;
|
|
212
220
|
continue;
|
|
213
221
|
}
|
|
214
|
-
if (child.type === "list" && bridged) {
|
|
222
|
+
if (child.type === "list" && (bridged || child.children.length > 0 && child.children.every((item) => extractListItemKV(item)))) {
|
|
215
223
|
segments.push(child);
|
|
216
224
|
bridged = false;
|
|
217
225
|
continue;
|
|
@@ -219,13 +227,13 @@ function extractListMetadataWithLoc(ast, body) {
|
|
|
219
227
|
break;
|
|
220
228
|
}
|
|
221
229
|
if (segments.length === 0) return null;
|
|
222
|
-
const values =
|
|
223
|
-
const loc =
|
|
224
|
-
const valueOffsets =
|
|
230
|
+
const values = nullProtoMap();
|
|
231
|
+
const loc = nullProtoMap();
|
|
232
|
+
const valueOffsets = nullProtoMap();
|
|
225
233
|
for (const list of segments) {
|
|
226
234
|
for (const item of list.children) {
|
|
227
235
|
const pair = extractListItemKV(item);
|
|
228
|
-
if (pair && !(pair.key
|
|
236
|
+
if (pair && !Object.hasOwn(values, pair.key)) {
|
|
229
237
|
values[pair.key] = pair.value;
|
|
230
238
|
const start = item.position?.start;
|
|
231
239
|
if (start) loc[pair.key] = { line: start.line, column: start.column };
|
|
@@ -238,7 +246,7 @@ function extractListMetadataWithLoc(ast, body) {
|
|
|
238
246
|
const hasRecognizedKey = Object.keys(values).some(
|
|
239
247
|
(k) => RECOGNIZED_METADATA_KEYS.has(k)
|
|
240
248
|
);
|
|
241
|
-
return hasRecognizedKey ? { values, loc, valueOffsets } : null;
|
|
249
|
+
return hasRecognizedKey ? { values: { ...values }, loc, valueOffsets } : null;
|
|
242
250
|
}
|
|
243
251
|
var KEY_PATTERN = /^[A-Za-z][A-Za-z0-9 \-_]*$/;
|
|
244
252
|
var SEPARATOR_PATTERN = /^\s*:\s*/;
|
|
@@ -445,9 +453,6 @@ var BASELINE_DISPLAY_PATH = ".madr-lint/baseline.json";
|
|
|
445
453
|
function baselinePath(cwd) {
|
|
446
454
|
return join3(cwd, ".madr-lint", "baseline.json");
|
|
447
455
|
}
|
|
448
|
-
function nullProtoMap() {
|
|
449
|
-
return /* @__PURE__ */ Object.create(null);
|
|
450
|
-
}
|
|
451
456
|
function buildBaseline(diagnostics) {
|
|
452
457
|
const entries = nullProtoMap();
|
|
453
458
|
for (const d of diagnostics) {
|
|
@@ -605,7 +610,13 @@ function loadManifest(path) {
|
|
|
605
610
|
) {
|
|
606
611
|
return null;
|
|
607
612
|
}
|
|
608
|
-
|
|
613
|
+
const files = nullProtoMap();
|
|
614
|
+
for (const [key, entry] of Object.entries(
|
|
615
|
+
data.files
|
|
616
|
+
)) {
|
|
617
|
+
files[key] = entry;
|
|
618
|
+
}
|
|
619
|
+
return { ...data, files };
|
|
609
620
|
} catch {
|
|
610
621
|
return null;
|
|
611
622
|
}
|
|
@@ -812,7 +823,7 @@ var DIRECTIVE_KINDS = /* @__PURE__ */ new Set([
|
|
|
812
823
|
"disable-next-line"
|
|
813
824
|
]);
|
|
814
825
|
var DIRECTIVE_PREFIX = "madr-lint-";
|
|
815
|
-
function parseDirective(value, line) {
|
|
826
|
+
function parseDirective(value, line, endLine) {
|
|
816
827
|
const trimmed = value.trim();
|
|
817
828
|
if (!trimmed.startsWith("<!--") || !trimmed.endsWith("-->")) return null;
|
|
818
829
|
const inner = trimmed.slice(4, -3).trim();
|
|
@@ -823,7 +834,12 @@ function parseDirective(value, line) {
|
|
|
823
834
|
const keyword = wsIndex === -1 ? afterPrefix : afterPrefix.slice(0, wsIndex);
|
|
824
835
|
if (!DIRECTIVE_KINDS.has(keyword)) return null;
|
|
825
836
|
const rest = wsIndex === -1 ? "" : afterPrefix.slice(wsIndex);
|
|
826
|
-
return {
|
|
837
|
+
return {
|
|
838
|
+
kind: keyword,
|
|
839
|
+
rules: parseRuleList(rest),
|
|
840
|
+
line,
|
|
841
|
+
endLine
|
|
842
|
+
};
|
|
827
843
|
}
|
|
828
844
|
function parseRuleList(rest) {
|
|
829
845
|
const trimmed = rest.trim();
|
|
@@ -834,8 +850,9 @@ function parseRuleList(rest) {
|
|
|
834
850
|
function walkHtml(node, out) {
|
|
835
851
|
if (node.type === "html") {
|
|
836
852
|
const line = node.position?.start.line;
|
|
837
|
-
|
|
838
|
-
|
|
853
|
+
const endLine = node.position?.end.line;
|
|
854
|
+
if (typeof line === "number" && typeof endLine === "number") {
|
|
855
|
+
const directive = parseDirective(node.value, line, endLine);
|
|
839
856
|
if (directive) out.push(directive);
|
|
840
857
|
}
|
|
841
858
|
}
|
|
@@ -874,7 +891,7 @@ function collectDirectives(ast, body) {
|
|
|
874
891
|
break;
|
|
875
892
|
}
|
|
876
893
|
case "disable-next-line": {
|
|
877
|
-
const target = nextNonBlankLine(directive.
|
|
894
|
+
const target = nextNonBlankLine(directive.endLine);
|
|
878
895
|
if (target === null) break;
|
|
879
896
|
let entry = index.nextLine.get(target);
|
|
880
897
|
if (!entry) {
|
|
@@ -1186,7 +1203,7 @@ function lintFiles(opts) {
|
|
|
1186
1203
|
schemaVersion: CACHE_SCHEMA_VERSION,
|
|
1187
1204
|
version: opts.cache.pkgVersion,
|
|
1188
1205
|
configHash: opts.cache.configHash,
|
|
1189
|
-
files:
|
|
1206
|
+
files: nullProtoMap()
|
|
1190
1207
|
};
|
|
1191
1208
|
}
|
|
1192
1209
|
}
|