@d3lm/lint-preset 1.5.2 → 1.5.5

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/README.md CHANGED
@@ -260,7 +260,8 @@ Verdicts are layout issues that can be measured without reading the prose. They
260
260
 
261
261
  Hints come from heuristics and can be false positives, because only the meaning of the prose shows whether the rule applies. A hint only sends a comment to the agent, which decides whether to change it. `check` only prints comments with verdicts, so a passing check stays quiet apart from its summary, even when hints remain after review. Use `list --suspicious` to inspect hints, which are marked with `(hint)` and never fail a check:
262
262
 
263
- - `orphan` means a line ends with the first word of a sentence, or the first of two lines ends with the start of a sentence that fits on the second without making it longer than the first
263
+ - `orphan` means a line ends with the first word of a sentence, a line ends with the first two words of a sentence that the suggested rewrap moves to the next line, or the first of two lines ends with the start of a sentence that fits on the second without making it longer than the first
264
+ - `split-parenthetical` means a line ends inside a parenthetical that the suggested rewrap keeps on one line within `maxWidth`
264
265
  - `em-dash` means the comment uses `—`
265
266
  - `dash` means the comment uses `–` or `--` between words
266
267
  - `semicolon` means the comment joins clauses with a semicolon
@@ -268,33 +269,25 @@ Hints come from heuristics and can be false positives, because only the meaning
268
269
  - `emphasis` means the comment uses `*emphasis*` or `**bold**`
269
270
  - `banner` means the comment is a divider made of repeated characters
270
271
 
271
- A paragraph with an `orphan` hint is not reported as `unbalanced`, because the suggested rewrap moves the word that the hint is about, and the agent may rightly keep that word where it is. A line that ends early is still an `underfill`, whatever its last word is.
272
+ A paragraph with an `orphan` hint is not reported as `unbalanced` when that word is all that sets it apart from the suggested rewrap, because the agent may rightly keep the word where it is. This holds when the paragraph matches the rewrap that allows orphans. A paragraph that differs from that rewrap too, like a line that ends with the hinted word while more words fit on it, is still `unbalanced`. A line that ends early is still an `underfill`, whatever its last word is.
273
+
274
+ In the same way, a paragraph that splits a parenthetical is not reported as `unbalanced` when the split is all that sets it apart from the suggested rewrap, because the split can read as well and keep the line closer to `width`. This holds when the lines before the split match the rewrap and the lines after it match the rewrap of the text after the split. Otherwise the paragraph is still `unbalanced`.
272
275
 
273
276
  Hints are only checked in prose, never in code spans, code blocks, `@example`, or URLs. Rules that can't be measured, like writing complete sentences, are part of the agent's prompt (see `DEFAULT_RULES` in `src/comments/agent.ts`). The prompt treats the style rules as preferences, so the agent keeps punctuation that carries meaning or reads clearly, and never changes code, code spans, literals, patterns, or quotations.
274
277
 
275
278
  ### Text Kept as Written
276
279
 
277
- The tool rewraps prose paragraphs, list items, and JSDoc tag descriptions. It keeps everything else exactly as written. Those parts have no suggestion, and any fix to them is manual. A paragraph is a run of lines that ends at an empty line, a code fence, a JSDoc tag, or a list item. Which checks run in a paragraph that is kept depends on its syntax, never on what the text means:
280
+ The tool rewraps prose paragraphs, list items, and JSDoc tag descriptions. Everything else is kept exactly as written and has no suggestion, so the agent or you have to fix it by hand:
278
281
 
279
- - Code gets no checks besides `banner`. This covers fenced and indented code blocks, the content of `@example`, `@default`, and `@defaultValue`, directives like `eslint-disable-next-line` or `@ts-expect-error` (only their exact names, so a line that starts with a package name like `eslint-config-prettier` stays prose), legal comments (`/*!`, `@license`, `@preserve`, and copyright notices, outside of code spans), and paragraphs that parse as JavaScript.
280
- - A line that only looks like code gets `overflow` but no hints, since the hints would point at the code. The whole paragraph is kept as written. This covers lines that end with `{`, start with `}`, `]`, or `)`, declare a `const`, `let`, or `var`, or are an `import` or `export` with `from` or braces, as well as lines that end with `;` and hold code characters or no spaces. Prose can look like that too, like a line that starts with `import them from`, so the width is still measured.
281
- - Layout syntax gets hints but no verdicts. It exempts the whole paragraph that holds one of these lines. That covers a line that starts with `|` and a Markdown table delimiter row like `--- | ---`, and any box-drawing character from `─` to `╿` (U+2500 to U+257F). It also covers a line that starts with four or more of the same character among `-=*#_~+/\` or ends with four or more among `-=*#_~` (but not a Markdown heading or a row of column underlines like `---- ----`), and aligned columns, meaning a gap of two or more spaces after which text continues at the same column on at least two lines.
282
- - Text laid out by hand gets hints and `overflow`. This covers a Markdown hard line break, meaning a line that ends with two or more spaces or with a backslash and is followed by another line of the same paragraph. The whole paragraph is kept byte for byte, trailing spaces included. Like in Markdown, a backslash that another backslash escapes, as in `\\`, and trailing spaces on the last line of a paragraph don't break a line, so they're rewrapped like any other prose. It also covers text that may be laid out by hand: a Markdown heading, link definition, HTML tag, or `>` quote at the start of a line (prose can start a line like `<T> is the type` or `# of retries` too), a line with two or more `|`, arrows like `-->`, `<--`, or `→`, rules like `---`, `+--`, or `====`, and two or more lines that each start with a term and a dash or colon, like `width - the maximum width`. Single-line block comments and block comments without a `*` on each line are checked the same way. So is prose that the tool can't rewrap without changing how it reads or adding a line that is too long. That is a paragraph whose rewrap would itself read as code or layout, like one that would end with `;` on the same line as a `>`. It is also a paragraph with a code span or `{...}` group that continues on the next line and doesn't fit on one line, which is what a stray backtick or brace looks like too. JSDoc tags whose content isn't a description, like `@typedef`, are never rewrapped but still have to fit the width.
282
+ - Code gets no checks besides `banner`. This covers code blocks, `@example`, `@default`, and `@defaultValue`, tool directives like `eslint-disable-next-line` or `@ts-expect-error`, legal comments (`/*!`, `@license`, `@preserve`, and copyright notices), and paragraphs that parse as JavaScript.
283
+ - Layout syntax gets hints but no verdicts, in the whole paragraph around it. This covers Markdown tables (a line that starts with `|`, or a delimiter row like `--- | ---`), box-drawing characters (U+2500 to U+257F), a line that starts with four or more of the same character among `-=*#_~+/\` or ends with four or more among `-=*#_~` (except Markdown headings and column underlines like `---- ----`), and aligned columns (a gap of two or more spaces after which text continues at the same column on at least two lines).
284
+ - Text that only looks like code or hand layout keeps its line breaks but still gets `overflow`, since prose can look like that too. Lines that look like code, such as ones that end with `{` or `;` or start with `import`, get no hints. Hand layout, such as Markdown hard line breaks, headings, quotes, arrows, and `term - description` lists, still gets hints.
283
285
 
284
- A comment with an issue in a part that is kept is handed to the agent, which can fix the prose by hand. To keep a free-form drawing that is wider than the width, put it in a code fence, or add `comments-ignore` to the comment.
286
+ To keep a drawing that is wider than the width, put it in a code fence or add `comments-ignore` to the comment.
285
287
 
286
288
  ### Configuration
287
289
 
288
- `width` is the preferred line length; `maxWidth` is the hard maximum, including indentation and the comment prefix. The CLI defaults to a maximum of 120 columns (or `width`, if larger). A balanced parenthetical can extend a line past `width` when this keeps it together within `maxWidth`. In longer paragraphs, the wrapper balances all lines together and may split a parenthetical when that produces a better layout. A line never sticks out more than 15 columns past the line before it, like a full second line after a first line that the orphan rule keeps short, when up to two more lines avoid that. If the parenthetical cannot fit on a line of its own, it wraps normally. For example, `width: 80, maxWidth: 120` keeps `(JSDoc types, \`{@link ...}\`)` together in the example below:
289
-
290
- ```ts
291
- /**
292
- * Splits prose into words, keeping backtick code spans and `{...}` groups (JSDoc types, `{@link ...}`)
293
- * intact so that they are never broken across lines.
294
- */
295
- ```
296
-
297
- The separate `singleLineWidth` allowance is capped by `maxWidth`. Code and documented layout regions retain their exemptions. An indivisible token in measured prose that exceeds `maxWidth` is reported as overflow; the wrapper preserves it, so resolving that case requires changing the maximum or the comment manually. `tolerance` lets prose lines go up to that many columns past `width` when that saves a line in a paragraph of three or more lines, and it never goes past `maxWidth`. It defaults to 0. When `maxWidth` is omitted, `tolerance` also sets a legacy maximum of `width + tolerance`. Direct `analyzeSource` callers can omit `maxWidth` to retain the legacy width/tolerance behavior.
290
+ `width` is the preferred line length, and `maxWidth` is the hard maximum, including indentation and the comment prefix. The CLI defaults `maxWidth` to 120, or to `width` if that is larger. The wrapper may go past `width`, up to `maxWidth`, to keep a parenthetical on one line. `tolerance` (default 0) lets lines in paragraphs of three or more lines go that many columns past `width` when that saves a line. A comment that is one paragraph stays on one line up to `singleLineWidth` plus `tolerance`, capped by `maxWidth`. A word or code span longer than `maxWidth` is never split and is reported as `overflow`.
298
291
 
299
292
  Like prettier, `comments` looks for a config file in the current directory and its parents, up to the repository root. The config can be a `comments.config.{ts,mts,js,mjs,cjs}`, `.commentsrc`, or `.commentsrc.json` file, or a `comments` key in `package.json`. All options are optional.
300
293
 
@@ -306,13 +299,10 @@ export default defineConfig({
306
299
  // defaults to prettier's printWidth, or 80
307
300
  width: 100,
308
301
  maxWidth: 120,
309
- // prose lines may go this many columns past `width` when that saves a line
310
302
  tolerance: 0,
311
- // a comment that is one paragraph stays on one line up to this length, and wraps to `width` beyond it
312
303
  singleLineWidth: 120,
313
304
  avoidSentenceOrphans: true,
314
- // look for possible semicolons, em-dashes, dashes, label colons, emphasis, and banners as hints,
315
- // which only decides what the agent sees, since its rules apply to every comment it gets
305
+ // report the style hints, like `em-dash` and `semicolon`
316
306
  style: true,
317
307
  ignore: ['src/generated/**'],
318
308
  agent: {
@@ -333,9 +323,7 @@ export default defineConfig({
333
323
 
334
324
  Most options also have a command line flag that overrides the config file for a single run. See `comments --help` for the full list.
335
325
 
336
- Comments are split across agents, because long prompts make an agent slower and less careful. All comments of a file go to the same agent, so two agents never edit the same file. Up to `maxAgents` agents run at once, and the rest wait their turn.
337
-
338
- In a terminal, `comments agent` shows a live table of the agents while they run. Afterwards, it lists the comments that still have issues and lets you open each agent's reply. `--no-interactive` skips that list, so the command shows the agents while they run and exits when they're done. Without a terminal, for example in a hook or in CI, it prints plain progress lines and only prints the replies of agents that failed. `--verbose` prints every reply in both cases.
326
+ Comments are split across several agents, and all comments of a file go to the same agent. In a terminal, `comments agent` shows the agents while they run, then lists the comments that still have issues. `--no-interactive` skips that list, and `--verbose` prints every agent's reply.
339
327
 
340
328
  `agent.rules` points to a Markdown file that replaces the built-in formatting rules. `agent.command` runs any other agent CLI, like `['my-agent', '--prompt', '{prompt}']`. Without a `{prompt}` placeholder, the prompt goes to stdin.
341
329
 
@@ -25,7 +25,7 @@ When rules pull in different directions, the earlier one wins.
25
25
  ## Layout
26
26
 
27
27
  - After you finish, the tool rewraps prose paragraphs, list items, and JSDoc tag descriptions. The width is a preferred target. A parenthetical may extend a line up to the configured maximum so it stays together, and lines may go up to the configured tolerance past the width when that saves a line. In longer paragraphs, balanced lines take priority over keeping every parenthetical on one line. The tool also avoids short last lines where possible. Leave these line breaks to the tool, except for the sentence-orphan case below.
28
- - Don't end a line with the first word of a new sentence, clause, or parenthetical, like \`This\` after a period, \`the\` after a comma, or \`(the\`. Move it to the next line instead.
28
+ - Don't end a line with the first word of a new sentence, clause, or parenthetical, like \`This\` after a period, \`the\` after a comma, or \`(the\`. Move it to the next line instead. The same goes for the first two words of a sentence, like \`It allocates\` after a period, when the note points at them. A long word, like an identifier or a code span, may stay at the end of a line when moving it would leave that line far shorter than the lines around it. Then leave the line breaks as they are.
29
29
  - In a paragraph of two lines, the first line ends with a sentence whenever the rest fits on the second line and is no longer than the first. Don't end it with the start of the next sentence, like \`A wrap\` in \`...lay out differently. A wrap\`. Move those words to the second line instead.
30
30
  - 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.
31
31
  - Separate paragraphs with an empty comment line.
@@ -65,7 +65,8 @@ var ISSUE_DESCRIPTIONS = {
65
65
  underfill: "ends early, and words from the next line fit on it",
66
66
  widow: "leaves fewer than three words on the last line of its paragraph",
67
67
  unbalanced: "is in a paragraph whose lines are less even than the suggested rewrap",
68
- orphan: "may end with the start of a new sentence, clause, or parenthetical, which belongs on the next line (leave it if the period or comma before it is part of an abbreviation, a name, or notation like `No. 3`)",
68
+ orphan: "may end with the start of a new sentence, clause, or parenthetical, which belongs on the next line (leave it if the period or comma before it is part of an abbreviation, a name, or notation like `No. 3`, or if it is a long word like an identifier and moving it would leave the line far shorter than the lines around it)",
69
+ "split-parenthetical": "may split a parenthetical that the suggested rewrap keeps on one line, past the width but within the maximum (leave it if the split reads as well, like at a comma inside a list, and the line stays closer to the width)",
69
70
  "em-dash": "may use an em-dash to join two thoughts (leave it if it's in a quotation, a title, or a string literal, or reads more clearly than the alternatives)",
70
71
  banner: "may be a section banner (leave it if it's part of a diagram, a table, a box, a Markdown rule, a separator in an example, or legal text)",
71
72
  semicolon: "may join clauses with a semicolon (leave it if it's code, a literal, not a clause join, or reads more clearly than a period or a joining word)",
@@ -131,7 +132,8 @@ function buildPrompt(options) {
131
132
  `Fix the formatting of the comments below, following the rules above. Paths are relative to ${options.root}. Widths include indentation and the comment prefix. The notes under each comment come from the tool's measurements, so never count columns yourself.`,
132
133
  "",
133
134
  "- After you finish, the tool rewraps every prose paragraph as described under Layout, so a layout note on a prose paragraph needs no change from you. Change only the wording there, and leave its line breaks to the tool.",
134
- "- A note that a line may end with the start of a sentence, clause, or parenthetical is the exception. Only you can tell whether it does, so if it does, move those words to the start of the next line.",
135
+ "- A note that a line may end with the start of a sentence, clause, or parenthetical is the exception. Only you can tell whether it does, so if it does, move those words to the start of the next line, unless the Layout rules let a long word stay.",
136
+ "- A note that a line may split a parenthetical is the other exception. If the parenthetical reads better on one line, move the rest of it up to the end of that line yourself.",
135
137
  "- Parts kept as written are never rewrapped, and their notes say so. Fix those notes by hand, for example by breaking an overflowing prose line or by turning a long single-line comment into a multi-line one. Keep code, code spans, tables, box drawings, hard line breaks, and other intentional layout in them exactly as they are. If a note can only be fixed by changing those, leave it and say why in your reply.",
136
138
  `- Notes marked \`${HINT_MARKER}\` come from heuristics. If the rule doesn't actually apply, don't change anything for that note.`,
137
139
  "- Notes only point at places worth a look. Review each comment against the rules, and leave it as it is when it already follows them.",
@@ -489,6 +491,9 @@ function parentheticalGroups(tokens, limit) {
489
491
  }
490
492
  return groups;
491
493
  }
494
+ function parentheticalAround(tokens, at, limit) {
495
+ return parentheticalGroups(tokens, limit).find((group) => group.start < at && at < group.end);
496
+ }
492
497
  function bindParentheticals(tokens, limit) {
493
498
  const groups = new Map(parentheticalGroups(tokens, limit).map((group) => [group.start, group.end]));
494
499
  const units = [];
@@ -578,6 +583,8 @@ function hasWidow(lines) {
578
583
  }
579
584
  var FULLNESS_WEIGHT = 0.07;
580
585
  var PARENTHETICAL_SPLIT_WEIGHT = 2;
586
+ var SENTENCE_START_COST = 25;
587
+ var MAX_SENTENCE_START_WORDS = 2;
581
588
  var MAX_STEP = 15;
582
589
  var EXTRA_LINES = 2;
583
590
  function fewestLines(starts, target) {
@@ -665,7 +672,7 @@ function balanceTokens(tokens, firstLimit, restLimit, count, options, preferredL
665
672
  afterOrphan: tokens.map((_, index) => {
666
673
  return options.avoidSentenceOrphans && index >= 2 && isOrphan(tokens[index - 2], tokens[index - 1]);
667
674
  }),
668
- splitCosts
675
+ splitCosts: withSentenceStartCosts(tokens, splitCosts, options)
669
676
  };
670
677
  const targets = [];
671
678
  for (let target = restLimit; target >= Math.max(longest, Math.floor(restLimit / 2)); target -= 1) {
@@ -731,6 +738,22 @@ function splitCostsOf(tokens, limit) {
731
738
  }
732
739
  return splitCosts;
733
740
  }
741
+ function withSentenceStartCosts(tokens, splitCosts, options) {
742
+ if (!options.avoidSentenceOrphans || options.ignoreSentenceStarts) {
743
+ return splitCosts;
744
+ }
745
+ const costs = new Map(splitCosts);
746
+ for (const [index, token] of tokens.entries()) {
747
+ if (!endsSentence(token)) {
748
+ continue;
749
+ }
750
+ const last = Math.min(index + 1 + MAX_SENTENCE_START_WORDS, tokens.length - 1);
751
+ for (let end = index + 2; end <= last && !endsSentence(tokens[end - 1]); end += 1) {
752
+ costs.set(end, (costs.get(end) ?? 0) + SENTENCE_START_COST);
753
+ }
754
+ }
755
+ return costs;
756
+ }
734
757
  function hasParentheticals(tokens, restLimit, options) {
735
758
  if (options.parentheticalOverflow === void 0) {
736
759
  return false;
@@ -893,6 +916,7 @@ var ISSUE_TIERS = {
893
916
  widow: "verdict",
894
917
  unbalanced: "verdict",
895
918
  orphan: "hint",
919
+ "split-parenthetical": "hint",
896
920
  banner: "hint",
897
921
  "em-dash": "hint",
898
922
  semicolon: "hint",
@@ -1388,9 +1412,48 @@ function analyzeParagraph(paragraph, options) {
1388
1412
  if (hasWidow(currentLines) && !hasWidow(wrapped)) {
1389
1413
  lineIssues.push({ type: "widow", line: currentLines.length - 1 });
1390
1414
  }
1391
- if (lineIssues.length === 0 && overflow.length === 0 && (currentLines.length === wrapped.length || options.maxWidth !== void 0)) {
1392
- const line = wrapped.findIndex((words, position) => words.join(" ") !== currentLines[position]?.join(" "));
1393
- lineIssues.push({ type: sameLayout(even, currentLines) ? "orphan" : "unbalanced", line: Math.max(line, 0) });
1415
+ const keepsOnlyOrphans = () => {
1416
+ const free = settle({ ...layoutOptions, avoidSentenceOrphans: false });
1417
+ return !("wrapped" in free) || sameLayout(free.wrapped, currentLines);
1418
+ };
1419
+ const keepsOnlySentenceStarts = () => {
1420
+ const free = settle({ ...layoutOptions, ignoreSentenceStarts: true });
1421
+ return "wrapped" in free && (sameLayout(free.even, currentLines) || sameLayout(free.wrapped, currentLines));
1422
+ };
1423
+ const splitsOnlyParenthetical = (line) => {
1424
+ if (layoutOptions.parentheticalOverflow === void 0) {
1425
+ return false;
1426
+ }
1427
+ const tokenCount = (lines) => tokenize(lines.flat().join(" ")).length;
1428
+ const end = tokenCount(currentLines.slice(0, line + 1));
1429
+ const group = parentheticalAround(tokens, end, restLimit + layoutOptions.parentheticalOverflow);
1430
+ if (!group) {
1431
+ return false;
1432
+ }
1433
+ const keepsGroup = wrapped.every((_, position) => {
1434
+ const breakAt = tokenCount(wrapped.slice(0, position + 1));
1435
+ return breakAt <= group.start || breakAt >= group.end;
1436
+ });
1437
+ if (!keepsGroup) {
1438
+ return false;
1439
+ }
1440
+ const restOptions = { ...layoutOptions, keepFirstLine: false };
1441
+ const rest = bindLineStarts(tokens.slice(end), startsLine);
1442
+ const restLines = layoutTokens(rest, restLimit, restLimit, restOptions);
1443
+ return sameLayout(breakAfterSentence(restLines, restLimit, restLimit, restOptions), currentLines.slice(line + 1));
1444
+ };
1445
+ if (overflow.length === 0 && (currentLines.length === wrapped.length || options.maxWidth !== void 0)) {
1446
+ const line = Math.max(
1447
+ wrapped.findIndex((words, position) => words.join(" ") !== currentLines[position]?.join(" ")),
1448
+ 0
1449
+ );
1450
+ if (lineIssues.length === 0 && (sameLayout(even, currentLines) || keepsOnlySentenceStarts())) {
1451
+ lineIssues.push({ type: "orphan", line });
1452
+ } else if (lineIssues.length === 0) {
1453
+ lineIssues.push({ type: splitsOnlyParenthetical(line) ? "split-parenthetical" : "unbalanced", line });
1454
+ } else if (lineIssues.every((issue) => issue.type === "orphan") && !sameLayout(even, currentLines) && !keepsOnlyOrphans()) {
1455
+ lineIssues.push({ type: "unbalanced", line });
1456
+ }
1394
1457
  }
1395
1458
  }
1396
1459
  const suggestion = wrapped.map(
@@ -1520,7 +1583,9 @@ function inspectSource(source, filename, options) {
1520
1583
  options.width,
1521
1584
  Math.min(options.singleLineWidth ?? options.width, options.maxWidth ?? Infinity)
1522
1585
  );
1523
- const paragraphOptions = shape.paragraphs.length === 1 && singleLineLength(shape.paragraphs[0], options) <= singleLineWidth ? { ...options, width: singleLineWidth } : options;
1586
+ const singleLineLimit = Math.min(singleLineWidth + options.tolerance, options.maxWidth ?? Infinity);
1587
+ const singleLine = shape.paragraphs.length === 1 ? singleLineLength(shape.paragraphs[0], options) : Infinity;
1588
+ const paragraphOptions = singleLine <= singleLineLimit ? { ...options, width: Math.max(singleLineWidth, singleLine) } : options;
1524
1589
  const lines = sourceLines.slice(shape.startLine, shape.endLine + 1);
1525
1590
  reports.push({
1526
1591
  file: filename,