@d3lm/lint-preset 1.5.5 → 1.6.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/dist/comments.js CHANGED
@@ -1,6 +1,71 @@
1
1
  // src/comments/analyze.ts
2
2
  import { parseSync } from "oxc-parser";
3
3
 
4
+ // src/comments/rust.ts
5
+ import { existsSync } from "fs";
6
+ import { createRequire } from "module";
7
+ import path from "path";
8
+ import { fileURLToPath } from "url";
9
+ var BINARY_NAME = "comments";
10
+ function platformSuffixes() {
11
+ const { platform, arch } = process;
12
+ switch (platform) {
13
+ case "darwin": {
14
+ return [`darwin-${arch}`, "darwin-universal"];
15
+ }
16
+ case "linux": {
17
+ return arch === "arm" ? ["linux-arm-gnueabihf", "linux-arm-musleabihf"] : [`linux-${arch}-gnu`, `linux-${arch}-musl`];
18
+ }
19
+ case "win32": {
20
+ return [`win32-${arch}-msvc`, `win32-${arch}-gnu`];
21
+ }
22
+ default: {
23
+ return [`${platform}-${arch}`];
24
+ }
25
+ }
26
+ }
27
+ function findBinaries() {
28
+ const names = platformSuffixes().map((suffix) => `${BINARY_NAME}.${suffix}.node`);
29
+ for (let directory = path.dirname(fileURLToPath(import.meta.url)); ; directory = path.dirname(directory)) {
30
+ const found = names.map((name) => path.join(directory, "native", name)).filter((file) => existsSync(file));
31
+ if (found.length > 0) {
32
+ return found;
33
+ }
34
+ if (path.dirname(directory) === directory) {
35
+ return [];
36
+ }
37
+ }
38
+ }
39
+ var binding;
40
+ function loadBinding() {
41
+ if (binding) {
42
+ return binding;
43
+ }
44
+ const require2 = createRequire(import.meta.url);
45
+ const errors = [];
46
+ for (const file of findBinaries()) {
47
+ try {
48
+ binding = require2(file);
49
+ return binding;
50
+ } catch (error) {
51
+ errors.push(`${file}: ${error.message}`);
52
+ }
53
+ }
54
+ const reason = errors.length > 0 ? `, and loading it failed (${errors.join("; ")})` : "";
55
+ throw new Error(
56
+ `comments: Rust files need the native addon for ${process.platform}-${process.arch}${reason}. Build it with \`pnpm run build:native\`, or leave \`.rs\` out of \`extensions\`.`
57
+ );
58
+ }
59
+ function scanRust(source) {
60
+ return loadBinding().scanRust(source);
61
+ }
62
+ function rustFingerprint(source) {
63
+ return loadBinding().rustFingerprint(source);
64
+ }
65
+ function isRustCode(text) {
66
+ return loadBinding().isRustCode(text);
67
+ }
68
+
4
69
  // src/comments/style.ts
5
70
  var BANNER = /^(?!#{1,6}\s)([-=*#_~+/\\])\1{3,}|([-=*#_~])\2{3,}\s*$/;
6
71
  var COLUMN_RULE = /^([-=_~])\1*(?:\s+\1+)+$/;
@@ -124,12 +189,94 @@ function findCodeSpanEnd(text, start) {
124
189
  }
125
190
  return -1;
126
191
  }
192
+ function isEscaped(text, index) {
193
+ let backslashes = 0;
194
+ while (text[index - 1 - backslashes] === "\\") {
195
+ backslashes += 1;
196
+ }
197
+ return backslashes % 2 === 1;
198
+ }
199
+ function findUnescaped(text, char, start) {
200
+ for (let index = start; index < text.length; index += 1) {
201
+ if (text[index] === "\\") {
202
+ index += 1;
203
+ } else if (text[index] === char) {
204
+ return index;
205
+ }
206
+ }
207
+ return -1;
208
+ }
209
+ function findClosingBracket(text, start) {
210
+ let depth = 0;
211
+ for (let index = start; index < text.length; index += 1) {
212
+ switch (text[index]) {
213
+ case "\\": {
214
+ index += 1;
215
+ break;
216
+ }
217
+ case "`": {
218
+ const close = findCodeSpanEnd(text, index);
219
+ index = close === -1 ? index + backtickRun(text, index) - 1 : close;
220
+ break;
221
+ }
222
+ case "[": {
223
+ depth += 1;
224
+ break;
225
+ }
226
+ case "]": {
227
+ depth -= 1;
228
+ if (depth === 0) {
229
+ return index;
230
+ }
231
+ break;
232
+ }
233
+ }
234
+ }
235
+ return -1;
236
+ }
237
+ function findDestinationEnd(text, start) {
238
+ let depth = 0;
239
+ for (let index = start; index < text.length; index += 1) {
240
+ const char = text[index];
241
+ let close = -1;
242
+ if (char === "\\") {
243
+ index += 1;
244
+ } else if (char === "<" && text.slice(start + 1, index).trim() === "") {
245
+ close = findUnescaped(text, ">", index + 1);
246
+ } else if ((char === '"' || char === "'") && /\s/.test(text[index - 1])) {
247
+ close = findUnescaped(text, char, index + 1);
248
+ } else if (char === "(") {
249
+ depth += 1;
250
+ } else if (char === ")" && --depth === 0) {
251
+ return index;
252
+ }
253
+ index = close === -1 ? index : close;
254
+ }
255
+ return -1;
256
+ }
257
+ function findLinkEnd(text, start) {
258
+ const close = findClosingBracket(text, start);
259
+ if (close === -1 || text[close + 1] !== "(") {
260
+ return close;
261
+ }
262
+ const end = findDestinationEnd(text, close + 1);
263
+ return end === -1 ? close : end;
264
+ }
127
265
  function tokenize(text) {
266
+ return splitWords(text, true);
267
+ }
268
+ function splitWords(text, keepLinks) {
128
269
  const tokens = [];
129
270
  let current = "";
130
271
  let index = 0;
131
272
  while (index < text.length) {
132
273
  const char = text[index];
274
+ const linkEnd = keepLinks && char === "[" && !isEscaped(text, index) ? findLinkEnd(text, index) : -1;
275
+ if (linkEnd !== -1) {
276
+ current += splitWords(text.slice(index, linkEnd + 1), false).join(" ");
277
+ index = linkEnd + 1;
278
+ continue;
279
+ }
133
280
  const close = char === "`" ? findCodeSpanEnd(text, index) : char === "{" ? findClosingBrace(text, index) : -1;
134
281
  if (char === "`" && close === -1) {
135
282
  const run = backtickRun(text, index);
@@ -225,7 +372,7 @@ function isOrphan(previous, token) {
225
372
  return opensAside(token) || endsClause(previous) && !CLAUSE_END.test(token);
226
373
  }
227
374
  function startsBlock(token) {
228
- return /^(?:[-*+>]|\d+[.)]|#{1,6})$/.test(token) || /^(?:@[A-Za-z]|`{3}|~{3}|\||<\/?[A-Za-z])/.test(token);
375
+ return /^(?:\+|-+|=+|\*+|_{3,}|\d+[.)]|#{1,6})$/.test(token) || /^(?:>|@[A-Za-z]|`{3}|~{3}|\||<(?:\/?[A-Za-z]|[!?]))/.test(token);
229
376
  }
230
377
  function bindLineStarts(tokens, startsLine2 = startsBlock) {
231
378
  const units = [];
@@ -663,6 +810,7 @@ var NAMED_TAGS = /* @__PURE__ */ new Set(["arg", "argument", "param", "prop", "p
663
810
  var LINE_DIRECTIVE = /^\s*(?:(?:eslint|oxlint|stylelint)-(?:disable(?:-next-line|-line)?|enable)\b|eslint-env\b|@ts-(?:ignore|expect-error|nocheck|check)\b|@jsx\w*\b|@vite-ignore\b|[@#]__[A-Z_]+__|@(?:vitest|jest)-environment\b|(?:istanbul|c8|v8)\s+ignore\b|prettier-ignore\b|biome-ignore\b|deno-(?:lint-ignore|fmt-ignore)(?:-file)?\b|tslint:|(?:c[sS]pell|spell-?checker)\s*:|(?:webpack|turbopack)[A-Z][a-zA-Z]*\s*:|vim?:\s|#(?:region|endregion)\b|<reference\s)/;
664
811
  var BLOCK_DIRECTIVE = /^\s*(?:eslint\s|globals?\s|exported\s|jshint\s|jslint\s|type\s*:)/;
665
812
  var LEGAL_COMMENT = /^!|@license\b|@preserve\b|\bcopyright\s+(?:\(c\)|©|\d{4})/i;
813
+ var RUST_LEGAL_COMMENT = /@license\b|@preserve\b|\bcopyright\s+(?:\(c\)|©|\d{4})/i;
666
814
  var DEFINITION = /^(?:``|[\w$.[\]]+(?:[ -][\w$.[\]]+)?)(?: [-–]|:) \S/;
667
815
  var BOX_DRAWING = /[\u2500-\u257F]/;
668
816
  var TABLE_DELIMITER = /^\|?\s*:?-+:?\s*(?:\|\s*:?-+:?\s*)+\|?$/;
@@ -737,12 +885,35 @@ ${text}
737
885
  }
738
886
  return properties.length > 1 || text.trimEnd().endsWith(",") || properties.some((property) => property.type === "Property" && CODE_VALUES.has(property.value.type));
739
887
  }
740
- function isCode(texts) {
741
- const text = texts.map((line) => line.replace(LIST_MARKER, "")).join("\n");
742
- return parsesAsStatements(text) || parsesAsProperties(text);
888
+ function isCode(texts, language) {
889
+ return language.isCode(texts.map((line) => line.replace(LIST_MARKER, "")).join("\n"));
743
890
  }
744
891
  function isCodeLine(text) {
745
- return text.endsWith("{") || text.endsWith(";") && (/[()[\]{}=<>]/.test(text) || !/\s/.test(text)) || /^[}\])]/.test(text) || /^(?:const|let|var)\s+[\w$]+\s*[:=]/.test(text) || /^(?:import|export)\s.*(?:\sfrom\s|[{}])/.test(text);
892
+ return text.endsWith("{") || text.endsWith(";") && (/[()[\]{}=<>]/.test(text) || !/\s/.test(text)) || /^[}\])]/.test(text);
893
+ }
894
+ var JAVASCRIPT = {
895
+ name: "javascript",
896
+ isCode: (text) => parsesAsStatements(text) || parsesAsProperties(text),
897
+ isCodeLine: (text) => {
898
+ return isCodeLine(text) || /^(?:const|let|var)\s+[\w$]+\s*[:=]/.test(text) || /^(?:import|export)\s.*(?:\sfrom\s|[{}])/.test(text);
899
+ },
900
+ legal: LEGAL_COMMENT,
901
+ blockDirective: BLOCK_DIRECTIVE,
902
+ tags: true,
903
+ joinsCodeSpans: true
904
+ };
905
+ var RUST = {
906
+ name: "rust",
907
+ isCode: (text) => isRustCode(text),
908
+ isCodeLine: (text) => {
909
+ return isCodeLine(text) || /^let\s+(?:mut\s+)?\w+\s*[:=]/.test(text) || /^(?:pub(?:\([\w:\s]+\))?\s+)?(?:const|static(?:\s+mut)?)\s+[A-Z_\d]+\s*:/.test(text) || /^(?:pub(?:\([\w:\s]+\))?\s+)?use\s+[\w:]*::/.test(text) || /^#!?\[/.test(text);
910
+ },
911
+ legal: RUST_LEGAL_COMMENT,
912
+ tags: false,
913
+ joinsCodeSpans: false
914
+ };
915
+ function languageOf(filename) {
916
+ return filename.endsWith(".rs") ? RUST : JAVASCRIPT;
746
917
  }
747
918
  function layoutReason(prose, raw) {
748
919
  if (prose.startsWith("|") || TABLE_DELIMITER.test(prose)) {
@@ -756,8 +927,10 @@ function layoutReason(prose, raw) {
756
927
  }
757
928
  return void 0;
758
929
  }
930
+ var THEMATIC_BREAK = /^(?:(?:\*\s*){3,}|(?:-\s*){3,}|(?:_\s*){3,})$/;
759
931
  function isMarkup(prose) {
760
- return /^#{1,6}\s/.test(prose) || /^\[[^\]]+\]:/.test(prose) || /^<\/?[a-z]/i.test(prose) || /^>(?:\s|$)/.test(prose);
932
+ const text = prose.replace(LIST_MARKER, "");
933
+ return /^#{1,6}(?:\s|$)/.test(text) || /^\[[^\]]+\]:/.test(text) || /^<(?:\/?[a-z]|[!?])/i.test(text) || text.startsWith(">") || /^(?:=+|-+)$/.test(prose) || THEMATIC_BREAK.test(prose);
761
934
  }
762
935
  function mask(text) {
763
936
  return text.replaceAll(/[^\n]/g, "x");
@@ -784,11 +957,14 @@ function hasAlignedColumns(lines) {
784
957
  }
785
958
  return false;
786
959
  }
787
- function structureOf(lines) {
960
+ function structureOf(lines, language) {
788
961
  const trimmed = lines.map((line) => line.text.trim()).join("\n");
789
962
  const texts = replaceKeepingLines(trimmed, CODE_SPAN, "``").split("\n");
790
963
  const proseLines = stripNonProse(trimmed).split("\n");
791
- if (isCode(lines.map((line) => line.text.trim()))) {
964
+ if (isCode(
965
+ lines.map((line) => line.text.trim()),
966
+ language
967
+ )) {
792
968
  return { tier: "code", reason: "code" };
793
969
  }
794
970
  let layout;
@@ -797,7 +973,7 @@ function structureOf(lines) {
797
973
  for (const [position, line] of texts.entries()) {
798
974
  const prose = proseLines[position].trim();
799
975
  layout ??= layoutReason(prose, lines[position]?.text ?? "");
800
- if (isCodeLine(line.trim())) {
976
+ if (language.isCodeLine(line.trim())) {
801
977
  codeLike = true;
802
978
  } else if (isMarkup(prose)) {
803
979
  manual ??= "markup";
@@ -836,7 +1012,7 @@ var FenceTracker = class {
836
1012
  return true;
837
1013
  }
838
1014
  };
839
- function segment(lines) {
1015
+ function segment(lines, language) {
840
1016
  const paragraphs = [];
841
1017
  const fence = new FenceTracker();
842
1018
  let current;
@@ -873,6 +1049,10 @@ function segment(lines) {
873
1049
  continue;
874
1050
  }
875
1051
  if (line.verbatim) {
1052
+ if (fence.next(text)) {
1053
+ start("code", line, "", "code block");
1054
+ continue;
1055
+ }
876
1056
  start("code", line, "", "directive");
877
1057
  flush();
878
1058
  continue;
@@ -897,7 +1077,7 @@ function segment(lines) {
897
1077
  }
898
1078
  continue;
899
1079
  }
900
- const tag = JSDOC_TAG.exec(text);
1080
+ const tag = language.tags ? JSDOC_TAG.exec(text) : null;
901
1081
  if (tag) {
902
1082
  start("tag", line, "", PROSE_TAGS.has(tag[1]) ? void 0 : `@${tag[1]} tag`);
903
1083
  inCodeTag = CODE_TAGS.has(tag[1]);
@@ -1004,7 +1184,19 @@ function byLine(issues) {
1004
1184
  function sameLayout(a, b) {
1005
1185
  return a.length === b.length && a.every((words, position) => words.join(" ") === b[position].join(" "));
1006
1186
  }
1007
- function analyzeParagraph(paragraph, options) {
1187
+ function splitTokenLine(lines, tokens) {
1188
+ let next = 0;
1189
+ return lines.findIndex((words) => {
1190
+ const line = words.join(" ");
1191
+ let text = "";
1192
+ while (text.length < line.length && next < tokens.length) {
1193
+ text += `${text ? " " : ""}${tokens[next]}`;
1194
+ next += 1;
1195
+ }
1196
+ return text !== line;
1197
+ });
1198
+ }
1199
+ function analyzeParagraph(paragraph, options, language) {
1008
1200
  const first = paragraph.lines[0];
1009
1201
  const last = paragraph.lines.at(-1) ?? first;
1010
1202
  const base = { kind: paragraph.kind, startLine: first.index + 1, endLine: last.index + 1 };
@@ -1031,7 +1223,8 @@ function analyzeParagraph(paragraph, options) {
1031
1223
  const structure2 = structureOf(
1032
1224
  prose.lines.map((line) => {
1033
1225
  return { ...line, text: stripBlockSyntax(line.text) };
1034
- })
1226
+ }),
1227
+ language
1035
1228
  );
1036
1229
  const issues2 = keptIssues(prose, structure2, options);
1037
1230
  return { ...base, reflowable: false, reason: paragraph.reason, issues: issues2, suggestion: null };
@@ -1040,7 +1233,7 @@ function analyzeParagraph(paragraph, options) {
1040
1233
  const issues2 = keptIssues(paragraph, structure2, options);
1041
1234
  return { ...base, reflowable: false, reason: structure2.reason, issues: issues2, suggestion: null };
1042
1235
  };
1043
- const structure = structureOf(paragraph.lines);
1236
+ const structure = structureOf(paragraph.lines, language);
1044
1237
  if (structure) {
1045
1238
  return keep(structure);
1046
1239
  }
@@ -1059,9 +1252,9 @@ function analyzeParagraph(paragraph, options) {
1059
1252
  parentheticalOverflow: options.maxWidth === void 0 ? void 0 : options.maxWidth - options.width,
1060
1253
  keepFirstLine: paragraph.kind === "tag"
1061
1254
  };
1062
- const layOut = (from, settings) => {
1063
- const even2 = layoutTokens(from, firstLimit, restLimit, settings);
1064
- return { even: even2, wrapped: breakAfterSentence(even2, firstLimit, restLimit, settings) };
1255
+ const layOut = (from, settings, extra) => {
1256
+ const even2 = layoutTokens(from, firstLimit + extra, restLimit + extra, settings);
1257
+ return { even: even2, wrapped: breakAfterSentence(even2, firstLimit + extra, restLimit + extra, settings) };
1065
1258
  };
1066
1259
  const units = bindLineStarts(paragraph.kind === "tag" ? bindTagHead(tokens) : tokens, startsLine);
1067
1260
  const hardLimit = restLimit + (options.maxWidth === void 0 ? options.tolerance : options.maxWidth - options.width);
@@ -1072,22 +1265,26 @@ function analyzeParagraph(paragraph, options) {
1072
1265
  if (!units.every((unit) => staysInLimit(unit))) {
1073
1266
  return keep({ tier: "manual", reason: "unbreakable text across lines" });
1074
1267
  }
1268
+ const text = currentLines.map((words) => words.join(" ")).join("\n");
1269
+ if (!language.joinsCodeSpans && [...text.matchAll(CODE_SPAN)].some((span) => span[0].includes("\n"))) {
1270
+ return keep({ tier: "manual", reason: "code span across lines" });
1271
+ }
1075
1272
  const bodyOf = (lines) => {
1076
1273
  return lines.map((words, position) => {
1077
- const text = `${position === 0 ? paragraph.marker : hang}${words.join(" ")}`;
1078
- return { ...first, text, verbatim: false, hardBreak: HARD_BREAK.test(text) };
1274
+ const text2 = `${position === 0 ? paragraph.marker : hang}${words.join(" ")}`;
1275
+ return { ...first, text: text2, verbatim: false, hardBreak: HARD_BREAK.test(text2) };
1079
1276
  });
1080
1277
  };
1081
- const settle = (settings) => {
1278
+ const settle = (settings, extra = 0) => {
1082
1279
  let current = units;
1083
- let { even: even2, wrapped: wrapped2 } = layOut(current, settings);
1280
+ let { even: even2, wrapped: wrapped2 } = layOut(current, settings, extra);
1084
1281
  for (; ; ) {
1085
1282
  const body = bodyOf(wrapped2);
1086
- const rewrap = structureOf(body);
1283
+ const rewrap = structureOf(body, language);
1087
1284
  if (!rewrap) {
1088
1285
  return { even: even2, wrapped: wrapped2 };
1089
1286
  }
1090
- const tipping = body.findIndex((_, position) => structureOf(body.slice(0, position + 1)) !== void 0);
1287
+ const tipping = body.findIndex((_, position) => structureOf(body.slice(0, position + 1), language) !== void 0);
1091
1288
  const start = wrapped2.slice(0, Math.max(tipping, 1)).flat().length;
1092
1289
  const pair = `${current[start - 1]} ${current[start]}`;
1093
1290
  if (body.length < 2 || !staysInLimit(pair)) {
@@ -1095,16 +1292,25 @@ function analyzeParagraph(paragraph, options) {
1095
1292
  return { tier: "manual", reason: `rewrap would read as ${reading}` };
1096
1293
  }
1097
1294
  current = current.toSpliced(start - 1, 2, pair);
1098
- ({ even: even2, wrapped: wrapped2 } = layOut(current, settings));
1295
+ ({ even: even2, wrapped: wrapped2 } = layOut(current, settings, extra));
1099
1296
  }
1100
1297
  };
1101
- const settled = settle(layoutOptions);
1102
- const layout = "wrapped" in settled ? settled : settle({ ...layoutOptions, keepSteps: true });
1298
+ const layoutAt = (extra) => {
1299
+ const settings = {
1300
+ ...layoutOptions,
1301
+ tolerance: layoutOptions.tolerance - extra,
1302
+ parentheticalOverflow: layoutOptions.parentheticalOverflow === void 0 ? void 0 : layoutOptions.parentheticalOverflow - extra
1303
+ };
1304
+ const settled = settle(settings, extra);
1305
+ return "wrapped" in settled ? settled : settle({ ...settings, keepSteps: true }, extra);
1306
+ };
1307
+ const layout = layoutAt(0);
1103
1308
  if (!("wrapped" in layout)) {
1104
1309
  return keep(layout);
1105
1310
  }
1106
1311
  const { even, wrapped } = layout;
1107
1312
  const matchesLayout = sameLayout(wrapped, currentLines);
1313
+ const isVerdict = (issue) => ISSUE_TIERS[issue.type] === "verdict";
1108
1314
  const lineIssues = [];
1109
1315
  const overflow = options.maxWidth === void 0 ? [] : overflowIssues(paragraph.lines, options);
1110
1316
  if (!matchesLayout) {
@@ -1163,14 +1369,41 @@ function analyzeParagraph(paragraph, options) {
1163
1369
  lineIssues.push({ type: "unbalanced", line });
1164
1370
  }
1165
1371
  }
1372
+ const splitLine = splitTokenLine(currentLines, tokens);
1373
+ if (splitLine !== -1 && overflow.length === 0 && !lineIssues.some((issue) => isVerdict(issue))) {
1374
+ lineIssues.push({ type: "unbalanced", line: splitLine });
1375
+ }
1166
1376
  }
1167
- const suggestion = wrapped.map(
1377
+ const matchesWiderLayout = () => {
1378
+ for (let extra = 1; extra <= layoutOptions.tolerance; extra += 1) {
1379
+ const wider = layoutAt(extra);
1380
+ if ("wrapped" in wider && sameLayout(wider.wrapped, currentLines)) {
1381
+ return true;
1382
+ }
1383
+ }
1384
+ return false;
1385
+ };
1386
+ const accepted = lineIssues.some((issue) => isVerdict(issue)) && matchesWiderLayout();
1387
+ const suggested = accepted ? currentLines : wrapped;
1388
+ const suggestion = suggested.map(
1168
1389
  (words, position) => `${prefix}${position === 0 ? paragraph.marker : hang}${words.join(" ")}`
1169
1390
  );
1170
- const wrapIssues = lineIssues.map((issue) => lineIssue(issue.type, paragraph.lines[issue.line], options));
1391
+ const wrapIssues = lineIssues.filter((issue) => !accepted || !isVerdict(issue)).map((issue) => lineIssue(issue.type, paragraph.lines[issue.line], options));
1171
1392
  const issues = byLine([...overflow, ...wrapIssues, ...styleIssues(paragraph, options)]);
1172
1393
  return { ...base, reflowable: true, issues, suggestion };
1173
1394
  }
1395
+ var LINE_COMMENT = "line comment";
1396
+ function analyzeLineParagraph(paragraph, options, language) {
1397
+ const first = paragraph.lines[0];
1398
+ const last = paragraph.lines.at(-1) ?? first;
1399
+ const base = { kind: paragraph.kind, startLine: first.index + 1, endLine: last.index + 1 };
1400
+ if (paragraph.kind === "code") {
1401
+ return { ...base, reflowable: false, reason: paragraph.reason, issues: [], suggestion: null };
1402
+ }
1403
+ const tier = structureOf(paragraph.lines, language)?.tier;
1404
+ const issues = tier === "code" || tier === "layout" ? [] : overflowIssues(paragraph.lines, options);
1405
+ return { ...base, reflowable: false, reason: LINE_COMMENT, issues, suggestion: null };
1406
+ }
1174
1407
  function computeLineStarts(source) {
1175
1408
  const starts = [0];
1176
1409
  for (let index = 0; index < source.length; index += 1) {
@@ -1196,31 +1429,42 @@ function lineOf(lineStarts, offset) {
1196
1429
  function stripCarriageReturn(line) {
1197
1430
  return line.endsWith("\r") ? line.slice(0, -1) : line;
1198
1431
  }
1199
- function shapeBlock(comment, sourceLines, lineStarts) {
1432
+ function blockKind(comment, language) {
1433
+ if (language.name === "rust") {
1434
+ return comment.doc ? `${comment.doc}-block-doc` : "block";
1435
+ }
1436
+ return comment.value.startsWith("*") ? "jsdoc" : "block";
1437
+ }
1438
+ function wholeComment(shape, lines, paragraphKind, reason) {
1439
+ const otherLines = lines.map((text, offset) => {
1440
+ return { index: shape.startLine + offset, prefix: "", text, verbatim: false, hardBreak: false };
1441
+ });
1442
+ return { ...shape, paragraphs: [{ kind: paragraphKind, marker: "", reason, lines: otherLines }] };
1443
+ }
1444
+ function isLegal(value, language) {
1445
+ return language.legal.test(replaceKeepingLines(value, CODE_SPAN, "``"));
1446
+ }
1447
+ function shapeBlock(comment, sourceLines, lineStarts, language) {
1200
1448
  const startLine = lineOf(lineStarts, comment.start);
1201
1449
  const endLine = lineOf(lineStarts, comment.end - 1);
1202
- const kind = comment.value.startsWith("*") ? "jsdoc" : "block";
1450
+ const shape = { kind: blockKind(comment, language), startLine, endLine };
1203
1451
  const lines = sourceLines.slice(startLine, endLine + 1);
1204
- const wholeComment = (paragraphKind, reason) => {
1205
- const otherLines = lines.map((text, offset) => {
1206
- return { index: startLine + offset, prefix: "", text, verbatim: false, hardBreak: false };
1207
- });
1208
- return { kind, startLine, endLine, paragraphs: [{ kind: paragraphKind, marker: "", reason, lines: otherLines }] };
1209
- };
1210
- const value = comment.value.replace(/^\*/, "");
1211
- if (LEGAL_COMMENT.test(replaceKeepingLines(comment.value, CODE_SPAN, "``"))) {
1212
- return wholeComment("code", "legal comment");
1452
+ const value = comment.doc ? comment.value.slice(1) : comment.value.replace(/^\*/, "");
1453
+ if (isLegal(comment.value, language)) {
1454
+ return wholeComment(shape, lines, "code", "legal comment");
1213
1455
  }
1214
- if (LINE_DIRECTIVE.test(value) || BLOCK_DIRECTIVE.test(value)) {
1215
- return wholeComment("code", "directive");
1456
+ if (LINE_DIRECTIVE.test(value) || language.blockDirective?.test(value)) {
1457
+ return wholeComment(shape, lines, "code", "directive");
1216
1458
  }
1217
1459
  const other = () => {
1218
1460
  return wholeComment(
1461
+ shape,
1462
+ lines,
1219
1463
  "other",
1220
1464
  startLine === endLine ? "single-line block comment" : "unsupported block comment layout"
1221
1465
  );
1222
1466
  };
1223
- if (startLine === endLine || !/^\s*\/\*\*?$/.test(lines[0]) || !/^\s*\*\/$/.test(lines.at(-1) ?? "")) {
1467
+ if (startLine === endLine || !/^\s*\/\*[*!]?$/.test(lines[0]) || !/^\s*\*\/$/.test(lines.at(-1) ?? "")) {
1224
1468
  return other();
1225
1469
  }
1226
1470
  const body = [];
@@ -1239,7 +1483,52 @@ function shapeBlock(comment, sourceLines, lineStarts) {
1239
1483
  hardBreak: HARD_BREAK.test(text)
1240
1484
  });
1241
1485
  }
1242
- return { kind, startLine, endLine, paragraphs: segment(body) };
1486
+ return { ...shape, paragraphs: segment(body, language) };
1487
+ }
1488
+ function shapeLineRun(group, sourceLines, lineStarts, language) {
1489
+ const startLine = lineOf(lineStarts, group[0].start);
1490
+ const endLine = startLine + group.length - 1;
1491
+ const kind = group[0].doc === "inner" ? "inner-line-doc" : "outer-line-doc";
1492
+ const shape = { kind, startLine, endLine };
1493
+ const lines = sourceLines.slice(startLine, endLine + 1);
1494
+ const values = group.map((comment) => comment.value.slice(1));
1495
+ if (isLegal(values.join("\n"), language)) {
1496
+ return wholeComment(shape, lines, "code", "legal comment");
1497
+ }
1498
+ if (LINE_DIRECTIVE.test(values[0])) {
1499
+ return wholeComment(shape, lines, "code", "directive");
1500
+ }
1501
+ const body = lines.map((line, offset) => {
1502
+ const [, marker, space, text] = /^(\s*\/\/[/!])( ?)(.*)$/.exec(line) ?? ["", "", "", line];
1503
+ return {
1504
+ index: startLine + offset,
1505
+ prefix: `${marker} `,
1506
+ text: text.trimEnd(),
1507
+ verbatim: !space && text.length > 0 || LINE_DIRECTIVE.test(text),
1508
+ hardBreak: HARD_BREAK.test(text)
1509
+ };
1510
+ });
1511
+ return { ...shape, paragraphs: segment(body, language) };
1512
+ }
1513
+ function shapeLineComment(group, sourceLines, lineStarts, language) {
1514
+ const startLine = lineOf(lineStarts, group[0].start);
1515
+ const endLine = startLine + group.length - 1;
1516
+ const shape = { kind: "line", startLine, endLine };
1517
+ const lines = sourceLines.slice(startLine, endLine + 1);
1518
+ if (isLegal(group.map((comment) => comment.value).join("\n"), language)) {
1519
+ return wholeComment(shape, lines, "code", "legal comment");
1520
+ }
1521
+ const body = lines.map((line, offset) => {
1522
+ const [, marker, space, text] = /^(\s*\/\/[/!]*)( ?)(.*)$/.exec(line) ?? ["", "", "", line];
1523
+ return {
1524
+ index: startLine + offset,
1525
+ prefix: `${marker}${space}`,
1526
+ text: text.trimEnd(),
1527
+ verbatim: LINE_DIRECTIVE.test(text),
1528
+ hardBreak: HARD_BREAK.test(text)
1529
+ };
1530
+ });
1531
+ return { ...shape, paragraphs: segment(body, { ...language, tags: false }) };
1243
1532
  }
1244
1533
  function ownsLines(comment, source, lineStarts) {
1245
1534
  const startLine = lineOf(lineStarts, comment.start);
@@ -1249,12 +1538,12 @@ function ownsLines(comment, source, lineStarts) {
1249
1538
  const after = source.slice(comment.end, lineEnd);
1250
1539
  return /^[\t ]*$/.test(before) && /^[\t ]*\r?\n?$/.test(after);
1251
1540
  }
1252
- function singleLineLength(paragraph, options) {
1541
+ function singleLineLength(paragraph, options, language) {
1253
1542
  const [first] = paragraph.lines;
1254
1543
  if (paragraph.kind === "other" && paragraph.reason === "single-line block comment") {
1255
1544
  return sourceLineLength(first, options.tabWidth);
1256
1545
  }
1257
- if (paragraph.kind !== "prose" || structureOf(paragraph.lines)) {
1546
+ if (paragraph.kind !== "prose" || structureOf(paragraph.lines, language)) {
1258
1547
  return Infinity;
1259
1548
  }
1260
1549
  const text = tokenize(paragraph.lines.map((line) => line.text).join(" ")).join(" ");
@@ -1269,6 +1558,58 @@ function describeFailure(errors, lineStarts) {
1269
1558
  const offset = first.labels.at(0)?.start;
1270
1559
  return { message: first.message, line: offset === void 0 ? void 0 : lineOf(lineStarts, offset) + 1 };
1271
1560
  }
1561
+ function scanJavaScript(filename, text, lineStarts) {
1562
+ const { program, comments, errors } = parseSource(filename, text);
1563
+ if (errors.length > 0 && program.body.length === 0) {
1564
+ return { comments: [], syntaxError: describeFailure(errors, lineStarts) };
1565
+ }
1566
+ return {
1567
+ comments: comments.filter((comment) => !(comment.start === 0 && text.startsWith("#!")))
1568
+ };
1569
+ }
1570
+ function scanRustSource(text, lineStarts) {
1571
+ const { comments, error } = scanRust(text);
1572
+ if (error) {
1573
+ return { comments: [], syntaxError: { message: error.message, line: lineOf(lineStarts, error.offset) + 1 } };
1574
+ }
1575
+ return {
1576
+ comments: comments.map((comment) => {
1577
+ return {
1578
+ type: comment.block ? "Block" : "Line",
1579
+ value: text.slice(comment.start + 2, comment.block ? comment.end - 2 : comment.end),
1580
+ start: comment.start,
1581
+ end: comment.end,
1582
+ doc: comment.doc
1583
+ };
1584
+ })
1585
+ };
1586
+ }
1587
+ function groupLineRuns(comments, text, lineStarts, markerOf) {
1588
+ const groups = [];
1589
+ let current;
1590
+ let previousLine = -1;
1591
+ let indent = "";
1592
+ let marker;
1593
+ for (const comment of comments) {
1594
+ const next = comment.type === "Line" ? markerOf(comment) : void 0;
1595
+ if (next === void 0 || !ownsLines(comment, text, lineStarts)) {
1596
+ current = void 0;
1597
+ continue;
1598
+ }
1599
+ const line = lineOf(lineStarts, comment.start);
1600
+ const before = text.slice(lineStarts[line], comment.start);
1601
+ if (current && line === previousLine + 1 && next === marker && before === indent) {
1602
+ current.push(comment);
1603
+ } else {
1604
+ current = [comment];
1605
+ indent = before;
1606
+ marker = next;
1607
+ groups.push(current);
1608
+ }
1609
+ previousLine = line;
1610
+ }
1611
+ return groups;
1612
+ }
1272
1613
  function analyzeSource(source, filename, options) {
1273
1614
  return inspectSource(source, filename, options).comments;
1274
1615
  }
@@ -1277,28 +1618,16 @@ function inspectSource(source, filename, options) {
1277
1618
  throw new Error("comments: maxWidth must be an integer at least as large as width");
1278
1619
  }
1279
1620
  const text = source.startsWith(BYTE_ORDER_MARK) ? source.slice(1) : source;
1280
- const { program, comments, errors } = parseSource(filename, text);
1621
+ const language = languageOf(filename);
1281
1622
  const lineStarts = computeLineStarts(text);
1282
- if (errors.length > 0 && program.body.length === 0) {
1283
- return { comments: [], syntaxError: describeFailure(errors, lineStarts) };
1623
+ const scanned = language.name === "rust" ? scanRustSource(text, lineStarts) : scanJavaScript(filename, text, lineStarts);
1624
+ if (scanned.syntaxError) {
1625
+ return { comments: [], syntaxError: scanned.syntaxError };
1284
1626
  }
1285
1627
  const sourceLines = text.split("\n").map((line) => stripCarriageReturn(line));
1286
- const parsed = comments.filter((comment) => !(comment.start === 0 && text.startsWith("#!")));
1287
1628
  const reports = [];
1288
- const blocks = parsed.filter((comment) => {
1289
- return comment.type === "Block" && ownsLines(comment, text, lineStarts) && !comment.value.includes(IGNORE_DIRECTIVE);
1290
- });
1291
- for (const block of blocks) {
1292
- const shape = shapeBlock(block, sourceLines, lineStarts);
1293
- const singleLineWidth = Math.max(
1294
- options.width,
1295
- Math.min(options.singleLineWidth ?? options.width, options.maxWidth ?? Infinity)
1296
- );
1297
- const singleLineLimit = Math.min(singleLineWidth + options.tolerance, options.maxWidth ?? Infinity);
1298
- const singleLine = shape.paragraphs.length === 1 ? singleLineLength(shape.paragraphs[0], options) : Infinity;
1299
- const paragraphOptions = singleLine <= singleLineLimit ? { ...options, width: Math.max(singleLineWidth, singleLine) } : options;
1300
- const lines = sourceLines.slice(shape.startLine, shape.endLine + 1);
1301
- reports.push({
1629
+ const report = (shape, lines, paragraphs) => {
1630
+ return {
1302
1631
  file: filename,
1303
1632
  kind: shape.kind,
1304
1633
  startLine: shape.startLine + 1,
@@ -1308,8 +1637,47 @@ function inspectSource(source, filename, options) {
1308
1637
  maxWidth: options.maxWidth,
1309
1638
  lengths: lines.map((line) => visualLength(line.trimEnd(), options.tabWidth)),
1310
1639
  text: lines.join("\n"),
1311
- paragraphs: shape.paragraphs.map((paragraph) => analyzeParagraph(paragraph, paragraphOptions))
1640
+ paragraphs
1641
+ };
1642
+ };
1643
+ const blocks = scanned.comments.filter((comment) => {
1644
+ return comment.type === "Block" && ownsLines(comment, text, lineStarts) && !comment.value.includes(IGNORE_DIRECTIVE);
1645
+ });
1646
+ const runs = (markerOf) => {
1647
+ return groupLineRuns(scanned.comments, text, lineStarts, markerOf).filter((group) => {
1648
+ return !group.some((comment) => comment.value.includes(IGNORE_DIRECTIVE));
1312
1649
  });
1650
+ };
1651
+ const shapes = [
1652
+ ...blocks.map((block) => shapeBlock(block, sourceLines, lineStarts, language)),
1653
+ ...runs((comment) => comment.doc).map((group) => shapeLineRun(group, sourceLines, lineStarts, language)),
1654
+ ...runs((comment) => comment.doc ? void 0 : "//").map(
1655
+ (group) => shapeLineComment(group, sourceLines, lineStarts, language)
1656
+ )
1657
+ ].toSorted((a, b) => a.startLine - b.startLine);
1658
+ for (const shape of shapes) {
1659
+ const lines = sourceLines.slice(shape.startLine, shape.endLine + 1);
1660
+ if (shape.kind === "line") {
1661
+ const paragraphs = shape.paragraphs.map((paragraph) => analyzeLineParagraph(paragraph, options, language));
1662
+ if (paragraphs.some((paragraph) => paragraph.issues.length > 0)) {
1663
+ reports.push(report(shape, lines, paragraphs));
1664
+ }
1665
+ continue;
1666
+ }
1667
+ const singleLineWidth = Math.max(
1668
+ options.width,
1669
+ Math.min(options.singleLineWidth ?? options.width, options.maxWidth ?? Infinity)
1670
+ );
1671
+ const singleLineLimit = Math.min(singleLineWidth + options.tolerance, options.maxWidth ?? Infinity);
1672
+ const singleLine = shape.paragraphs.length === 1 ? singleLineLength(shape.paragraphs[0], options, language) : Infinity;
1673
+ const paragraphOptions = singleLine <= singleLineLimit ? { ...options, width: Math.max(singleLineWidth, singleLine) } : options;
1674
+ reports.push(
1675
+ report(
1676
+ shape,
1677
+ lines,
1678
+ shape.paragraphs.map((paragraph) => analyzeParagraph(paragraph, paragraphOptions, language))
1679
+ )
1680
+ );
1313
1681
  }
1314
1682
  return { comments: reports };
1315
1683
  }
@@ -1330,6 +1698,9 @@ function rewrapComment(report) {
1330
1698
  }
1331
1699
  var POSITION_KEYS = /* @__PURE__ */ new Set(["start", "end", "range", "loc"]);
1332
1700
  function codeFingerprint(source, filename) {
1701
+ if (languageOf(filename).name === "rust") {
1702
+ return rustFingerprint(source);
1703
+ }
1333
1704
  const { program, errors } = parseSource(filename, source);
1334
1705
  if (errors.length > 0) {
1335
1706
  return JSON.stringify({ source, errors: errors.map((error) => error.message) });
@@ -1343,7 +1714,7 @@ function codeFingerprint(source, filename) {
1343
1714
  // src/comments/agent.ts
1344
1715
  import { spawn } from "child_process";
1345
1716
  import { readFileSync } from "fs";
1346
- import path from "path";
1717
+ import path2 from "path";
1347
1718
  var DEFAULT_RULES = `
1348
1719
  # Comment rules
1349
1720
 
@@ -1351,7 +1722,7 @@ var DEFAULT_RULES = `
1351
1722
 
1352
1723
  When rules pull in different directions, the earlier one wins.
1353
1724
 
1354
- 1. Keep code and exact text byte for byte, including every punctuation character in it. That covers code blocks, \`@example\`, code spans, identifiers, string literals like error messages, regexes, globs and other patterns like \`*test*\`, URLs, quoted text, titles, and notation like \`host:port\`.
1725
+ 1. Keep code and exact text byte for byte, including every punctuation character in it. That covers code blocks, \`@example\`, code spans, identifiers, string literals like error messages, regexes, globs and other patterns like \`*test*\`, URLs, quoted text, titles, notation like \`host:port\`, and Rust intra-doc links (see below).
1355
1726
  2. Keep the meaning. An edit may make a comment clearer, never different.
1356
1727
  3. Follow the style preferences below.
1357
1728
  4. Change as little as possible. Wording that already reads clearly stays, even when a note points at it.
@@ -1364,10 +1735,12 @@ When rules pull in different directions, the earlier one wins.
1364
1735
  - A comment whose whole text is one paragraph stays on a single line when that line fits a separate, larger single-line width, even past the width. Only a longer comment wraps to the width.
1365
1736
  - Separate paragraphs with an empty comment line.
1366
1737
  - Never split a backtick code span across lines.
1738
+ - Never split a Markdown link across lines. A link is the text in brackets and the target right after it, like \`[the docs](https://example.com)\`, \`[the docs][label]\`, or \`[the docs]\`, and all of it stays on one line, from its first bracket to its last bracket or parenthesis. An editor can't follow a link that continues on the next line. When a line is too long, break it before or after the link.
1367
1739
  - Keep code blocks, \`@example\`, lists, tables, box drawings, aligned columns, and Markdown hard line breaks (a line that ends with two spaces or a backslash) laid out as they are. List items wrap with their continuation lines indented to the item text, and a JSDoc tag description that doesn't fit continues on the next line without indentation.
1368
1740
  - The tool measures every line, taking the tolerance and the parts it keeps as written into account, and the notes on each comment list what is still wrong. Act on those notes. Don't count columns, and don't apply a stricter limit than the notes show.
1369
1741
  - The maximum is a hard limit for measured prose, including long tokens. If a URL, code span, or other exact text exceeds it, preserve that text and explain the unresolved overflow instead of changing its contents.
1370
1742
  - The tool never rewraps the parts it keeps as written, like a single-line block comment, a paragraph with hard line breaks, or lines that may be laid out by hand. When a note says one of their prose lines is too long, break or shorten it yourself. Exact text and intentional layout win over the width, so keep code, code spans, and the layout in them intact, and if a note can only be fixed by changing them, leave it and say why in your reply.
1743
+ - The tool never rewraps a line comment (\`//\`) either. When a note says one of its lines is too long, shorten the wording so the comment fits on its line and still says the same thing. When it can't be shortened without losing meaning, turn it into a multi-line block comment instead: \`/**\` in JavaScript and TypeScript, or \`/*\` in Rust, on its own line, then the text on lines that start with \` * \`, then \` */\` on its own line, all at the indentation of the line comment. Like other block comments, it starts with a capital letter and ends with punctuation. Leave its line breaks to the tool, which rewraps it like any other block comment. Never split a line comment into more \`//\` lines.
1371
1744
 
1372
1745
  ## Style
1373
1746
 
@@ -1378,8 +1751,16 @@ These are preferences for comment prose. They never apply to code or exact text
1378
1751
  - Prefer a sentence over a "Label: explanation" fragment, like \`...the socket. Optional: without it...\` or \`Default: refuse\` in the middle of prose. A colon is fine where it introduces a list, code block, or example, in a term's definition, in notation, in a quotation, and in a \`Note:\` or \`Optional:\` that starts a comment. A JSDoc tag like \`@note\` or \`@todo\` can replace a label, and \`@default\` fits a default value, not a description.
1379
1752
  - Avoid asterisk emphasis like \`*this*\`, and use plain wording instead. A name or pattern in asterisks, like \`*test*\` or \`*.spec.ts\`, is a pattern, so keep its asterisks and wrap it in a code span, like \`\` \`*test*\` \`\`.
1380
1753
  - Avoid section banner comments, i.e. dividers or headings made of dashes, equals signs, asterisks, or other repeated characters that split a file into visual sections. Let the code structure carry the grouping, or write one short, plain sentence comment (e.g. \`// handlers for control mode\`). Rules that belong to a diagram, a table, a box drawing, a Markdown document, an example, or a legal header aren't banners.
1381
- - Block and JSDoc comments are capitalized and end with punctuation.
1382
- - Never turn a block or JSDoc comment into \`//\` line comments, and never write a run of consecutive \`//\` lines.
1754
+ - Block, JSDoc, and Rust doc comments are capitalized and end with punctuation.
1755
+ - Never change the kind of a comment, except to turn a line comment that is too long into a block comment (see Layout). Don't turn a block or JSDoc comment into \`//\` line comments, and never write a run of consecutive \`//\` lines. In Rust, a run of \`///\` or \`//!\` lines is one doc comment, so keep its markers, and never turn a doc comment (\`///\`, \`//!\`, \`/**\`, \`/*!\`) into another kind, since that changes or removes documentation.
1756
+
1757
+ ## Rust doc comments
1758
+
1759
+ Rust doc comments are Markdown that rustdoc renders.
1760
+
1761
+ - Intra-doc links are exact text. Keep their brackets, backticks, and targets, like \`\` [\`count_provider_reads\`] \`\`, \`\` [\`permissions\`](crate::permissions) \`\`, \`\` [the parser][\`Parser\`] \`\`, or \`[Vec<T>]\`, since rustdoc resolves the text in brackets, or the target after it, as a path. Each link stays on one line (see Layout), including a link with spaces in its text, like \`[the provider module](crate::provider)\`.
1762
+ - A link reference definition, like \`\` [\`Foo\`]: crate::Foo \`\`, stays on its own line as it is. Its colon is syntax, not a label.
1763
+ - Headings like \`# Examples\`, \`# Panics\`, \`# Errors\`, and \`# Safety\` are sections that rustdoc and readers look for, so keep them. Code blocks are doctests, so keep their lines, including hidden lines that start with \`#\`, exactly as they are.
1383
1764
 
1384
1765
  ## Editing
1385
1766
 
@@ -1388,11 +1769,11 @@ These are preferences for comment prose. They never apply to code or exact text
1388
1769
  `.trim();
1389
1770
 
1390
1771
  // src/comments/config.ts
1391
- import { existsSync, readFileSync as readFileSync2 } from "fs";
1392
- import { createRequire } from "module";
1393
- import path2 from "path";
1772
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
1773
+ import { createRequire as createRequire2 } from "module";
1774
+ import path3 from "path";
1394
1775
  import { pathToFileURL } from "url";
1395
- var DEFAULT_EXTENSIONS = Object.freeze([".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx", ".mts", ".cts"]);
1776
+ var DEFAULT_EXTENSIONS = Object.freeze([".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx", ".mts", ".cts", ".rs"]);
1396
1777
  var CONFIG_FILES = [
1397
1778
  "comments.config.ts",
1398
1779
  "comments.config.mts",
@@ -1443,10 +1824,12 @@ function validateConfig(raw, filepath) {
1443
1824
  assertType(config.avoidSentenceOrphans, "boolean", "avoidSentenceOrphans", filepath);
1444
1825
  assertType(config.style, "boolean", "style", filepath);
1445
1826
  assertType(config.extensions, "array", "extensions", filepath);
1827
+ assertType(config.include, "array", "include", filepath);
1446
1828
  assertType(config.ignore, "array", "ignore", filepath);
1447
1829
  assertType(config.agent, "object", "agent", filepath);
1448
1830
  assertType(config.hook, "object", "hook", filepath);
1449
1831
  assertStrings(config.extensions, "extensions", filepath);
1832
+ assertStrings(config.include, "include", filepath);
1450
1833
  assertStrings(config.ignore, "ignore", filepath);
1451
1834
  for (const key of ["width", "maxWidth", "singleLineWidth", "tolerance"]) {
1452
1835
  const value = config[key];
@@ -1479,8 +1862,8 @@ function validateConfig(raw, filepath) {
1479
1862
  }
1480
1863
  assertType(agent.rules, "string", "agent.rules", filepath);
1481
1864
  assertType(agent.instructions, "string", "agent.instructions", filepath);
1482
- if (agent.scope !== void 0 && agent.scope !== "suspicious" && agent.scope !== "all") {
1483
- throw new Error(`comments: \`agent.scope\` in ${filepath} must be "suspicious" or "all"`);
1865
+ if (agent.scope !== void 0 && !["suspicious", "failing", "all"].includes(agent.scope)) {
1866
+ throw new Error(`comments: \`agent.scope\` in ${filepath} must be one of suspicious, failing, all`);
1484
1867
  }
1485
1868
  const hook = config.hook ?? {};
1486
1869
  if (hook.mode !== void 0 && !["agent", "report", "off"].includes(hook.mode)) {
@@ -1490,25 +1873,25 @@ function validateConfig(raw, filepath) {
1490
1873
  }
1491
1874
  async function loadConfig(cwd, options = {}) {
1492
1875
  if (options.explicitPath) {
1493
- const filepath = path2.resolve(cwd, options.explicitPath);
1876
+ const filepath = path3.resolve(cwd, options.explicitPath);
1494
1877
  return { config: validateConfig(await readConfigFile(filepath), filepath), filepath };
1495
1878
  }
1496
- let directory = path2.resolve(cwd);
1879
+ let directory = path3.resolve(cwd);
1497
1880
  for (; ; ) {
1498
1881
  for (const name of CONFIG_FILES) {
1499
- const filepath = path2.join(directory, name);
1500
- if (existsSync(filepath)) {
1882
+ const filepath = path3.join(directory, name);
1883
+ if (existsSync2(filepath)) {
1501
1884
  return { config: validateConfig(await readConfigFile(filepath), filepath), filepath };
1502
1885
  }
1503
1886
  }
1504
- const packageJsonPath = path2.join(directory, "package.json");
1505
- if (existsSync(packageJsonPath)) {
1887
+ const packageJsonPath = path3.join(directory, "package.json");
1888
+ if (existsSync2(packageJsonPath)) {
1506
1889
  const value = readJson(packageJsonPath)[PACKAGE_JSON_KEY];
1507
1890
  if (value !== void 0) {
1508
1891
  return { config: validateConfig(value, packageJsonPath), filepath: packageJsonPath };
1509
1892
  }
1510
1893
  }
1511
- const parent = path2.dirname(directory);
1894
+ const parent = path3.dirname(directory);
1512
1895
  if (directory === options.stopDir || parent === directory) {
1513
1896
  return { config: {} };
1514
1897
  }