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.js
ADDED
|
@@ -0,0 +1,975 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __export = (target, all) => {
|
|
3
|
+
for (var name in all)
|
|
4
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
// src/core/runner.ts
|
|
8
|
+
import Ajv from "ajv";
|
|
9
|
+
|
|
10
|
+
// src/core/parser.ts
|
|
11
|
+
import { fromMarkdown } from "mdast-util-from-markdown";
|
|
12
|
+
import { toString } from "mdast-util-to-string";
|
|
13
|
+
import grayMatter from "gray-matter";
|
|
14
|
+
function parseFile(content) {
|
|
15
|
+
const matter = grayMatter(content);
|
|
16
|
+
const data = matter.data;
|
|
17
|
+
const frontmatter = Object.keys(data).length > 0 ? data : null;
|
|
18
|
+
const ast = fromMarkdown(matter.content);
|
|
19
|
+
const listMetadata = extractListMetadata(ast);
|
|
20
|
+
let metadata = null;
|
|
21
|
+
if (frontmatter && listMetadata) {
|
|
22
|
+
const frontmatterDefined = {};
|
|
23
|
+
for (const [k, v] of Object.entries(frontmatter)) {
|
|
24
|
+
if (v !== null && v !== void 0) frontmatterDefined[k] = v;
|
|
25
|
+
}
|
|
26
|
+
metadata = { ...listMetadata, ...frontmatterDefined };
|
|
27
|
+
} else if (frontmatter) {
|
|
28
|
+
metadata = frontmatter;
|
|
29
|
+
} else if (listMetadata) {
|
|
30
|
+
metadata = listMetadata;
|
|
31
|
+
}
|
|
32
|
+
return { frontmatter, listMetadata, metadata, ast, body: matter.content };
|
|
33
|
+
}
|
|
34
|
+
var RECOGNIZED_METADATA_KEYS = /* @__PURE__ */ new Set([
|
|
35
|
+
"status",
|
|
36
|
+
"date",
|
|
37
|
+
"deciders",
|
|
38
|
+
"decision-makers",
|
|
39
|
+
"consulted",
|
|
40
|
+
"informed"
|
|
41
|
+
]);
|
|
42
|
+
function extractListMetadata(ast) {
|
|
43
|
+
let firstList = null;
|
|
44
|
+
for (const child of ast.children) {
|
|
45
|
+
if (child.type === "heading") {
|
|
46
|
+
if (child.depth >= 2) break;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (child.type === "html") continue;
|
|
50
|
+
if (child.type === "list") {
|
|
51
|
+
firstList = child;
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
if (!firstList) return null;
|
|
57
|
+
const result = {};
|
|
58
|
+
for (const item of firstList.children) {
|
|
59
|
+
const pair = extractListItemKV(item);
|
|
60
|
+
if (pair && !(pair.key in result)) {
|
|
61
|
+
result[pair.key] = pair.value;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const hasRecognizedKey = Object.keys(result).some(
|
|
65
|
+
(k) => RECOGNIZED_METADATA_KEYS.has(k)
|
|
66
|
+
);
|
|
67
|
+
return hasRecognizedKey ? result : null;
|
|
68
|
+
}
|
|
69
|
+
var KEY_PATTERN = /^[A-Za-z][A-Za-z0-9 \-_]*$/;
|
|
70
|
+
var SEPARATOR_PATTERN = /^\s*:\s*/;
|
|
71
|
+
function normalizeKey(rawKey) {
|
|
72
|
+
return rawKey.trim().toLowerCase().replace(/\s+/g, "-");
|
|
73
|
+
}
|
|
74
|
+
function extractListItemKV(item) {
|
|
75
|
+
const para = item.children[0];
|
|
76
|
+
if (para?.type !== "paragraph") return null;
|
|
77
|
+
const paragraph = para;
|
|
78
|
+
const first = paragraph.children[0];
|
|
79
|
+
if (!first) return null;
|
|
80
|
+
if (first.type === "strong") {
|
|
81
|
+
const keyNode = first.children[0];
|
|
82
|
+
if (keyNode?.type !== "text" || !KEY_PATTERN.test(keyNode.value)) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
const sep = paragraph.children[1];
|
|
86
|
+
if (sep?.type !== "text" || !SEPARATOR_PATTERN.test(sep.value)) return null;
|
|
87
|
+
const valueHead = sep.value.replace(SEPARATOR_PATTERN, "");
|
|
88
|
+
const valueTail = paragraph.children.slice(2).map((n) => toString(n)).join("");
|
|
89
|
+
const value = (valueHead + valueTail).trim();
|
|
90
|
+
if (value === "") return null;
|
|
91
|
+
return { key: normalizeKey(keyNode.value), value };
|
|
92
|
+
}
|
|
93
|
+
if (first.type === "text") {
|
|
94
|
+
const colonIdx = first.value.indexOf(":");
|
|
95
|
+
if (colonIdx === -1) return null;
|
|
96
|
+
const rawKey = first.value.slice(0, colonIdx);
|
|
97
|
+
if (!KEY_PATTERN.test(rawKey)) return null;
|
|
98
|
+
const valueHead = first.value.slice(colonIdx + 1).replace(/^\s+/, "");
|
|
99
|
+
const valueTail = paragraph.children.slice(1).map((n) => toString(n)).join("");
|
|
100
|
+
const value = (valueHead + valueTail).trim();
|
|
101
|
+
if (value === "") return null;
|
|
102
|
+
return { key: normalizeKey(rawKey), value };
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// src/core/runner.ts
|
|
108
|
+
var ajv = new Ajv({ strict: true, allErrors: true });
|
|
109
|
+
var validatorCache = /* @__PURE__ */ new WeakMap();
|
|
110
|
+
function getValidator(schema) {
|
|
111
|
+
let validator = validatorCache.get(schema);
|
|
112
|
+
if (!validator) {
|
|
113
|
+
validator = ajv.compile(schema);
|
|
114
|
+
validatorCache.set(schema, validator);
|
|
115
|
+
}
|
|
116
|
+
return validator;
|
|
117
|
+
}
|
|
118
|
+
var INTERNAL_ERROR_RULE_NAME = "core/internal-error";
|
|
119
|
+
var RuleOptionsError = class extends Error {
|
|
120
|
+
constructor(ruleName, ajvErrors) {
|
|
121
|
+
super(`Invalid options for rule ${ruleName}: ${ajvErrors}`);
|
|
122
|
+
this.ruleName = ruleName;
|
|
123
|
+
this.ajvErrors = ajvErrors;
|
|
124
|
+
this.name = "RuleOptionsError";
|
|
125
|
+
}
|
|
126
|
+
ruleName;
|
|
127
|
+
ajvErrors;
|
|
128
|
+
};
|
|
129
|
+
function runRule(rule9, file, runtime = {}) {
|
|
130
|
+
return runRulesOnFile([rule9], file, runtime);
|
|
131
|
+
}
|
|
132
|
+
function runRulesOnFile(rules, file, runtime = {}) {
|
|
133
|
+
const diagnostics = [];
|
|
134
|
+
const severity = runtime.severity ?? "error";
|
|
135
|
+
let parsed = null;
|
|
136
|
+
const ensureParsed = () => {
|
|
137
|
+
parsed ??= parseFile(file.content);
|
|
138
|
+
return parsed;
|
|
139
|
+
};
|
|
140
|
+
const enterMap = {};
|
|
141
|
+
const exitMap = {};
|
|
142
|
+
let hasASTRule = false;
|
|
143
|
+
for (const rule9 of rules) {
|
|
144
|
+
const perRuleOptions = runtime.optionsByRule?.[rule9.meta.name] ?? runtime.options;
|
|
145
|
+
const mergedOptions = {
|
|
146
|
+
...rule9.meta.defaultOptions,
|
|
147
|
+
...perRuleOptions
|
|
148
|
+
};
|
|
149
|
+
if (rule9.meta.schema && !runtime.skipValidation) {
|
|
150
|
+
const validator = getValidator(rule9.meta.schema);
|
|
151
|
+
if (!validator(mergedOptions)) {
|
|
152
|
+
throw new RuleOptionsError(rule9.meta.name, ajv.errorsText(validator.errors));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const context = {
|
|
156
|
+
file,
|
|
157
|
+
get frontmatter() {
|
|
158
|
+
return ensureParsed().frontmatter;
|
|
159
|
+
},
|
|
160
|
+
get metadata() {
|
|
161
|
+
return ensureParsed().metadata;
|
|
162
|
+
},
|
|
163
|
+
options: mergedOptions,
|
|
164
|
+
report(d) {
|
|
165
|
+
diagnostics.push({
|
|
166
|
+
ruleName: rule9.meta.name,
|
|
167
|
+
severity,
|
|
168
|
+
path: file.path,
|
|
169
|
+
...d
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
let listeners;
|
|
174
|
+
try {
|
|
175
|
+
listeners = rule9.create(context);
|
|
176
|
+
} catch (err) {
|
|
177
|
+
if (err instanceof RuleOptionsError) throw err;
|
|
178
|
+
diagnostics.push(
|
|
179
|
+
internalErrorDiagnostic(rule9.meta.name, "create", err, file.path)
|
|
180
|
+
);
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (listeners) {
|
|
184
|
+
hasASTRule = true;
|
|
185
|
+
collectListeners(rule9, listeners, enterMap, exitMap, diagnostics, file);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (hasASTRule) {
|
|
189
|
+
walk(ensureParsed().ast, enterMap, exitMap);
|
|
190
|
+
}
|
|
191
|
+
return diagnostics;
|
|
192
|
+
}
|
|
193
|
+
function internalErrorDiagnostic(ruleName, operation, err, path) {
|
|
194
|
+
return {
|
|
195
|
+
ruleName: INTERNAL_ERROR_RULE_NAME,
|
|
196
|
+
messageId: "ruleThrew",
|
|
197
|
+
severity: "error",
|
|
198
|
+
path,
|
|
199
|
+
data: {
|
|
200
|
+
rule: ruleName,
|
|
201
|
+
operation,
|
|
202
|
+
error: err instanceof Error ? err.message : String(err)
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
function collectListeners(rule9, listeners, enterMap, exitMap, diagnostics, file) {
|
|
207
|
+
const wrap = (op) => (handler) => (node) => {
|
|
208
|
+
try {
|
|
209
|
+
handler(node);
|
|
210
|
+
} catch (err) {
|
|
211
|
+
diagnostics.push(
|
|
212
|
+
internalErrorDiagnostic(rule9.meta.name, op, err, file.path)
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
if (listeners.enter) {
|
|
217
|
+
const wrapEnter = wrap("enter");
|
|
218
|
+
for (const [type, handler] of Object.entries(listeners.enter)) {
|
|
219
|
+
if (handler) (enterMap[type] ??= []).push(wrapEnter(handler));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (listeners.exit) {
|
|
223
|
+
const wrapExit = wrap("exit");
|
|
224
|
+
for (const [type, handler] of Object.entries(listeners.exit)) {
|
|
225
|
+
if (handler) (exitMap[type] ??= []).push(wrapExit(handler));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
function walk(node, enterMap, exitMap) {
|
|
230
|
+
const enterHandlers = enterMap[node.type];
|
|
231
|
+
if (enterHandlers) {
|
|
232
|
+
for (const handler of enterHandlers) handler(node);
|
|
233
|
+
}
|
|
234
|
+
if ("children" in node && Array.isArray(node.children)) {
|
|
235
|
+
for (const child of node.children) {
|
|
236
|
+
walk(child, enterMap, exitMap);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
const exitHandlers = exitMap[node.type];
|
|
240
|
+
if (exitHandlers) {
|
|
241
|
+
for (const handler of exitHandlers) handler(node);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
function buildProjectFile(file) {
|
|
245
|
+
const parsed = parseFile(file.content);
|
|
246
|
+
return {
|
|
247
|
+
path: file.path,
|
|
248
|
+
content: file.content,
|
|
249
|
+
frontmatter: parsed.frontmatter,
|
|
250
|
+
metadata: parsed.metadata,
|
|
251
|
+
ast: parsed.ast
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
function runRulesOnProject(rules, files, runtime = {}) {
|
|
255
|
+
const diagnostics = [];
|
|
256
|
+
const severity = runtime.severity ?? "error";
|
|
257
|
+
for (const rule9 of rules) {
|
|
258
|
+
const perRuleOptions = runtime.optionsByRule?.[rule9.meta.name] ?? runtime.options;
|
|
259
|
+
const mergedOptions = {
|
|
260
|
+
...rule9.meta.defaultOptions,
|
|
261
|
+
...perRuleOptions
|
|
262
|
+
};
|
|
263
|
+
if (rule9.meta.schema && !runtime.skipValidation) {
|
|
264
|
+
const validator = getValidator(rule9.meta.schema);
|
|
265
|
+
if (!validator(mergedOptions)) {
|
|
266
|
+
throw new RuleOptionsError(
|
|
267
|
+
rule9.meta.name,
|
|
268
|
+
ajv.errorsText(validator.errors)
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
const context = {
|
|
273
|
+
files,
|
|
274
|
+
options: mergedOptions,
|
|
275
|
+
fileExists: runtime.fileExists,
|
|
276
|
+
report(d) {
|
|
277
|
+
diagnostics.push({
|
|
278
|
+
ruleName: rule9.meta.name,
|
|
279
|
+
severity,
|
|
280
|
+
...d
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
try {
|
|
285
|
+
rule9.check(context);
|
|
286
|
+
} catch (err) {
|
|
287
|
+
diagnostics.push(
|
|
288
|
+
internalErrorDiagnostic(rule9.meta.name, "check", err, "<project>")
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return diagnostics;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// src/core/types.ts
|
|
296
|
+
function isProjectRule(rule9) {
|
|
297
|
+
return "check" in rule9;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// src/rules/index.ts
|
|
301
|
+
var rules_exports = {};
|
|
302
|
+
__export(rules_exports, {
|
|
303
|
+
dateIso8601: () => date_iso8601_default,
|
|
304
|
+
filenameFormat: () => filename_format_default,
|
|
305
|
+
noBrokenLinks: () => no_broken_links_default,
|
|
306
|
+
noDuplicateNumbering: () => no_duplicate_numbering_default,
|
|
307
|
+
noNumberingGap: () => no_numbering_gap_default,
|
|
308
|
+
requiredSections: () => required_sections_default,
|
|
309
|
+
statusEnum: () => status_enum_default,
|
|
310
|
+
supersedesBidirectional: () => supersedes_bidirectional_default
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// src/rules/date-iso8601/schema.json
|
|
314
|
+
var schema_default = {
|
|
315
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
316
|
+
type: "object",
|
|
317
|
+
additionalProperties: false,
|
|
318
|
+
properties: {
|
|
319
|
+
field: {
|
|
320
|
+
type: "string",
|
|
321
|
+
description: "Frontmatter key to read. Default: 'date'."
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
// src/rules/date-iso8601/index.ts
|
|
327
|
+
function normalizeDate(value) {
|
|
328
|
+
if (typeof value === "string") return value;
|
|
329
|
+
if (value instanceof Date) {
|
|
330
|
+
if (Number.isNaN(value.getTime())) return null;
|
|
331
|
+
return value.toISOString().slice(0, 10);
|
|
332
|
+
}
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
function isValidIso8601Date(s) {
|
|
336
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(s)) return false;
|
|
337
|
+
const parts = s.split("-").map(Number);
|
|
338
|
+
const [year, month, day] = parts;
|
|
339
|
+
if (year === void 0 || month === void 0 || day === void 0) return false;
|
|
340
|
+
const d = new Date(Date.UTC(year, month - 1, day));
|
|
341
|
+
return d.getUTCFullYear() === year && d.getUTCMonth() === month - 1 && d.getUTCDate() === day;
|
|
342
|
+
}
|
|
343
|
+
var rule = {
|
|
344
|
+
meta: {
|
|
345
|
+
name: "madr/date-iso8601",
|
|
346
|
+
type: "perFile",
|
|
347
|
+
versionCompat: ["v2", "v3", "v4"],
|
|
348
|
+
docs: {
|
|
349
|
+
description: "Validate that ADR `date` (frontmatter or v2 bold-list) is a valid ISO 8601 calendar date (YYYY-MM-DD)",
|
|
350
|
+
url: "https://github.com/knktkc/madr-lint/blob/main/docs/rules/date-iso8601.md",
|
|
351
|
+
recommended: true
|
|
352
|
+
},
|
|
353
|
+
messages: {
|
|
354
|
+
missingDate: 'Metadata does not contain a "{{field}}" field (checked frontmatter and v2 bold-list)',
|
|
355
|
+
invalidDate: 'Date "{{date}}" is not a valid ISO 8601 calendar date (YYYY-MM-DD)'
|
|
356
|
+
},
|
|
357
|
+
defaultOptions: {
|
|
358
|
+
field: "date"
|
|
359
|
+
},
|
|
360
|
+
schema: schema_default
|
|
361
|
+
},
|
|
362
|
+
create(context) {
|
|
363
|
+
const meta = context.metadata;
|
|
364
|
+
const fieldName = context.options.field;
|
|
365
|
+
if (!meta) {
|
|
366
|
+
context.report({
|
|
367
|
+
messageId: "missingDate",
|
|
368
|
+
data: { field: fieldName }
|
|
369
|
+
});
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
const raw = meta[fieldName];
|
|
373
|
+
const dateStr = normalizeDate(raw);
|
|
374
|
+
if (dateStr === null) {
|
|
375
|
+
context.report({
|
|
376
|
+
messageId: "missingDate",
|
|
377
|
+
data: { field: fieldName }
|
|
378
|
+
});
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
if (!isValidIso8601Date(dateStr)) {
|
|
382
|
+
context.report({
|
|
383
|
+
messageId: "invalidDate",
|
|
384
|
+
data: { date: dateStr }
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
var date_iso8601_default = rule;
|
|
390
|
+
|
|
391
|
+
// src/rules/filename-format/index.ts
|
|
392
|
+
import { basename } from "path";
|
|
393
|
+
|
|
394
|
+
// src/core/regex-safety.ts
|
|
395
|
+
import safeRegex from "safe-regex2";
|
|
396
|
+
var SAFE_REGEX_LIMIT = 25;
|
|
397
|
+
var cache = /* @__PURE__ */ new Map();
|
|
398
|
+
function assertSafeRegex(pattern, ruleName, optionPath) {
|
|
399
|
+
const cached = cache.get(pattern);
|
|
400
|
+
if (cached) return cached;
|
|
401
|
+
let regex;
|
|
402
|
+
try {
|
|
403
|
+
regex = new RegExp(pattern);
|
|
404
|
+
} catch (err) {
|
|
405
|
+
throw new RuleOptionsError(
|
|
406
|
+
ruleName,
|
|
407
|
+
`option "${optionPath}" is not a valid regular expression: ${err.message}`
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
if (!safeRegex(regex, { limit: SAFE_REGEX_LIMIT })) {
|
|
411
|
+
throw new RuleOptionsError(
|
|
412
|
+
ruleName,
|
|
413
|
+
`option "${optionPath}" (${pattern}) appears unsafe (catastrophic backtracking). Reject before runtime.`
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
cache.set(pattern, regex);
|
|
417
|
+
return regex;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// src/rules/filename-format/schema.json
|
|
421
|
+
var schema_default2 = {
|
|
422
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
423
|
+
type: "object",
|
|
424
|
+
additionalProperties: false,
|
|
425
|
+
properties: {
|
|
426
|
+
pattern: {
|
|
427
|
+
type: "string",
|
|
428
|
+
description: "Regex (as string) the basename must match. Defaults to ^[0-9]{4}-[a-z0-9-]+\\.md$"
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
// src/rules/filename-format/index.ts
|
|
434
|
+
var rule2 = {
|
|
435
|
+
meta: {
|
|
436
|
+
name: "madr/filename-format",
|
|
437
|
+
type: "perFile",
|
|
438
|
+
versionCompat: ["v2", "v3", "v4"],
|
|
439
|
+
docs: {
|
|
440
|
+
description: "Enforce ADR filename convention NNNN-kebab-case.md",
|
|
441
|
+
url: "https://github.com/knktkc/madr-lint/blob/main/docs/rules/filename-format.md",
|
|
442
|
+
recommended: true
|
|
443
|
+
},
|
|
444
|
+
messages: {
|
|
445
|
+
invalidFilename: 'Filename "{{filename}}" does not match expected pattern "{{expected}}"'
|
|
446
|
+
},
|
|
447
|
+
defaultOptions: {
|
|
448
|
+
pattern: "^[0-9]{4}-[a-z0-9-]+\\.md$"
|
|
449
|
+
},
|
|
450
|
+
schema: schema_default2
|
|
451
|
+
},
|
|
452
|
+
create(context) {
|
|
453
|
+
const { pattern } = context.options;
|
|
454
|
+
const regex = assertSafeRegex(pattern, "madr/filename-format", "pattern");
|
|
455
|
+
const filename = basename(context.file.path);
|
|
456
|
+
if (!regex.test(filename)) {
|
|
457
|
+
context.report({
|
|
458
|
+
messageId: "invalidFilename",
|
|
459
|
+
data: { filename, expected: pattern }
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
var filename_format_default = rule2;
|
|
465
|
+
|
|
466
|
+
// src/rules/no-broken-links/index.ts
|
|
467
|
+
import { dirname, posix } from "path";
|
|
468
|
+
|
|
469
|
+
// src/rules/no-broken-links/schema.json
|
|
470
|
+
var schema_default3 = {
|
|
471
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
472
|
+
type: "object",
|
|
473
|
+
additionalProperties: false,
|
|
474
|
+
properties: {}
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
// src/rules/no-broken-links/index.ts
|
|
478
|
+
var PROTOCOL_REGEX = /^[a-z][a-z0-9+.-]*:/i;
|
|
479
|
+
function isLinkNode(node) {
|
|
480
|
+
return node.type === "link" && "url" in node && typeof node.url === "string";
|
|
481
|
+
}
|
|
482
|
+
function isExternalOrAnchor(url) {
|
|
483
|
+
if (url === "" || url.startsWith("#")) return true;
|
|
484
|
+
return PROTOCOL_REGEX.test(url);
|
|
485
|
+
}
|
|
486
|
+
function stripAnchorAndQuery(url) {
|
|
487
|
+
const hash = url.indexOf("#");
|
|
488
|
+
const query = url.indexOf("?");
|
|
489
|
+
let cut = hash;
|
|
490
|
+
if (query !== -1 && (cut === -1 || query < cut)) cut = query;
|
|
491
|
+
return cut === -1 ? url : url.slice(0, cut);
|
|
492
|
+
}
|
|
493
|
+
function safeDecode(path) {
|
|
494
|
+
if (!path.includes("%")) return path;
|
|
495
|
+
try {
|
|
496
|
+
return decodeURIComponent(path);
|
|
497
|
+
} catch {
|
|
498
|
+
return path;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
function resolveRelative(fromDir, url) {
|
|
502
|
+
if (url.startsWith("/")) return url.slice(1);
|
|
503
|
+
return posix.normalize(posix.join(fromDir, url));
|
|
504
|
+
}
|
|
505
|
+
function collectLinks(node, out) {
|
|
506
|
+
if (isLinkNode(node)) out.push(node);
|
|
507
|
+
if ("children" in node && Array.isArray(node.children)) {
|
|
508
|
+
for (const child of node.children) {
|
|
509
|
+
collectLinks(child, out);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
var rule3 = {
|
|
514
|
+
meta: {
|
|
515
|
+
name: "madr/no-broken-links",
|
|
516
|
+
type: "project",
|
|
517
|
+
versionCompat: ["v2", "v3", "v4"],
|
|
518
|
+
docs: {
|
|
519
|
+
description: "Relative-path Markdown links must resolve to an existing file in the project",
|
|
520
|
+
url: "https://github.com/knktkc/madr-lint/blob/main/docs/rules/no-broken-links.md",
|
|
521
|
+
recommended: true
|
|
522
|
+
},
|
|
523
|
+
messages: {
|
|
524
|
+
brokenLink: 'Link to "{{url}}" resolves to "{{resolvedPath}}", which does not exist in the project'
|
|
525
|
+
},
|
|
526
|
+
defaultOptions: {},
|
|
527
|
+
schema: schema_default3
|
|
528
|
+
},
|
|
529
|
+
check(context) {
|
|
530
|
+
const knownPaths = /* @__PURE__ */ new Set();
|
|
531
|
+
for (const file of context.files) {
|
|
532
|
+
knownPaths.add(file.path);
|
|
533
|
+
}
|
|
534
|
+
for (const file of context.files) {
|
|
535
|
+
const links = [];
|
|
536
|
+
collectLinks(file.ast, links);
|
|
537
|
+
for (const link of links) {
|
|
538
|
+
const url = link.url;
|
|
539
|
+
if (isExternalOrAnchor(url)) continue;
|
|
540
|
+
const pathOnly = safeDecode(stripAnchorAndQuery(url));
|
|
541
|
+
if (pathOnly === "") continue;
|
|
542
|
+
const fileDir = dirname(file.path);
|
|
543
|
+
const resolvedPath = resolveRelative(fileDir, pathOnly);
|
|
544
|
+
const exists = knownPaths.has(resolvedPath) || (context.fileExists?.(resolvedPath) ?? false);
|
|
545
|
+
if (!exists) {
|
|
546
|
+
context.report({
|
|
547
|
+
messageId: "brokenLink",
|
|
548
|
+
path: file.path,
|
|
549
|
+
data: { url, resolvedPath }
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
var no_broken_links_default = rule3;
|
|
557
|
+
|
|
558
|
+
// src/rules/no-duplicate-numbering/index.ts
|
|
559
|
+
import { basename as basename2 } from "path";
|
|
560
|
+
|
|
561
|
+
// src/rules/no-duplicate-numbering/schema.json
|
|
562
|
+
var schema_default4 = {
|
|
563
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
564
|
+
type: "object",
|
|
565
|
+
additionalProperties: false,
|
|
566
|
+
properties: {}
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
// src/rules/no-duplicate-numbering/index.ts
|
|
570
|
+
var NUMBER_REGEX = /^(\d{4})-/;
|
|
571
|
+
var rule4 = {
|
|
572
|
+
meta: {
|
|
573
|
+
name: "madr/no-duplicate-numbering",
|
|
574
|
+
type: "project",
|
|
575
|
+
versionCompat: ["v2", "v3", "v4"],
|
|
576
|
+
docs: {
|
|
577
|
+
description: "No two ADRs share the same NNNN- number prefix",
|
|
578
|
+
url: "https://github.com/knktkc/madr-lint/blob/main/docs/rules/no-duplicate-numbering.md",
|
|
579
|
+
recommended: true
|
|
580
|
+
},
|
|
581
|
+
messages: {
|
|
582
|
+
duplicateNumber: "ADR number {{number}} is used by multiple files: {{paths}}"
|
|
583
|
+
},
|
|
584
|
+
defaultOptions: {},
|
|
585
|
+
schema: schema_default4
|
|
586
|
+
},
|
|
587
|
+
check(context) {
|
|
588
|
+
const byNumber = /* @__PURE__ */ new Map();
|
|
589
|
+
for (const file of context.files) {
|
|
590
|
+
const match = NUMBER_REGEX.exec(basename2(file.path));
|
|
591
|
+
if (!match) continue;
|
|
592
|
+
const number = match[1];
|
|
593
|
+
if (number === void 0) continue;
|
|
594
|
+
const group = byNumber.get(number) ?? [];
|
|
595
|
+
group.push(file.path);
|
|
596
|
+
byNumber.set(number, group);
|
|
597
|
+
}
|
|
598
|
+
for (const [number, paths] of byNumber) {
|
|
599
|
+
if (paths.length > 1) {
|
|
600
|
+
const pathList = paths.join(", ");
|
|
601
|
+
for (const path of paths) {
|
|
602
|
+
context.report({
|
|
603
|
+
messageId: "duplicateNumber",
|
|
604
|
+
path,
|
|
605
|
+
data: { number, paths: pathList }
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
};
|
|
612
|
+
var no_duplicate_numbering_default = rule4;
|
|
613
|
+
|
|
614
|
+
// src/rules/no-numbering-gap/index.ts
|
|
615
|
+
import { basename as basename3 } from "path";
|
|
616
|
+
|
|
617
|
+
// src/rules/no-numbering-gap/schema.json
|
|
618
|
+
var schema_default5 = {
|
|
619
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
620
|
+
type: "object",
|
|
621
|
+
additionalProperties: false,
|
|
622
|
+
properties: {}
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
// src/rules/no-numbering-gap/index.ts
|
|
626
|
+
var NUMBER_REGEX2 = /^(\d{4})-/;
|
|
627
|
+
function format4(n) {
|
|
628
|
+
return n.toString().padStart(4, "0");
|
|
629
|
+
}
|
|
630
|
+
var rule5 = {
|
|
631
|
+
meta: {
|
|
632
|
+
name: "madr/no-numbering-gap",
|
|
633
|
+
type: "project",
|
|
634
|
+
versionCompat: ["v2", "v3", "v4"],
|
|
635
|
+
docs: {
|
|
636
|
+
description: "Detect gaps in ADR numbering (e.g. 0001 and 0003 exist but 0002 is missing). Convention-only; default off.",
|
|
637
|
+
url: "https://github.com/knktkc/madr-lint/blob/main/docs/rules/no-numbering-gap.md",
|
|
638
|
+
recommended: false
|
|
639
|
+
},
|
|
640
|
+
messages: {
|
|
641
|
+
numberingGap: "Numbering gap: missing {{missing}} between ADR-{{from}} and ADR-{{to}}"
|
|
642
|
+
},
|
|
643
|
+
defaultOptions: {},
|
|
644
|
+
schema: schema_default5
|
|
645
|
+
},
|
|
646
|
+
check(context) {
|
|
647
|
+
const numberToPath = /* @__PURE__ */ new Map();
|
|
648
|
+
for (const file of context.files) {
|
|
649
|
+
const match = NUMBER_REGEX2.exec(basename3(file.path));
|
|
650
|
+
if (!match?.[1]) continue;
|
|
651
|
+
const n = Number.parseInt(match[1], 10);
|
|
652
|
+
if (!Number.isNaN(n)) numberToPath.set(n, file.path);
|
|
653
|
+
}
|
|
654
|
+
if (numberToPath.size < 2) return;
|
|
655
|
+
const sortedNumbers = Array.from(numberToPath.keys()).toSorted(
|
|
656
|
+
(a, b) => a - b
|
|
657
|
+
);
|
|
658
|
+
for (let i = 1; i < sortedNumbers.length; i++) {
|
|
659
|
+
const prev = sortedNumbers[i - 1];
|
|
660
|
+
const curr = sortedNumbers[i];
|
|
661
|
+
if (prev === void 0 || curr === void 0) continue;
|
|
662
|
+
if (curr === prev + 1) continue;
|
|
663
|
+
const missingNumbers = [];
|
|
664
|
+
for (let n = prev + 1; n < curr; n++) {
|
|
665
|
+
missingNumbers.push(format4(n));
|
|
666
|
+
}
|
|
667
|
+
const currPath = numberToPath.get(curr);
|
|
668
|
+
if (!currPath) continue;
|
|
669
|
+
context.report({
|
|
670
|
+
messageId: "numberingGap",
|
|
671
|
+
path: currPath,
|
|
672
|
+
data: {
|
|
673
|
+
from: format4(prev),
|
|
674
|
+
to: format4(curr),
|
|
675
|
+
missing: missingNumbers.join(", ")
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
};
|
|
681
|
+
var no_numbering_gap_default = rule5;
|
|
682
|
+
|
|
683
|
+
// src/rules/required-sections/index.ts
|
|
684
|
+
import { toString as toString2 } from "mdast-util-to-string";
|
|
685
|
+
|
|
686
|
+
// src/rules/required-sections/schema.json
|
|
687
|
+
var schema_default6 = {
|
|
688
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
689
|
+
type: "object",
|
|
690
|
+
additionalProperties: false,
|
|
691
|
+
properties: {
|
|
692
|
+
sections: {
|
|
693
|
+
type: "array",
|
|
694
|
+
items: { type: "string" },
|
|
695
|
+
description: "Required heading texts. Order does not matter."
|
|
696
|
+
},
|
|
697
|
+
matchMode: {
|
|
698
|
+
type: "string",
|
|
699
|
+
enum: ["exact", "startsWith"],
|
|
700
|
+
description: "Comparison mode against heading text."
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
// src/rules/required-sections/index.ts
|
|
706
|
+
var rule6 = {
|
|
707
|
+
meta: {
|
|
708
|
+
name: "madr/required-sections",
|
|
709
|
+
type: "perFile",
|
|
710
|
+
versionCompat: ["v2", "v3", "v4"],
|
|
711
|
+
docs: {
|
|
712
|
+
description: "Enforce that ADRs contain required heading sections",
|
|
713
|
+
url: "https://github.com/knktkc/madr-lint/blob/main/docs/rules/required-sections.md",
|
|
714
|
+
recommended: true
|
|
715
|
+
},
|
|
716
|
+
messages: {
|
|
717
|
+
missingSection: 'Missing required section: "{{section}}"'
|
|
718
|
+
},
|
|
719
|
+
defaultOptions: {
|
|
720
|
+
sections: [
|
|
721
|
+
"Context and Problem Statement",
|
|
722
|
+
"Decision Outcome",
|
|
723
|
+
"Consequences"
|
|
724
|
+
],
|
|
725
|
+
matchMode: "exact"
|
|
726
|
+
},
|
|
727
|
+
schema: schema_default6
|
|
728
|
+
},
|
|
729
|
+
create(context) {
|
|
730
|
+
const seen = [];
|
|
731
|
+
return {
|
|
732
|
+
enter: {
|
|
733
|
+
heading(node) {
|
|
734
|
+
if (node.type !== "heading") return;
|
|
735
|
+
seen.push(toString2(node).trim());
|
|
736
|
+
}
|
|
737
|
+
},
|
|
738
|
+
exit: {
|
|
739
|
+
root() {
|
|
740
|
+
const { sections, matchMode } = context.options;
|
|
741
|
+
for (const section of sections) {
|
|
742
|
+
const matched = seen.some(
|
|
743
|
+
(heading) => matchMode === "startsWith" ? heading.startsWith(section) : heading === section
|
|
744
|
+
);
|
|
745
|
+
if (!matched) {
|
|
746
|
+
context.report({
|
|
747
|
+
messageId: "missingSection",
|
|
748
|
+
data: { section, found: [...seen] }
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
};
|
|
757
|
+
var required_sections_default = rule6;
|
|
758
|
+
|
|
759
|
+
// src/rules/status-enum/schema.json
|
|
760
|
+
var schema_default7 = {
|
|
761
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
762
|
+
type: "object",
|
|
763
|
+
additionalProperties: false,
|
|
764
|
+
properties: {
|
|
765
|
+
values: {
|
|
766
|
+
type: "array",
|
|
767
|
+
items: { type: "string" },
|
|
768
|
+
description: "Exact-match allowed status values."
|
|
769
|
+
},
|
|
770
|
+
prefixValues: {
|
|
771
|
+
type: "array",
|
|
772
|
+
items: { type: "string" },
|
|
773
|
+
description: "startsWith-match allowed prefixes (e.g. 'superseded by')."
|
|
774
|
+
},
|
|
775
|
+
caseSensitive: {
|
|
776
|
+
type: "boolean",
|
|
777
|
+
description: "When false, comparisons are case-insensitive."
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
};
|
|
781
|
+
|
|
782
|
+
// src/rules/status-enum/index.ts
|
|
783
|
+
var rule7 = {
|
|
784
|
+
meta: {
|
|
785
|
+
name: "madr/status-enum",
|
|
786
|
+
type: "perFile",
|
|
787
|
+
versionCompat: ["v2", "v3", "v4"],
|
|
788
|
+
docs: {
|
|
789
|
+
description: "Validate that ADR `status` (frontmatter or v2 bold-list) is in the allowed enum",
|
|
790
|
+
url: "https://github.com/knktkc/madr-lint/blob/main/docs/rules/status-enum.md",
|
|
791
|
+
recommended: true
|
|
792
|
+
},
|
|
793
|
+
messages: {
|
|
794
|
+
missingStatus: 'Metadata does not contain a "status" field (checked frontmatter and v2 bold-list)',
|
|
795
|
+
invalidStatus: 'Status "{{status}}" is not one of: {{allowed}}'
|
|
796
|
+
},
|
|
797
|
+
defaultOptions: {
|
|
798
|
+
values: ["proposed", "rejected", "accepted", "deprecated"],
|
|
799
|
+
prefixValues: ["superseded by"],
|
|
800
|
+
caseSensitive: false
|
|
801
|
+
},
|
|
802
|
+
schema: schema_default7
|
|
803
|
+
},
|
|
804
|
+
create(context) {
|
|
805
|
+
const meta = context.metadata;
|
|
806
|
+
if (!meta || typeof meta.status !== "string") {
|
|
807
|
+
context.report({ messageId: "missingStatus", data: {} });
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
const { values, prefixValues, caseSensitive } = context.options;
|
|
811
|
+
const status = meta.status;
|
|
812
|
+
const compareStatus = caseSensitive ? status : status.toLowerCase();
|
|
813
|
+
const compareValues = caseSensitive ? values : values.map((v) => v.toLowerCase());
|
|
814
|
+
const comparePrefixes = caseSensitive ? prefixValues : prefixValues.map((p) => p.toLowerCase());
|
|
815
|
+
const exactMatch = compareValues.includes(compareStatus);
|
|
816
|
+
const prefixMatch = comparePrefixes.some(
|
|
817
|
+
(p) => compareStatus.startsWith(p)
|
|
818
|
+
);
|
|
819
|
+
if (!exactMatch && !prefixMatch) {
|
|
820
|
+
const allowed = [
|
|
821
|
+
...values,
|
|
822
|
+
...prefixValues.map((p) => `${p} ...`)
|
|
823
|
+
];
|
|
824
|
+
context.report({
|
|
825
|
+
messageId: "invalidStatus",
|
|
826
|
+
data: { status, allowed }
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
};
|
|
831
|
+
var status_enum_default = rule7;
|
|
832
|
+
|
|
833
|
+
// src/rules/supersedes-bidirectional/index.ts
|
|
834
|
+
import { basename as basename4 } from "path";
|
|
835
|
+
|
|
836
|
+
// src/rules/supersedes-bidirectional/schema.json
|
|
837
|
+
var schema_default8 = {
|
|
838
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
839
|
+
type: "object",
|
|
840
|
+
additionalProperties: false,
|
|
841
|
+
properties: {}
|
|
842
|
+
};
|
|
843
|
+
|
|
844
|
+
// src/rules/supersedes-bidirectional/index.ts
|
|
845
|
+
var ADR_NUMBER_REGEX = /^(\d{4})-/;
|
|
846
|
+
var FORWARD_FIELD = "supersedes";
|
|
847
|
+
var BACKWARD_FIELD = "superseded-by";
|
|
848
|
+
function extractAdrNumber(path) {
|
|
849
|
+
const match = ADR_NUMBER_REGEX.exec(basename4(path));
|
|
850
|
+
return match?.[1] ?? null;
|
|
851
|
+
}
|
|
852
|
+
function normalizeRefs(value) {
|
|
853
|
+
if (typeof value === "string") return [value];
|
|
854
|
+
if (Array.isArray(value)) {
|
|
855
|
+
return value.filter((v) => typeof v === "string");
|
|
856
|
+
}
|
|
857
|
+
return [];
|
|
858
|
+
}
|
|
859
|
+
var rule8 = {
|
|
860
|
+
meta: {
|
|
861
|
+
name: "madr/supersedes-bidirectional",
|
|
862
|
+
type: "project",
|
|
863
|
+
versionCompat: ["v3", "v4"],
|
|
864
|
+
docs: {
|
|
865
|
+
description: "Frontmatter `supersedes` and `superseded-by` must reference each other bidirectionally",
|
|
866
|
+
url: "https://github.com/knktkc/madr-lint/blob/main/docs/rules/supersedes-bidirectional.md",
|
|
867
|
+
recommended: true
|
|
868
|
+
},
|
|
869
|
+
messages: {
|
|
870
|
+
unknownReference: "Frontmatter `{{direction}}: {{ref}}` references an ADR that does not exist",
|
|
871
|
+
missingBackReference: "`{{source}}` declares `{{direction}}: {{ref}}`, but `{{ref}}` (this file) does not back-reference it via `{{expected}}`"
|
|
872
|
+
},
|
|
873
|
+
defaultOptions: {},
|
|
874
|
+
schema: schema_default8
|
|
875
|
+
},
|
|
876
|
+
check(context) {
|
|
877
|
+
const adrIndex = /* @__PURE__ */ new Map();
|
|
878
|
+
for (const file of context.files) {
|
|
879
|
+
const num = extractAdrNumber(file.path);
|
|
880
|
+
if (num) adrIndex.set(`ADR-${num}`, file);
|
|
881
|
+
}
|
|
882
|
+
for (const file of context.files) {
|
|
883
|
+
const fm = file.frontmatter;
|
|
884
|
+
if (!fm) continue;
|
|
885
|
+
const fileNumber = extractAdrNumber(file.path);
|
|
886
|
+
if (!fileNumber) continue;
|
|
887
|
+
const fileRef = `ADR-${fileNumber}`;
|
|
888
|
+
const forwardRefs = normalizeRefs(fm[FORWARD_FIELD]);
|
|
889
|
+
const backwardRefs = normalizeRefs(fm[BACKWARD_FIELD]);
|
|
890
|
+
for (const ref of forwardRefs) {
|
|
891
|
+
const target = adrIndex.get(ref);
|
|
892
|
+
if (!target) {
|
|
893
|
+
context.report({
|
|
894
|
+
messageId: "unknownReference",
|
|
895
|
+
path: file.path,
|
|
896
|
+
data: { ref, direction: FORWARD_FIELD }
|
|
897
|
+
});
|
|
898
|
+
continue;
|
|
899
|
+
}
|
|
900
|
+
const targetBack = normalizeRefs(target.frontmatter?.[BACKWARD_FIELD]);
|
|
901
|
+
if (!targetBack.includes(fileRef)) {
|
|
902
|
+
context.report({
|
|
903
|
+
messageId: "missingBackReference",
|
|
904
|
+
path: target.path,
|
|
905
|
+
data: {
|
|
906
|
+
ref,
|
|
907
|
+
source: file.path,
|
|
908
|
+
direction: BACKWARD_FIELD,
|
|
909
|
+
expected: fileRef
|
|
910
|
+
}
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
for (const ref of backwardRefs) {
|
|
915
|
+
const target = adrIndex.get(ref);
|
|
916
|
+
if (!target) {
|
|
917
|
+
context.report({
|
|
918
|
+
messageId: "unknownReference",
|
|
919
|
+
path: file.path,
|
|
920
|
+
data: { ref, direction: BACKWARD_FIELD }
|
|
921
|
+
});
|
|
922
|
+
continue;
|
|
923
|
+
}
|
|
924
|
+
const targetFwd = normalizeRefs(target.frontmatter?.[FORWARD_FIELD]);
|
|
925
|
+
if (!targetFwd.includes(fileRef)) {
|
|
926
|
+
context.report({
|
|
927
|
+
messageId: "missingBackReference",
|
|
928
|
+
path: target.path,
|
|
929
|
+
data: {
|
|
930
|
+
ref,
|
|
931
|
+
source: file.path,
|
|
932
|
+
direction: FORWARD_FIELD,
|
|
933
|
+
expected: fileRef
|
|
934
|
+
}
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
};
|
|
941
|
+
var supersedes_bidirectional_default = rule8;
|
|
942
|
+
|
|
943
|
+
// src/configs/recommended.ts
|
|
944
|
+
var recommended = {
|
|
945
|
+
"madr/date-iso8601": "error",
|
|
946
|
+
"madr/filename-format": "error",
|
|
947
|
+
"madr/no-broken-links": "error",
|
|
948
|
+
"madr/no-duplicate-numbering": "error",
|
|
949
|
+
// Convention-only — opt in if your team treats ADR numbering as a
|
|
950
|
+
// contiguous sequence. See docs/rules/no-numbering-gap.md.
|
|
951
|
+
"madr/no-numbering-gap": "off",
|
|
952
|
+
"madr/required-sections": "error",
|
|
953
|
+
"madr/status-enum": "error",
|
|
954
|
+
"madr/supersedes-bidirectional": "error"
|
|
955
|
+
};
|
|
956
|
+
|
|
957
|
+
// src/index.ts
|
|
958
|
+
function defineConfig(config) {
|
|
959
|
+
return config;
|
|
960
|
+
}
|
|
961
|
+
export {
|
|
962
|
+
INTERNAL_ERROR_RULE_NAME,
|
|
963
|
+
RuleOptionsError,
|
|
964
|
+
buildProjectFile,
|
|
965
|
+
defineConfig,
|
|
966
|
+
extractListMetadata,
|
|
967
|
+
isProjectRule,
|
|
968
|
+
parseFile,
|
|
969
|
+
recommended,
|
|
970
|
+
rules_exports as rules,
|
|
971
|
+
runRule,
|
|
972
|
+
runRulesOnFile,
|
|
973
|
+
runRulesOnProject
|
|
974
|
+
};
|
|
975
|
+
//# sourceMappingURL=index.js.map
|