@d3lm/lint-preset 1.6.0 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"sources":["/home/runner/work/lint-preset/lint-preset/dist/comments.cjs","../src/comments/analyze.ts","../src/comments/rust.ts","../src/comments/style.ts","../src/comments/wrap.ts","../src/comments/agent.ts","../src/comments/config.ts"],"names":["startsLine","balanced","structure","wrapped","even","existsSync","createRequire","readFileSync","path"],"mappings":"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","file":"/home/runner/work/lint-preset/lint-preset/dist/comments.cjs","sourcesContent":[null,"import { parseSync, type OxcError, type Statement } from 'oxc-parser';\nimport { isRustCode, rustFingerprint, scanRust } from './rust.js';\nimport {\n CODE_SPAN,\n findStyleIssues,\n isBanner,\n LIST_MARKER,\n replaceKeepingLines,\n stripNonProse,\n type StyleIssueType,\n} from './style.js';\nimport {\n bindLineStarts,\n breakAfterSentence,\n findLineIssues,\n hasWidow,\n layoutTokens,\n parentheticalAround,\n startsBlock,\n tokenize,\n type LayoutOptions,\n type LineIssue,\n type LineIssueType,\n type WrapOptions,\n} from './wrap.js';\n\n/**\n * Describes the syntax of a comment. Rust has doc comments for the item after them (`outer`, like `///` and `/**`)\n * and for the item that holds them (`inner`, like `//!` and `/*!`). A `line` comment is a run of plain line comments\n * (`//`), which is never rewrapped.\n */\nexport type CommentKind =\n | 'jsdoc'\n | 'block'\n | 'line'\n | 'outer-line-doc'\n | 'inner-line-doc'\n | 'outer-block-doc'\n | 'inner-block-doc';\n\nexport type ParagraphKind = 'prose' | 'list' | 'tag' | 'code' | 'other';\n\nexport interface AnalyzeOptions extends WrapOptions {\n /**\n * Sets the preferred line length, including indentation and the comment prefix.\n */\n readonly width: number;\n\n /**\n * Sets the hard limit for measured prose. Parentheticals may exceed `width` up to this limit.\n * When omitted, the existing width and tolerance behavior is retained.\n */\n readonly maxWidth?: number;\n\n /**\n * Sets the maximum length of the line that holds a comment whose whole text is one\n * prose paragraph. Such a comment stays on one line up to this length plus `tolerance`,\n * even past `width`, but never past `maxWidth`. It defaults to `width`.\n */\n readonly singleLineWidth?: number;\n\n readonly tabWidth: number;\n\n /**\n * Whether to report prose style issues like semicolons, em-dashes, and section banners.\n */\n readonly style: boolean;\n}\n\nexport type IssueType = LineIssueType | StyleIssueType;\n\nexport type IssueTier = 'verdict' | 'hint';\n\nconst ISSUE_TIERS: Record<IssueType, IssueTier> = {\n overflow: 'verdict',\n underfill: 'verdict',\n widow: 'verdict',\n unbalanced: 'verdict',\n orphan: 'hint',\n 'split-parenthetical': 'hint',\n banner: 'hint',\n 'em-dash': 'hint',\n semicolon: 'hint',\n 'label-colon': 'hint',\n emphasis: 'hint',\n dash: 'hint',\n};\n\nexport interface IssueReport {\n readonly type: IssueType;\n readonly tier: IssueTier;\n readonly line: number;\n readonly length: number;\n}\n\nexport interface ParagraphReport {\n readonly kind: ParagraphKind;\n readonly startLine: number;\n readonly endLine: number;\n\n /**\n * Whether the paragraph is prose that can be rewrapped. When `false`, `reason` says why.\n */\n readonly reflowable: boolean;\n readonly reason?: string;\n readonly issues: readonly IssueReport[];\n\n /**\n * Holds the exact source lines for `startLine` through `endLine` when the paragraph is\n * greedily wrapped to the width, or `null` when the paragraph isn't prose, e.g. code,\n * a table, a diagram, or `@example`. A paragraph that is accepted with the layout for a\n * wider width within the tolerance keeps its lines.\n */\n readonly suggestion: readonly string[] | null;\n}\n\nexport interface CommentReport {\n readonly file: string;\n readonly kind: CommentKind;\n readonly startLine: number;\n readonly endLine: number;\n readonly indent: string;\n readonly width: number;\n readonly maxWidth?: number;\n\n /**\n * Holds the length of each source line from `startLine` through `endLine`, with tabs expanded.\n */\n readonly lengths: readonly number[];\n readonly text: string;\n readonly paragraphs: readonly ParagraphReport[];\n}\n\nexport interface SyntaxFailure {\n readonly message: string;\n\n /**\n * Holds the 1-based line of the error, when the parser names one.\n */\n readonly line?: number;\n}\n\nexport interface SourceAnalysis {\n readonly comments: CommentReport[];\n\n /**\n * Describes the syntax error that the parser stopped at. The parser never sees the comments after\n * one, so such a source has no comments at all instead of only those before the error.\n */\n readonly syntaxError?: SyntaxFailure;\n}\n\ninterface BodyLine {\n /**\n * Holds the 0-based index of the line in the file.\n */\n readonly index: number;\n readonly prefix: string;\n readonly text: string;\n readonly verbatim: boolean;\n\n /**\n * Whether the source line ends with Markdown hard line break syntax, which `text` no longer\n * holds after trimming.\n */\n readonly hardBreak: boolean;\n}\n\ninterface RawParagraph {\n kind: ParagraphKind;\n lines: BodyLine[];\n marker: string;\n reason?: string;\n}\n\ninterface ParsedComment {\n readonly type: 'Line' | 'Block';\n\n /**\n * Holds the text between the comment markers, like the `/ text` of `/// text`.\n */\n readonly value: string;\n readonly start: number;\n readonly end: number;\n readonly doc?: 'outer' | 'inner';\n}\n\n/**\n * Holds what tells code, legal text, and directives apart in the comments of a language.\n */\ninterface Language {\n readonly name: 'javascript' | 'rust';\n\n /**\n * Whether a whole paragraph parses as code of the language.\n */\n readonly isCode: (text: string) => boolean;\n\n /**\n * Whether a single line looks like code of the language. Prose can look like that too.\n */\n readonly isCodeLine: (text: string) => boolean;\n readonly legal: RegExp;\n readonly blockDirective?: RegExp;\n\n /**\n * Whether lines that start with `@`, like `@param`, are JSDoc tags.\n */\n readonly tags: boolean;\n\n /**\n * Whether a code span that continues on the next line may be joined into one line. rustdoc keeps\n * and shows the spaces at the start of the next line inside the code span, and how many there are\n * depends on every line of the item's docs, so Rust keeps such a paragraph as it is.\n */\n readonly joinsCodeSpans: boolean;\n}\n\nconst IGNORE_DIRECTIVE = 'comments-ignore';\n\nconst FENCE = /^(?:`{3,}(?!.*`)|~{3,})/;\nconst JSDOC_TAG = /^@([A-Za-z]+)/;\nconst NESTED_LIST_MARKER = /^\\s+(?:[-*+]|\\d+[.)])\\s+/;\n\n/**\n * Tags whose content is a prose description, while others like `@example` hold code and\n * are never rewrapped.\n */\nconst PROSE_TAGS = new Set([\n 'arg',\n 'argument',\n 'deprecated',\n 'description',\n 'exception',\n 'note',\n 'param',\n 'prop',\n 'property',\n 'remarks',\n 'return',\n 'returns',\n 'see',\n 'since',\n 'summary',\n 'template',\n 'throws',\n 'todo',\n 'typeParam',\n 'yields',\n]);\n\n// tags whose content is code that runs until the next tag, empty lines included, like a multi-part `@example`\nconst CODE_TAGS = new Set(['default', 'defaultValue', 'example']);\n\n// tags followed by a name, as in `@param {Type} name - description`\nconst NAMED_TAGS = new Set(['arg', 'argument', 'param', 'prop', 'property', 'template', 'typeParam']);\n\n/**\n * Tool directives take effect in any comment line, so a JSDoc line that starts with one is kept\n * as it is. Each is the exact name of a directive, since prose can start a line with a package\n * name like `eslint-config-prettier`.\n */\nconst LINE_DIRECTIVE =\n /^\\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)/;\n\n/**\n * These directives are only read from a whole block comment, like `/* global foo *\\/`\n * or `/* eslint quotes: 1 *\\/`. Their markers are ordinary words, so they never count\n * as directives on a single line of a longer comment.\n */\nconst BLOCK_DIRECTIVE = /^\\s*(?:eslint\\s|globals?\\s|exported\\s|jshint\\s|jslint\\s|type\\s*:)/;\n\n/**\n * Legal comments are kept as they are. A comment is legal when it uses the same markers\n * as bundlers like esbuild (`/*!`, `//!`, `@license`, and `@preserve`), or when it is a\n * copyright notice like `Copyright (c) 2020 Foo`.\n */\nconst LEGAL_COMMENT = /^!|@license\\b|@preserve\\b|\\bcopyright\\s+(?:\\(c\\)|©|\\d{4})/i;\n\n/**\n * In Rust, `/*!` and `//!` start inner doc comments, so only the other markers make\n * a comment legal.\n */\nconst RUST_LEGAL_COMMENT = /@license\\b|@preserve\\b|\\bcopyright\\s+(?:\\(c\\)|©|\\d{4})/i;\n\n// a term of one or two words or a code span (stripped to two backticks), then a dash or colon and its definition\nconst DEFINITION = /^(?:``|[\\w$.[\\]]+(?:[ -][\\w$.[\\]]+)?)(?: [-–]|:) \\S/;\n\nconst BOX_DRAWING = /[\\u2500-\\u257F]/;\n\n// a Markdown table delimiter row like `--- | ---` or `|:--|--:|`, which needs no leading pipe\nconst TABLE_DELIMITER = /^\\|?\\s*:?-+:?\\s*(?:\\|\\s*:?-+:?\\s*)+\\|?$/;\n\nconst ARROWS_OR_RULES = /[▲▼◀▶→←↑↓]|-->|<--|\\+-{2,}|-{3,}|={4,}/;\n\n// two or more spaces after text, or a backslash that another backslash doesn't escape\nconst HARD_BREAK = /\\S {2,}$|(?<!\\\\)\\\\(?:\\\\\\\\)*$/;\n\n/**\n * Describes how a paragraph that the tool keeps as written is checked.\n *\n * - `code`: code that the parser accepts, which gets no checks besides banners\n * - `code-like`: a paragraph with a line that may be code, like one that ends with `{`, which gets\n * no style hints, since they would point at the code, but must still fit the width\n * - `layout`: documented layout syntax like a table row, which gets style hints but no verdicts\n * - `manual`: text that is or may be laid out by hand or may be markup, like a Markdown hard line\n * break, arrows, or a heading, which gets style hints and must still fit the width.\n */\ntype StructureTier = 'code' | 'code-like' | 'layout' | 'manual';\n\ninterface Structure {\n readonly tier: StructureTier;\n readonly reason: string;\n}\n\n/**\n * Whether a statement is something prose also parses as, like a single word (`Done`),\n * a code span, a word before a code span (`Returns \\`null\\``, a tagged template), or a\n * label (`Default: refuse`). A label that ends with a semicolon, like the commented-out\n * CSS declaration `display: none;`, is code.\n */\nfunction isProseLike(statement: Statement, source: string): boolean {\n if (statement.type === 'LabeledStatement') {\n return source[statement.end - 1] !== ';';\n }\n\n if (statement.type === 'EmptyStatement') {\n return true;\n }\n\n if (statement.type !== 'ExpressionStatement') {\n return false;\n }\n\n let { expression } = statement;\n\n while (expression.type === 'ParenthesizedExpression') {\n expression = expression.expression;\n }\n\n if (expression.type === 'TaggedTemplateExpression') {\n return expression.tag.type === 'Identifier';\n }\n\n return expression.type === 'Identifier' || expression.type === 'Literal' || expression.type === 'TemplateLiteral';\n}\n\nfunction parsesAsStatements(text: string): boolean {\n if (!/[()[\\]{}=;<>]/.test(text)) {\n return false;\n }\n\n // a generator body accepts `return`, `await`, and `yield`, which are common in commented-out code\n const source = `async function* comment() {\\n${text}\\n}`;\n const { program, errors } = parseSync('comment.tsx', source);\n const wrapper = program.body.at(0);\n\n if (errors.length > 0 || wrapper?.type !== 'FunctionDeclaration' || !wrapper.body) {\n return false;\n }\n\n const statements = wrapper.body.body;\n\n return statements.length > 0 && !statements.every((statement) => isProseLike(statement, source));\n}\n\nconst CODE_VALUES = new Set([\n 'Literal',\n 'ArrayExpression',\n 'ObjectExpression',\n 'CallExpression',\n 'NewExpression',\n 'ArrowFunctionExpression',\n 'FunctionExpression',\n 'UnaryExpression',\n 'MemberExpression',\n]);\n\n/**\n * A lone property whose value is a word or code span, like `Default: refuse`, reads as a label in\n * prose rather than as commented-out options.\n */\nfunction parsesAsProperties(text: string): boolean {\n if (!text.includes(':')) {\n return false;\n }\n\n const { program, errors } = parseSync('comment.ts', `({\\n${text}\\n})`);\n const statement = program.body.at(0);\n\n if (errors.length > 0 || program.body.length !== 1 || statement?.type !== 'ExpressionStatement') {\n return false;\n }\n\n let { expression } = statement;\n\n while (expression.type === 'ParenthesizedExpression') {\n expression = expression.expression;\n }\n\n if (expression.type !== 'ObjectExpression' || expression.properties.length === 0) {\n return false;\n }\n\n const { properties } = expression;\n\n if (!properties.every((property) => property.type === 'Property' && !property.shorthand && !property.method)) {\n return false;\n }\n\n return (\n properties.length > 1 ||\n text.trimEnd().endsWith(',') ||\n properties.some((property) => property.type === 'Property' && CODE_VALUES.has(property.value.type))\n );\n}\n\n/**\n * Prose almost never parses as code, since two words in a row are a syntax error.\n */\nfunction isCode(texts: readonly string[], language: Language): boolean {\n return language.isCode(texts.map((line) => line.replace(LIST_MARKER, '')).join('\\n'));\n}\n\n/**\n * A prose clause can end with a semicolon too, which is a style issue.\n * So `;` only ends code with code around it.\n */\nfunction isCodeLine(text: string): boolean {\n return (\n text.endsWith('{') ||\n (text.endsWith(';') && (/[()[\\]{}=<>]/.test(text) || !/\\s/.test(text))) ||\n /^[}\\])]/.test(text)\n );\n}\n\nconst JAVASCRIPT: Language = {\n name: 'javascript',\n isCode: (text) => parsesAsStatements(text) || parsesAsProperties(text),\n isCodeLine: (text) => {\n return (\n isCodeLine(text) ||\n /^(?:const|let|var)\\s+[\\w$]+\\s*[:=]/.test(text) ||\n /^(?:import|export)\\s.*(?:\\sfrom\\s|[{}])/.test(text)\n );\n },\n legal: LEGAL_COMMENT,\n blockDirective: BLOCK_DIRECTIVE,\n tags: true,\n joinsCodeSpans: true,\n};\n\nconst RUST: Language = {\n name: 'rust',\n isCode: (text) => isRustCode(text),\n isCodeLine: (text) => {\n return (\n isCodeLine(text) ||\n /^let\\s+(?:mut\\s+)?\\w+\\s*[:=]/.test(text) ||\n /^(?:pub(?:\\([\\w:\\s]+\\))?\\s+)?(?:const|static(?:\\s+mut)?)\\s+[A-Z_\\d]+\\s*:/.test(text) ||\n /^(?:pub(?:\\([\\w:\\s]+\\))?\\s+)?use\\s+[\\w:]*::/.test(text) ||\n /^#!?\\[/.test(text)\n );\n },\n legal: RUST_LEGAL_COMMENT,\n tags: false,\n joinsCodeSpans: false,\n};\n\n/**\n * Whether a paragraph parses as JavaScript code, like the check for Rust code in `isRustCode`.\n */\nexport function isJavaScriptCode(text: string): boolean {\n return JAVASCRIPT.isCode(text);\n}\n\nfunction languageOf(filename: string): Language {\n return filename.endsWith('.rs') ? RUST : JAVASCRIPT;\n}\n\nfunction layoutReason(prose: string, raw: string): string | undefined {\n if (prose.startsWith('|') || TABLE_DELIMITER.test(prose)) {\n return 'table row';\n }\n\n if (BOX_DRAWING.test(prose)) {\n return 'box drawing';\n }\n\n // a rule or border, like the `***` lines of a box, would be joined into the prose around it\n if (isBanner(raw)) {\n return 'repeated characters';\n }\n\n return undefined;\n}\n\n// a rule like `***`, `- - -`, or `___`\nconst THEMATIC_BREAK = /^(?:(?:\\*\\s*){3,}|(?:-\\s*){3,}|(?:_\\s*){3,})$/;\n\n/**\n * Headings, link definitions, HTML, quotes, and rules are markup, since joining them with the\n * next line would turn them into prose. That includes the start of a list item, like `- [x]: y`,\n * a quote like `>=`, an HTML comment like `<!--`, and a line of only `=` or `-`, which underlines\n * a heading. Prose can start a line like that too, like `<T> is the type` or `# of retries`,\n * so markup is kept as written, but it still has to fit the width.\n */\nfunction isMarkup(prose: string): boolean {\n const text = prose.replace(LIST_MARKER, '');\n\n return (\n /^#{1,6}(?:\\s|$)/.test(text) ||\n /^\\[[^\\]]+\\]:/.test(text) ||\n /^<(?:\\/?[a-z]|[!?])/i.test(text) ||\n text.startsWith('>') ||\n /^(?:=+|-+)$/.test(prose) ||\n THEMATIC_BREAK.test(prose)\n );\n}\n\nfunction mask(text: string): string {\n return text.replaceAll(/[^\\n]/g, 'x');\n}\n\nfunction maskCode(text: string): string {\n let masked = text.replaceAll(CODE_SPAN, mask);\n\n for (let previous = ''; previous !== masked; ) {\n previous = masked;\n masked = masked.replaceAll(/\\{[^{}]*\\}/g, mask);\n }\n\n return masked;\n}\n\n/**\n * Two spaces after a sentence are an old typing convention, and a gap on a single line is a typo,\n * so neither is a column.\n */\nfunction hasAlignedColumns(lines: readonly BodyLine[]): boolean {\n const masked = maskCode(lines.map((body) => body.text).join('\\n')).split('\\n');\n const seen = new Set<number>();\n\n for (const line of masked) {\n const columns = new Set([...line.matchAll(/[^\\s.!?][^\\S\\n]{2,}(?=\\S)/g)].map((gap) => gap.index + gap[0].length));\n\n if ([...columns].some((column) => seen.has(column))) {\n return true;\n }\n\n for (const column of columns) {\n seen.add(column);\n }\n }\n\n return false;\n}\n\nfunction structureOf(lines: readonly BodyLine[], language: Language): Structure | undefined {\n const trimmed = lines.map((line) => line.text.trim()).join('\\n');\n const texts = replaceKeepingLines(trimmed, CODE_SPAN, '``').split('\\n');\n\n /**\n * Types like `{string | null}`, links like `{@link a | b}`, and URLs hold pipes and dashes\n * that aren't layout.\n */\n const proseLines = stripNonProse(trimmed).split('\\n');\n\n // the parser only accepts a whole paragraph as code when it is code, since two words in a row are a syntax error\n if (\n isCode(\n lines.map((line) => line.text.trim()),\n language,\n )\n ) {\n return { tier: 'code', reason: 'code' };\n }\n\n let layout: string | undefined;\n let codeLike = false;\n\n // a hard break on the last line of a paragraph ends the paragraph instead, as in Markdown\n let manual = lines.slice(0, -1).some((line) => line.hardBreak) ? 'hard line break' : undefined;\n\n for (const [position, line] of texts.entries()) {\n const prose = proseLines[position].trim();\n\n layout ??= layoutReason(prose, lines[position]?.text ?? '');\n\n // a single line that looks like code may be prose, like one that starts with `import them from`\n if (language.isCodeLine(line.trim())) {\n codeLike = true;\n } else if (isMarkup(prose)) {\n manual ??= 'markup';\n } else if ((prose.match(/\\|/g)?.length ?? 0) >= 2) {\n manual ??= 'pipes';\n } else if (ARROWS_OR_RULES.test(prose)) {\n manual ??= 'arrows or rules';\n }\n }\n\n layout ??= hasAlignedColumns(lines) ? 'aligned columns' : undefined;\n\n if (layout) {\n return { tier: 'layout', reason: layout };\n }\n\n if (codeLike) {\n return { tier: 'code-like', reason: 'code-like line' };\n }\n\n /**\n * One term per line, like `width - the maximum width` above `height - the maximum height`, may be\n * a definition list.\n */\n if (texts.filter((text) => DEFINITION.test(text.trim())).length >= 2) {\n manual ??= 'definition-like lines';\n }\n\n return manual ? { tier: 'manual', reason: manual } : undefined;\n}\n\n/**\n * A Markdown code fence closes only with the character it opened with, repeated at least as\n * often. A tilde fence inside a backtick block, or three backticks inside a four-backtick\n * block, doesn't end it.\n */\nclass FenceTracker {\n private _open: string | undefined;\n\n isInside(): boolean {\n return this._open !== undefined;\n }\n\n next(text: string): boolean {\n const trimmed = text.trim();\n\n if (this._open === undefined) {\n this._open = FENCE.exec(trimmed)?.[0];\n\n return this._open !== undefined;\n }\n\n if (trimmed.startsWith(this._open) && new RegExp(`^\\\\${this._open[0]}+$`).test(trimmed)) {\n this._open = undefined;\n }\n\n return true;\n }\n}\n\nfunction segment(lines: readonly BodyLine[], language: Language): RawParagraph[] {\n const paragraphs: RawParagraph[] = [];\n const fence = new FenceTracker();\n\n let current: RawParagraph | undefined;\n let inCodeTag = false;\n\n const flush = (): void => {\n if (current) {\n while (current.lines.length > 1 && !current.lines.at(-1)?.text.trim()) {\n current.lines.pop();\n }\n\n paragraphs.push(current);\n\n current = undefined;\n }\n };\n\n const start = (kind: ParagraphKind, line: BodyLine, marker = '', reason?: string): void => {\n flush();\n\n current = { kind, lines: [line], marker, reason };\n };\n\n for (const line of lines) {\n const { text } = line;\n\n if (inCodeTag) {\n /**\n * A fence inside the example may hold a line like `@decorator()`, which only starts a tag\n * outside of it.\n */\n if (fence.isInside() || !JSDOC_TAG.test(text)) {\n fence.next(text);\n current?.lines.push(line);\n\n continue;\n }\n\n inCodeTag = false;\n }\n\n if (fence.isInside()) {\n current?.lines.push(line);\n fence.next(text);\n\n if (!fence.isInside()) {\n flush();\n }\n\n continue;\n }\n\n if (line.verbatim) {\n // a fence without a space after the comment prefix, like `//!```toml`, still opens a code block\n if (fence.next(text)) {\n start('code', line, '', 'code block');\n\n continue;\n }\n\n start('code', line, '', 'directive');\n flush();\n\n continue;\n }\n\n if (!text.trim()) {\n flush();\n\n continue;\n }\n\n if (fence.next(text)) {\n start('code', line, '', 'code block');\n\n continue;\n }\n\n if (/^\\s/.test(text)) {\n const holdsProse = current?.kind === 'list' || (current?.kind === 'tag' && !current.reason);\n const nested = holdsProse ? NESTED_LIST_MARKER.exec(text) : null;\n\n if (nested) {\n start('list', line, nested[0]);\n } else if (current && (current.kind === 'list' || current.kind === 'tag' || current.reason === 'indented')) {\n current.lines.push(line);\n } else {\n start('code', line, '', 'indented');\n }\n\n continue;\n }\n\n const tag = language.tags ? JSDOC_TAG.exec(text) : null;\n\n if (tag) {\n start('tag', line, '', PROSE_TAGS.has(tag[1]) ? undefined : `@${tag[1]} tag`);\n\n inCodeTag = CODE_TAGS.has(tag[1]);\n\n continue;\n }\n\n const marker = LIST_MARKER.exec(text);\n\n if (marker) {\n start('list', line, marker[0]);\n\n continue;\n }\n\n // like Markdown, an unindented line right after a list item lazily continues it\n if (current && (current.kind === 'prose' || current.kind === 'tag' || current.kind === 'list')) {\n current.lines.push(line);\n\n continue;\n }\n\n start('prose', line);\n }\n\n flush();\n\n return paragraphs;\n}\n\nfunction startsLine(token: string, next: string | undefined): boolean {\n return startsBlock(token) || LINE_DIRECTIVE.test(next === undefined ? token : `${token} ${next}`);\n}\n\nfunction visualLength(text: string, tabWidth: number): number {\n return text.replaceAll('\\t', ' '.repeat(tabWidth)).length;\n}\n\nfunction sourceLineLength(line: BodyLine, tabWidth: number): number {\n // the text holds the indentation for comments without a prefix, like `other` block comments\n return visualLength(`${line.prefix}${line.text.trimEnd()}`, tabWidth);\n}\n\n/**\n * In legacy width/tolerance mode, an indivisible token is exempt because wrapping cannot\n * make it fit.\n */\nfunction overflowIssues(lines: readonly BodyLine[], options: AnalyzeOptions): IssueReport[] {\n const issues: IssueReport[] = [];\n const limit = options.maxWidth ?? options.width + options.tolerance;\n\n for (const line of lines) {\n const length = sourceLineLength(line, options.tabWidth);\n\n if (length <= limit) {\n continue;\n }\n\n if (options.maxWidth !== undefined) {\n issues.push(lineIssue('overflow', line, options));\n\n continue;\n }\n\n const prefixLength = visualLength(`${line.prefix}${/^\\s*/.exec(line.text)?.[0] ?? ''}`, options.tabWidth);\n const tokens = bindLineStarts(tokenize(line.text), startsLine);\n\n if (tokens.length > 1 && tokens.every((token) => prefixLength + token.length <= limit)) {\n issues.push(lineIssue('overflow', line, options));\n }\n }\n\n return issues;\n}\n\nfunction lineIssue(type: IssueType, line: BodyLine, options: AnalyzeOptions): IssueReport {\n return { type, tier: ISSUE_TIERS[type], line: line.index + 1, length: sourceLineLength(line, options.tabWidth) };\n}\n\nfunction stripBlockSyntax(text: string): string {\n return text\n .replace(/^\\s*\\/\\*+/, '')\n .replace(/\\*+\\/\\s*$/, '')\n .replace(/^\\s*\\*(?=\\s|$)/, '');\n}\n\nfunction bannerIssues(paragraph: RawParagraph, options: AnalyzeOptions): IssueReport[] {\n const verbatim = ['code block', 'indented', 'legal comment'];\n\n if (!options.style || (paragraph.reason !== undefined && verbatim.includes(paragraph.reason))) {\n return [];\n }\n\n const banners = paragraph.lines.filter((line) => {\n const text = paragraph.kind === 'other' ? stripBlockSyntax(line.text) : line.text;\n\n return isBanner(text) || (paragraph.kind === 'other' && /^\\s*\\/\\*{3,}|\\*{3,}\\/\\s*$/.test(line.text));\n });\n\n return banners.map((line) => lineIssue('banner', line, options));\n}\n\nfunction styleIssues(paragraph: RawParagraph, options: AnalyzeOptions): IssueReport[] {\n if (!options.style) {\n return [];\n }\n\n const byIndex = new Map(paragraph.lines.map((line) => [line.index, line]));\n\n const lines = paragraph.lines.map((line) => {\n return { index: line.index, text: paragraph.kind === 'other' ? stripBlockSyntax(line.text) : line.text };\n });\n\n return findStyleIssues(lines).flatMap((issue) => {\n const line = byIndex.get(issue.index);\n\n return line ? [lineIssue(issue.type, line, options)] : [];\n });\n}\n\nfunction bindTagHead(tokens: readonly string[]): string[] {\n let end = 1;\n\n if (tokens[end]?.startsWith('{')) {\n end += 1;\n }\n\n if (NAMED_TAGS.has(tokens[0].slice(1)) && end < tokens.length) {\n end += 1;\n }\n\n if (tokens[end] === '-') {\n end += 1;\n }\n\n return [tokens.slice(0, end).join(' '), ...tokens.slice(end)];\n}\n\n/**\n * A paragraph without a structure, like a single-line block comment, is prose that just can't be\n * rewrapped, so it's checked like text laid out by hand.\n */\nfunction keptIssues(paragraph: RawParagraph, structure: Structure | undefined, options: AnalyzeOptions): IssueReport[] {\n const tier = structure?.tier ?? 'manual';\n\n return byLine([\n ...(tier === 'manual' || tier === 'code-like' ? overflowIssues(paragraph.lines, options) : []),\n ...(tier === 'code' || tier === 'code-like' ? [] : styleIssues(paragraph, options)),\n ...bannerIssues(paragraph, options),\n ]);\n}\n\nfunction byLine(issues: readonly IssueReport[]): IssueReport[] {\n const unique = new Map(issues.map((issue) => [`${issue.line}:${issue.type}`, issue]));\n\n return [...unique.values()].toSorted((a, b) => a.line - b.line);\n}\n\nfunction sameLayout(a: readonly (readonly string[])[], b: readonly (readonly string[])[]): boolean {\n return a.length === b.length && a.every((words, position) => words.join(' ') === b[position].join(' '));\n}\n\n/**\n * Returns the index of the first line that ends inside a token, like a link or a code span that\n * continues on the next line, or -1 when every line ends between two tokens. The lines hold the\n * text of the tokens.\n */\nfunction splitTokenLine(lines: readonly (readonly string[])[], tokens: readonly string[]): number {\n let next = 0;\n\n return lines.findIndex((words) => {\n const line = words.join(' ');\n\n let text = '';\n\n while (text.length < line.length && next < tokens.length) {\n text += `${text ? ' ' : ''}${tokens[next]}`;\n next += 1;\n }\n\n return text !== line;\n });\n}\n\nfunction analyzeParagraph(paragraph: RawParagraph, options: AnalyzeOptions, language: Language): ParagraphReport {\n const first = paragraph.lines[0];\n const last = paragraph.lines.at(-1) ?? first;\n const base = { kind: paragraph.kind, startLine: first.index + 1, endLine: last.index + 1 };\n\n if (paragraph.kind === 'code') {\n return {\n ...base,\n reflowable: false,\n reason: paragraph.reason,\n issues: bannerIssues(paragraph, options),\n suggestion: null,\n };\n }\n\n if (paragraph.kind === 'tag' && paragraph.reason) {\n const codeTag = CODE_TAGS.has(JSDOC_TAG.exec(first.text)?.[1] ?? '');\n const issues = codeTag ? [] : overflowIssues(paragraph.lines, options);\n\n return { ...base, reflowable: false, reason: paragraph.reason, issues, suggestion: null };\n }\n\n if (paragraph.kind === 'other') {\n const fence = new FenceTracker();\n\n const prose = {\n ...paragraph,\n lines: paragraph.lines.filter((line) => !fence.next(stripBlockSyntax(line.text))),\n };\n\n const structure = structureOf(\n prose.lines.map((line) => {\n return { ...line, text: stripBlockSyntax(line.text) };\n }),\n language,\n );\n\n const issues = keptIssues(prose, structure, options);\n\n return { ...base, reflowable: false, reason: paragraph.reason, issues, suggestion: null };\n }\n\n const keep = (structure: Structure): ParagraphReport => {\n const issues = keptIssues(paragraph, structure, options);\n\n return { ...base, reflowable: false, reason: structure.reason, issues, suggestion: null };\n };\n\n const structure = structureOf(paragraph.lines, language);\n\n if (structure) {\n return keep(structure);\n }\n\n const prefix = first.prefix;\n const prefixLength = visualLength(prefix, options.tabWidth);\n const hang = ' '.repeat(paragraph.marker.length);\n const firstLimit = options.width - prefixLength - paragraph.marker.length;\n const restLimit = options.width - prefixLength - hang.length;\n\n const currentLines = paragraph.lines.map((line, position) =>\n bindLineStarts(tokenize(position === 0 ? line.text.slice(paragraph.marker.length) : line.text), startsLine),\n );\n\n const tokens = tokenize(currentLines.flat().join(' '));\n\n const layoutOptions = {\n ...options,\n tolerance:\n options.maxWidth === undefined\n ? options.tolerance\n : Math.min(options.tolerance, options.maxWidth - options.width),\n parentheticalOverflow: options.maxWidth === undefined ? undefined : options.maxWidth - options.width,\n keepFirstLine: paragraph.kind === 'tag',\n };\n\n const layOut = (from: readonly string[], settings: LayoutOptions, extra: number) => {\n const even = layoutTokens(from, firstLimit + extra, restLimit + extra, settings);\n\n return { even, wrapped: breakAfterSentence(even, firstLimit + extra, restLimit + extra, settings) };\n };\n\n const units = bindLineStarts(paragraph.kind === 'tag' ? bindTagHead(tokens) : tokens, startsLine);\n\n // the first and the other lines have the same limit, since the hanging indent is as long as the marker\n const hardLimit = restLimit + (options.maxWidth === undefined ? options.tolerance : options.maxWidth - options.width);\n const written = currentLines.map((words) => ` ${words.join(' ')} `);\n\n /**\n * Text that stays on one line, like a code span or a link, may only be longer than the limit when\n * it already is on one line, so a rewrap never adds a line that is too long.\n */\n const staysInLimit = (unit: string): boolean => {\n return unit.length <= hardLimit || written.some((line) => line.includes(` ${unit} `));\n };\n\n // a code span, link, or `{...}` group that continues on the next line would be joined into one line that is too long\n if (!units.every((unit) => staysInLimit(unit))) {\n return keep({ tier: 'manual', reason: 'unbreakable text across lines' });\n }\n\n const text = currentLines.map((words) => words.join(' ')).join('\\n');\n\n if (!language.joinsCodeSpans && [...text.matchAll(CODE_SPAN)].some((span) => span[0].includes('\\n'))) {\n return keep({ tier: 'manual', reason: 'code span across lines' });\n }\n\n const bodyOf = (lines: readonly (readonly string[])[]): BodyLine[] => {\n return lines.map((words, position) => {\n const text = `${position === 0 ? paragraph.marker : hang}${words.join(' ')}`;\n\n return { ...first, text, verbatim: false, hardBreak: HARD_BREAK.test(text) };\n });\n };\n\n /**\n * Analyzing the rewrap must find the same prose, so when a new line's start or end would read as\n * structure, like two lines that start with `term - `, the first word of the line that tips it\n * moves up a line. Without such a layout, or when the text reads as structure on a single line,\n * no rewrap reads as the same prose, and the paragraph is kept as written. The lines get `extra`\n * columns more than the width allows.\n */\n const settle = (settings: LayoutOptions, extra = 0): { even: string[][]; wrapped: string[][] } | Structure => {\n let current = units;\n let { even, wrapped } = layOut(current, settings, extra);\n\n for (;;) {\n const body = bodyOf(wrapped);\n const rewrap = structureOf(body, language);\n\n if (!rewrap) {\n return { even, wrapped };\n }\n\n // the first line has no line to move a word up to, so the first word of the second line joins it instead\n const tipping = body.findIndex((_, position) => structureOf(body.slice(0, position + 1), language) !== undefined);\n const start = wrapped.slice(0, Math.max(tipping, 1)).flat().length;\n const pair = `${current[start - 1]} ${current[start]}`;\n\n if (body.length < 2 || !staysInLimit(pair)) {\n const reading = rewrap.tier === 'code-like' ? 'code' : rewrap.reason;\n\n return { tier: 'manual', reason: `rewrap would read as ${reading}` };\n }\n\n current = current.toSpliced(start - 1, 2, pair);\n ({ even, wrapped } = layOut(current, settings, extra));\n }\n };\n\n /**\n * Lays the paragraph out for a width that is `extra` columns wider. The columns come out of the\n * tolerance and out of the room that parentheticals have, so the hard limit stays where it is.\n */\n const layoutAt = (extra: number): { even: string[][]; wrapped: string[][] } | Structure => {\n const settings = {\n ...layoutOptions,\n tolerance: layoutOptions.tolerance - extra,\n parentheticalOverflow:\n layoutOptions.parentheticalOverflow === undefined ? undefined : layoutOptions.parentheticalOverflow - extra,\n };\n\n const settled = settle(settings, extra);\n\n // the extra lines that keep a line from sticking out can read as structure where the fewest lines don't\n return 'wrapped' in settled ? settled : settle({ ...settings, keepSteps: true }, extra);\n };\n\n const layout = layoutAt(0);\n\n if (!('wrapped' in layout)) {\n return keep(layout);\n }\n\n const { even, wrapped } = layout;\n const matchesLayout = sameLayout(wrapped, currentLines);\n const isVerdict = (issue: LineIssue): boolean => ISSUE_TIERS[issue.type] === 'verdict';\n\n const lineIssues: LineIssue[] = [];\n const overflow: IssueReport[] = options.maxWidth === undefined ? [] : overflowIssues(paragraph.lines, options);\n\n if (!matchesLayout) {\n const hasExtraLines = currentLines.length > wrapped.length;\n\n if (options.maxWidth === undefined) {\n overflow.push(...overflowIssues(paragraph.lines, options));\n }\n\n lineIssues.push(\n ...findLineIssues(currentLines, firstLimit, restLimit, layoutOptions).filter(\n (issue) => issue.type !== 'overflow' && (issue.type !== 'underfill' || hasExtraLines),\n ),\n );\n\n if (hasWidow(currentLines) && !hasWidow(wrapped)) {\n lineIssues.push({ type: 'widow', line: currentLines.length - 1 });\n }\n\n /**\n * The agent may keep a word that a possible orphan is about where it is, so a layout that only\n * differs from the suggestion by those words is no verdict. Anything else about it still is.\n */\n const keepsOnlyOrphans = (): boolean => {\n const free = settle({ ...layoutOptions, avoidSentenceOrphans: false });\n\n return !('wrapped' in free) || sameLayout(free.wrapped, currentLines);\n };\n\n const keepsOnlySentenceStarts = (): boolean => {\n const free = settle({ ...layoutOptions, ignoreSentenceStarts: true });\n\n return 'wrapped' in free && (sameLayout(free.even, currentLines) || sameLayout(free.wrapped, currentLines));\n };\n\n /**\n * Whether a parenthetical stays on one line is for the agent to decide, so a layout that\n * matches the suggestion up to a line that ends inside a parenthetical that the suggestion\n * keeps whole, and the rewrap of the rest after it, is no verdict.\n */\n const splitsOnlyParenthetical = (line: number): boolean => {\n if (layoutOptions.parentheticalOverflow === undefined) {\n return false;\n }\n\n const tokenCount = (lines: readonly (readonly string[])[]) => tokenize(lines.flat().join(' ')).length;\n const end = tokenCount(currentLines.slice(0, line + 1));\n const group = parentheticalAround(tokens, end, restLimit + layoutOptions.parentheticalOverflow);\n\n if (!group) {\n return false;\n }\n\n const keepsGroup = wrapped.every((_, position) => {\n const breakAt = tokenCount(wrapped.slice(0, position + 1));\n\n return breakAt <= group.start || breakAt >= group.end;\n });\n\n if (!keepsGroup) {\n return false;\n }\n\n const restOptions = { ...layoutOptions, keepFirstLine: false };\n const rest = bindLineStarts(tokens.slice(end), startsLine);\n const restLines = layoutTokens(rest, restLimit, restLimit, restOptions);\n\n return sameLayout(breakAfterSentence(restLines, restLimit, restLimit, restOptions), currentLines.slice(line + 1));\n };\n\n if (overflow.length === 0 && (currentLines.length === wrapped.length || options.maxWidth !== undefined)) {\n const line = Math.max(\n wrapped.findIndex((words, position) => words.join(' ') !== currentLines[position]?.join(' ')),\n 0,\n );\n\n /**\n * Lines that only miss the break after a sentence, or end with the first words of one, are a\n * hint, since a heuristic decides where a sentence ends.\n */\n if (lineIssues.length === 0 && (sameLayout(even, currentLines) || keepsOnlySentenceStarts())) {\n lineIssues.push({ type: 'orphan', line });\n } else if (lineIssues.length === 0) {\n lineIssues.push({ type: splitsOnlyParenthetical(line) ? 'split-parenthetical' : 'unbalanced', line });\n } else if (\n lineIssues.every((issue) => issue.type === 'orphan') &&\n !sameLayout(even, currentLines) &&\n !keepsOnlyOrphans()\n ) {\n lineIssues.push({ type: 'unbalanced', line });\n }\n }\n\n /**\n * The rewrap keeps a link, a code span, and a `{...}` group on one line. A line that ends\n * inside one is a verdict whatever else holds for the paragraph, so the rewrap always joins it.\n */\n const splitLine = splitTokenLine(currentLines, tokens);\n\n if (splitLine !== -1 && overflow.length === 0 && !lineIssues.some((issue) => isVerdict(issue))) {\n lineIssues.push({ type: 'unbalanced', line: splitLine });\n }\n }\n\n /**\n * The tolerance is room past the width, so a paragraph that already has the layout for a width\n * within the tolerance is fine as it is.\n */\n const matchesWiderLayout = (): boolean => {\n for (let extra = 1; extra <= layoutOptions.tolerance; extra += 1) {\n const wider = layoutAt(extra);\n\n if ('wrapped' in wider && sameLayout(wider.wrapped, currentLines)) {\n return true;\n }\n }\n\n return false;\n };\n\n /**\n * A paragraph that only has hints keeps them and its suggestion, since a hint is for the agent to\n * decide. Only verdicts go, and the suggestion then keeps the lines as they are.\n */\n const accepted = lineIssues.some((issue) => isVerdict(issue)) && matchesWiderLayout();\n const suggested = accepted ? currentLines : wrapped;\n\n const suggestion = suggested.map(\n (words, position) => `${prefix}${position === 0 ? paragraph.marker : hang}${words.join(' ')}`,\n );\n\n const wrapIssues = lineIssues\n .filter((issue) => !accepted || !isVerdict(issue))\n .map((issue) => lineIssue(issue.type, paragraph.lines[issue.line], options));\n\n const issues = byLine([...overflow, ...wrapIssues, ...styleIssues(paragraph, options)]);\n\n return { ...base, reflowable: true, issues, suggestion };\n}\n\nconst LINE_COMMENT = 'line comment';\n\n/**\n * Rewrapping a line comment would make a run of `//` lines, so the tool keeps it as written and\n * only reports the lines that are longer than the maximum. Code and layout syntax are exempt,\n * like in other text kept as written.\n */\nfunction analyzeLineParagraph(paragraph: RawParagraph, options: AnalyzeOptions, language: Language): ParagraphReport {\n const first = paragraph.lines[0];\n const last = paragraph.lines.at(-1) ?? first;\n const base = { kind: paragraph.kind, startLine: first.index + 1, endLine: last.index + 1 };\n\n if (paragraph.kind === 'code') {\n return { ...base, reflowable: false, reason: paragraph.reason, issues: [], suggestion: null };\n }\n\n const tier = structureOf(paragraph.lines, language)?.tier;\n const issues = tier === 'code' || tier === 'layout' ? [] : overflowIssues(paragraph.lines, options);\n\n return { ...base, reflowable: false, reason: LINE_COMMENT, issues, suggestion: null };\n}\n\nfunction computeLineStarts(source: string): number[] {\n const starts = [0];\n\n for (let index = 0; index < source.length; index += 1) {\n if (source.codePointAt(index) === 0x0a) {\n starts.push(index + 1);\n }\n }\n\n return starts;\n}\n\nfunction lineOf(lineStarts: readonly number[], offset: number): number {\n let low = 0;\n let high = lineStarts.length - 1;\n\n while (low < high) {\n const middle = Math.ceil((low + high) / 2);\n\n if (lineStarts[middle] <= offset) {\n low = middle;\n } else {\n high = middle - 1;\n }\n }\n\n return low;\n}\n\ninterface CommentShape {\n readonly kind: CommentKind;\n readonly startLine: number;\n readonly endLine: number;\n readonly paragraphs: RawParagraph[];\n}\n\nfunction stripCarriageReturn(line: string): string {\n return line.endsWith('\\r') ? line.slice(0, -1) : line;\n}\n\nfunction blockKind(comment: ParsedComment, language: Language): CommentKind {\n if (language.name === 'rust') {\n return comment.doc ? `${comment.doc}-block-doc` : 'block';\n }\n\n return comment.value.startsWith('*') ? 'jsdoc' : 'block';\n}\n\nfunction wholeComment(\n shape: Omit<CommentShape, 'paragraphs'>,\n lines: readonly string[],\n paragraphKind: ParagraphKind,\n reason: string,\n): CommentShape {\n const otherLines = lines.map((text, offset) => {\n return { index: shape.startLine + offset, prefix: '', text, verbatim: false, hardBreak: false };\n });\n\n return { ...shape, paragraphs: [{ kind: paragraphKind, marker: '', reason, lines: otherLines }] };\n}\n\n// a code span only names a marker, like the `@license` in a comment that explains legal comments\nfunction isLegal(value: string, language: Language): boolean {\n return language.legal.test(replaceKeepingLines(value, CODE_SPAN, '``'));\n}\n\nfunction shapeBlock(\n comment: ParsedComment,\n sourceLines: readonly string[],\n lineStarts: readonly number[],\n language: Language,\n): CommentShape {\n const startLine = lineOf(lineStarts, comment.start);\n const endLine = lineOf(lineStarts, comment.end - 1);\n const shape = { kind: blockKind(comment, language), startLine, endLine };\n const lines = sourceLines.slice(startLine, endLine + 1);\n\n // the `!` of a Rust inner doc comment marks it like the second `*` of a JSDoc comment\n const value = comment.doc ? comment.value.slice(1) : comment.value.replace(/^\\*/, '');\n\n if (isLegal(comment.value, language)) {\n return wholeComment(shape, lines, 'code', 'legal comment');\n }\n\n if (LINE_DIRECTIVE.test(value) || language.blockDirective?.test(value)) {\n return wholeComment(shape, lines, 'code', 'directive');\n }\n\n const other = (): CommentShape => {\n return wholeComment(\n shape,\n lines,\n 'other',\n startLine === endLine ? 'single-line block comment' : 'unsupported block comment layout',\n );\n };\n\n if (startLine === endLine || !/^\\s*\\/\\*[*!]?$/.test(lines[0]) || !/^\\s*\\*\\/$/.test(lines.at(-1) ?? '')) {\n return other();\n }\n\n const body: BodyLine[] = [];\n const inner = lines.slice(1, -1);\n\n for (const [offset, line] of inner.entries()) {\n const match = /^(\\s*\\*)( ?)(.*)$/.exec(line);\n\n if (!match) {\n return other();\n }\n\n const [, star, space, text] = match;\n\n body.push({\n index: startLine + 1 + offset,\n prefix: `${star} `,\n text: text.trimEnd(),\n verbatim: (!space && text.length > 0) || LINE_DIRECTIVE.test(text),\n hardBreak: HARD_BREAK.test(text),\n });\n }\n\n return { ...shape, paragraphs: segment(body, language) };\n}\n\n/**\n * Shapes a run of Rust line doc comments, like `///` or `//!` on consecutive lines, as one comment.\n * Each line holds a prefix like the ` * ` of a block comment.\n */\nfunction shapeLineRun(\n group: readonly ParsedComment[],\n sourceLines: readonly string[],\n lineStarts: readonly number[],\n language: Language,\n): CommentShape {\n const startLine = lineOf(lineStarts, group[0].start);\n const endLine = startLine + group.length - 1;\n const kind: CommentKind = group[0].doc === 'inner' ? 'inner-line-doc' : 'outer-line-doc';\n const shape = { kind, startLine, endLine };\n const lines = sourceLines.slice(startLine, endLine + 1);\n const values = group.map((comment) => comment.value.slice(1));\n\n if (isLegal(values.join('\\n'), language)) {\n return wholeComment(shape, lines, 'code', 'legal comment');\n }\n\n if (LINE_DIRECTIVE.test(values[0])) {\n return wholeComment(shape, lines, 'code', 'directive');\n }\n\n const body = lines.map((line, offset): BodyLine => {\n const [, marker, space, text] = /^(\\s*\\/\\/[/!])( ?)(.*)$/.exec(line) ?? ['', '', '', line];\n\n return {\n index: startLine + offset,\n prefix: `${marker} `,\n text: text.trimEnd(),\n verbatim: (!space && text.length > 0) || LINE_DIRECTIVE.test(text),\n hardBreak: HARD_BREAK.test(text),\n };\n });\n\n return { ...shape, paragraphs: segment(body, language) };\n}\n\n/**\n * Shapes a run of plain line comments (`//`) on consecutive lines as one comment. Each line is\n * measured with its own prefix, like `//` without a space or the `///` of a TypeScript directive.\n * Only the lines with a tool directive are directives, since a directive in a line comment covers\n * its own line.\n */\nfunction shapeLineComment(\n group: readonly ParsedComment[],\n sourceLines: readonly string[],\n lineStarts: readonly number[],\n language: Language,\n): CommentShape {\n const startLine = lineOf(lineStarts, group[0].start);\n const endLine = startLine + group.length - 1;\n const shape = { kind: 'line' as const, startLine, endLine };\n const lines = sourceLines.slice(startLine, endLine + 1);\n\n if (isLegal(group.map((comment) => comment.value).join('\\n'), language)) {\n return wholeComment(shape, lines, 'code', 'legal comment');\n }\n\n const body = lines.map((line, offset): BodyLine => {\n const [, marker, space, text] = /^(\\s*\\/\\/[/!]*)( ?)(.*)$/.exec(line) ?? ['', '', '', line];\n\n return {\n index: startLine + offset,\n prefix: `${marker}${space}`,\n text: text.trimEnd(),\n verbatim: LINE_DIRECTIVE.test(text),\n hardBreak: HARD_BREAK.test(text),\n };\n });\n\n // line comments have no JSDoc tags, so a line that starts with `@` is prose\n return { ...shape, paragraphs: segment(body, { ...language, tags: false }) };\n}\n\nfunction ownsLines(comment: ParsedComment, source: string, lineStarts: readonly number[]): boolean {\n const startLine = lineOf(lineStarts, comment.start);\n const before = source.slice(lineStarts[startLine], comment.start);\n const endLine = lineOf(lineStarts, comment.end - 1);\n const lineEnd = lineStarts[endLine + 1] ?? source.length;\n const after = source.slice(comment.end, lineEnd);\n\n return /^[\\t ]*$/.test(before) && /^[\\t ]*\\r?\\n?$/.test(after);\n}\n\n/**\n * Only a single-line block comment or prose without structure can stay on one line past the width.\n */\nfunction singleLineLength(paragraph: RawParagraph, options: AnalyzeOptions, language: Language): number {\n const [first] = paragraph.lines;\n\n if (paragraph.kind === 'other' && paragraph.reason === 'single-line block comment') {\n return sourceLineLength(first, options.tabWidth);\n }\n\n if (paragraph.kind !== 'prose' || structureOf(paragraph.lines, language)) {\n return Infinity;\n }\n\n const text = tokenize(paragraph.lines.map((line) => line.text).join(' ')).join(' ');\n\n return visualLength(first.prefix, options.tabWidth) + text.length;\n}\n\nexport const BYTE_ORDER_MARK = '\\uFEFF';\n\n/**\n * Most toolchains accept JSX in a `.js` file, and a file without JSX parses the same either way.\n */\nfunction parseSource(filename: string, source: string): ReturnType<typeof parseSync> {\n return parseSync(filename, source, filename.endsWith('.js') ? { lang: 'jsx' } : undefined);\n}\n\nfunction describeFailure(errors: readonly OxcError[], lineStarts: readonly number[]): SyntaxFailure {\n const [first] = errors;\n const offset = first.labels.at(0)?.start;\n\n return { message: first.message, line: offset === undefined ? undefined : lineOf(lineStarts, offset) + 1 };\n}\n\ninterface ScannedSource {\n readonly comments: readonly ParsedComment[];\n readonly syntaxError?: SyntaxFailure;\n}\n\nfunction scanJavaScript(filename: string, text: string, lineStarts: readonly number[]): ScannedSource {\n const { program, comments, errors } = parseSource(filename, text);\n\n /**\n * The parser returns an empty program when it stops at a syntax error, and it has only seen\n * the comments before that error. An error that it recovers from, like a `return` outside of\n * a function, leaves the program and its comments complete.\n */\n if (errors.length > 0 && program.body.length === 0) {\n return { comments: [], syntaxError: describeFailure(errors, lineStarts) };\n }\n\n // drop a leading hashbang, which some parser versions report as a comment\n return {\n comments: (comments as ParsedComment[]).filter((comment) => !(comment.start === 0 && text.startsWith('#!'))),\n };\n}\n\n/**\n * Rust is read by rustc's lexer, which never stops early. A token that never ends, like an\n * unterminated block comment or string, holds every comment after it, so such a source has\n * no comments, like a JavaScript source that the parser stops at.\n */\nfunction scanRustSource(text: string, lineStarts: readonly number[]): ScannedSource {\n const { comments, error } = scanRust(text);\n\n if (error) {\n return { comments: [], syntaxError: { message: error.message, line: lineOf(lineStarts, error.offset) + 1 } };\n }\n\n return {\n comments: comments.map((comment): ParsedComment => {\n return {\n type: comment.block ? 'Block' : 'Line',\n value: text.slice(comment.start + 2, comment.block ? comment.end - 2 : comment.end),\n start: comment.start,\n end: comment.end,\n doc: comment.doc,\n };\n }),\n };\n}\n\n/**\n * Groups the line comments that own their lines into runs of consecutive lines with the same\n * marker and indentation, since each run reads as one comment. `markerOf` returns `undefined`\n * for a comment that belongs to no run, which ends the run before it.\n */\nfunction groupLineRuns(\n comments: readonly ParsedComment[],\n text: string,\n lineStarts: readonly number[],\n markerOf: (comment: ParsedComment) => string | undefined,\n): ParsedComment[][] {\n const groups: ParsedComment[][] = [];\n\n let current: ParsedComment[] | undefined;\n let previousLine = -1;\n let indent = '';\n let marker: string | undefined;\n\n for (const comment of comments) {\n const next = comment.type === 'Line' ? markerOf(comment) : undefined;\n\n if (next === undefined || !ownsLines(comment, text, lineStarts)) {\n current = undefined;\n\n continue;\n }\n\n const line = lineOf(lineStarts, comment.start);\n const before = text.slice(lineStarts[line], comment.start);\n\n if (current && line === previousLine + 1 && next === marker && before === indent) {\n current.push(comment);\n } else {\n current = [comment];\n indent = before;\n marker = next;\n\n groups.push(current);\n }\n\n previousLine = line;\n }\n\n return groups;\n}\n\n/**\n * Finds all own-line block and JSDoc comments in a JavaScript or TypeScript source file, or block\n * and doc comments in a Rust source file (`.rs`), and reports how each paragraph deviates from a\n * greedy wrap at `options.width`. A run of Rust line doc comments (`///` or `//!`) counts as one\n * comment. A run of other line comments (`//`) on their own lines is never rewrapped, and it is\n * only reported when a line is longer than the maximum. A source with a syntax error that stops\n * the parser has no reports.\n */\nexport function analyzeSource(source: string, filename: string, options: AnalyzeOptions): CommentReport[] {\n return inspectSource(source, filename, options).comments;\n}\n\n/**\n * Analyzes a source like `analyzeSource`, and also tells a source without comments apart from one\n * that the parser can't read.\n */\nexport function inspectSource(source: string, filename: string, options: AnalyzeOptions): SourceAnalysis {\n if (options.maxWidth !== undefined && (!Number.isInteger(options.maxWidth) || options.maxWidth < options.width)) {\n throw new Error('comments: maxWidth must be an integer at least as large as width');\n }\n\n // a byte order mark is no text, so a comment on the first line owns its lines and is measured without the mark\n const text = source.startsWith(BYTE_ORDER_MARK) ? source.slice(1) : source;\n const language = languageOf(filename);\n const lineStarts = computeLineStarts(text);\n\n const scanned =\n language.name === 'rust' ? scanRustSource(text, lineStarts) : scanJavaScript(filename, text, lineStarts);\n\n if (scanned.syntaxError) {\n return { comments: [], syntaxError: scanned.syntaxError };\n }\n\n const sourceLines = text.split('\\n').map((line) => stripCarriageReturn(line));\n const reports: CommentReport[] = [];\n\n const report = (shape: CommentShape, lines: readonly string[], paragraphs: ParagraphReport[]): CommentReport => {\n return {\n file: filename,\n kind: shape.kind,\n startLine: shape.startLine + 1,\n endLine: shape.endLine + 1,\n indent: /^[\\t ]*/.exec(lines[0])?.[0] ?? '',\n width: options.width,\n maxWidth: options.maxWidth,\n lengths: lines.map((line) => visualLength(line.trimEnd(), options.tabWidth)),\n text: lines.join('\\n'),\n paragraphs,\n };\n };\n\n const blocks = scanned.comments.filter((comment) => {\n return (\n comment.type === 'Block' && ownsLines(comment, text, lineStarts) && !comment.value.includes(IGNORE_DIRECTIVE)\n );\n });\n\n const runs = (markerOf: (comment: ParsedComment) => string | undefined): ParsedComment[][] => {\n return groupLineRuns(scanned.comments, text, lineStarts, markerOf).filter((group) => {\n return !group.some((comment) => comment.value.includes(IGNORE_DIRECTIVE));\n });\n };\n\n const shapes = [\n ...blocks.map((block) => shapeBlock(block, sourceLines, lineStarts, language)),\n ...runs((comment) => comment.doc).map((group) => shapeLineRun(group, sourceLines, lineStarts, language)),\n ...runs((comment) => (comment.doc ? undefined : '//')).map((group) =>\n shapeLineComment(group, sourceLines, lineStarts, language),\n ),\n ].toSorted((a, b) => a.startLine - b.startLine);\n\n for (const shape of shapes) {\n const lines = sourceLines.slice(shape.startLine, shape.endLine + 1);\n\n if (shape.kind === 'line') {\n const paragraphs = shape.paragraphs.map((paragraph) => analyzeLineParagraph(paragraph, options, language));\n\n // a line comment only has overflow to report, so one that fits is no comment of interest\n if (paragraphs.some((paragraph) => paragraph.issues.length > 0)) {\n reports.push(report(shape, lines, paragraphs));\n }\n\n continue;\n }\n\n const singleLineWidth = Math.max(\n options.width,\n Math.min(options.singleLineWidth ?? options.width, options.maxWidth ?? Infinity),\n );\n\n // staying on one line always saves a line, so the tolerance applies on top of the single-line width\n const singleLineLimit = Math.min(singleLineWidth + options.tolerance, options.maxWidth ?? Infinity);\n\n const singleLine =\n shape.paragraphs.length === 1 ? singleLineLength(shape.paragraphs[0], options, language) : Infinity;\n\n const paragraphOptions =\n singleLine <= singleLineLimit ? { ...options, width: Math.max(singleLineWidth, singleLine) } : options;\n\n reports.push(\n report(\n shape,\n lines,\n shape.paragraphs.map((paragraph) => analyzeParagraph(paragraph, paragraphOptions, language)),\n ),\n );\n }\n\n return { comments: reports };\n}\n\n/**\n * Whether the comment has any issue, including hints, which makes it worth handing to the agent.\n */\nexport function isSuspicious(report: CommentReport): boolean {\n return report.paragraphs.some((paragraph) => paragraph.issues.length > 0);\n}\n\n/**\n * Whether the comment has an issue that isn't a hint, so it breaks the rules no matter what\n * the agent decided.\n */\nexport function hasVerdictIssues(report: CommentReport): boolean {\n return report.paragraphs.some((paragraph) => paragraph.issues.some((issue) => issue.tier === 'verdict'));\n}\n\n/**\n * Returns the comment's source text with every prose paragraph replaced by its suggestion.\n * Code, tables, diagrams, and similar paragraphs are kept as they are.\n */\nexport function rewrapComment(report: CommentReport): string {\n const lines = report.text.split('\\n');\n\n const paragraphs = report.paragraphs\n .filter((paragraph) => paragraph.suggestion !== null)\n .toSorted((a, b) => b.startLine - a.startLine);\n\n for (const paragraph of paragraphs) {\n const start = paragraph.startLine - report.startLine;\n\n lines.splice(start, paragraph.endLine - paragraph.startLine + 1, ...(paragraph.suggestion ?? []));\n }\n\n return lines.join('\\n');\n}\n\nconst POSITION_KEYS = new Set(['start', 'end', 'range', 'loc']);\n\n/**\n * Returns the syntax tree of the source as JSON, without positions or comments, used to verify that\n * only comments changed. Moving or rewording a comment leaves the tree the same, while any change to\n * a string, template, regex, JSX text, or where a statement ends changes it. A source that doesn't\n * parse has no tree to compare, so all of its text counts. A Rust source is compared by its tokens\n * instead, with each run of doc comments as one marker (see `rustFingerprint`).\n */\nexport function codeFingerprint(source: string, filename: string): string {\n if (languageOf(filename).name === 'rust') {\n return rustFingerprint(source);\n }\n\n const { program, errors } = parseSource(filename, source);\n\n if (errors.length > 0) {\n return JSON.stringify({ source, errors: errors.map((error) => error.message) });\n }\n\n // a BigInt literal's value is a `bigint`, which JSON can't hold, but its `raw` text already tells it apart\n return JSON.stringify(program, (key, value: unknown) =>\n POSITION_KEYS.has(key) || typeof value === 'bigint' ? undefined : value,\n );\n}\n","import { existsSync } from 'node:fs';\nimport { createRequire } from 'node:module';\nimport path from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nexport interface RustComment {\n /**\n * Holds the UTF-16 offset of the first character of the comment, like the `/` of `///`.\n */\n readonly start: number;\n\n /**\n * Holds the UTF-16 offset after the last character. A line comment ends before its line break.\n */\n readonly end: number;\n readonly block: boolean;\n readonly doc?: 'outer' | 'inner';\n}\n\nexport interface RustScan {\n readonly comments: readonly RustComment[];\n\n /**\n * Describes the first token that never ends, like an unterminated block comment or string.\n */\n readonly error?: { readonly message: string; readonly offset: number };\n}\n\ninterface NativeBinding {\n scanRust(source: string): RustScan;\n rustFingerprint(source: string): string;\n isRustCode(text: string): boolean;\n}\n\nconst BINARY_NAME = 'comments';\n\n/**\n * Lists the platform suffixes that `napi build --platform` gives the binary, in the order to try\n * them. A Linux binary is built for either glibc or musl, so both are candidates.\n */\nfunction platformSuffixes(): string[] {\n const { platform, arch } = process;\n\n switch (platform) {\n case 'darwin': {\n return [`darwin-${arch}`, 'darwin-universal'];\n }\n\n case 'linux': {\n return arch === 'arm'\n ? ['linux-arm-gnueabihf', 'linux-arm-musleabihf']\n : [`linux-${arch}-gnu`, `linux-${arch}-musl`];\n }\n\n case 'win32': {\n return [`win32-${arch}-msvc`, `win32-${arch}-gnu`];\n }\n\n default: {\n return [`${platform}-${arch}`];\n }\n }\n}\n\n/**\n * The module runs from `src/comments`, `dist`, or `dist/bin`, so the `native` directory is looked\n * up in the directories above it.\n */\nfunction findBinaries(): string[] {\n const names = platformSuffixes().map((suffix) => `${BINARY_NAME}.${suffix}.node`);\n\n for (let directory = path.dirname(fileURLToPath(import.meta.url)); ; directory = path.dirname(directory)) {\n const found = names.map((name) => path.join(directory, 'native', name)).filter((file) => existsSync(file));\n\n if (found.length > 0) {\n return found;\n }\n\n if (path.dirname(directory) === directory) {\n return [];\n }\n }\n}\n\nlet binding: NativeBinding | undefined;\n\nfunction loadBinding(): NativeBinding {\n if (binding) {\n return binding;\n }\n\n const require = createRequire(import.meta.url);\n const errors: string[] = [];\n\n for (const file of findBinaries()) {\n try {\n binding = require(file) as NativeBinding;\n\n return binding;\n } catch (error) {\n errors.push(`${file}: ${(error as Error).message}`);\n }\n }\n\n const reason = errors.length > 0 ? `, and loading it failed (${errors.join('; ')})` : '';\n\n throw new Error(\n `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\\`.`,\n );\n}\n\nexport function scanRust(source: string): RustScan {\n return loadBinding().scanRust(source);\n}\n\nexport function rustFingerprint(source: string): string {\n return loadBinding().rustFingerprint(source);\n}\n\nexport function isRustCode(text: string): boolean {\n return loadBinding().isRustCode(text);\n}\n","export type StyleIssueType = 'semicolon' | 'em-dash' | 'dash' | 'label-colon' | 'emphasis' | 'banner';\n\nexport interface StyleLine {\n /**\n * This is the 0-based line in the file.\n */\n readonly index: number;\n readonly text: string;\n}\n\nexport interface StyleIssue {\n readonly type: StyleIssueType;\n readonly index: number;\n}\n\n/**\n * Matches four or more repeated characters at the start or end of a line, since three like `===`,\n * `---`, or `~~~` are operators, rules, and fences in prose. A run in the middle of a line is a\n * table rule like `| ---- |` or an arrow like `---->`, and a Markdown heading like `#### Examples`\n * isn't a banner either.\n */\nconst BANNER = /^(?!#{1,6}\\s)([-=*#_~+/\\\\])\\1{3,}|([-=*#_~])\\2{3,}\\s*$/;\n\n/**\n * Runs of the same character separated only by spaces, like `-------- -----`, underline the\n * columns of a table.\n */\nconst COLUMN_RULE = /^([-=_~])\\1*(?:\\s+\\1+)+$/;\n\n/**\n * Matches an en-dash, a spaced hyphen, or a double hyphen between words, which may stand in for an\n * em-dash. An en-dash or hyphen between numbers, like `10 – 20`, is a range, and a double hyphen\n * next to a CLI flag, like `--watch -- file` or `npm test -- --watch`, separates arguments.\n */\nconst DASH = /(?<!\\d)\\s[–-]\\s(?!\\d)|(?<!(?:^|\\s)-[\\w-]*)\\s--\\s(?!-)/;\nconst SEMICOLON = /;(?:\\s|$)/;\n\n/**\n * Matches `*emphasis*` or `**bold**`, which starts with a letter or digit and whose\n * closing asterisks end the word, so that globs like `*foo*.ts` or `*.test.*` aren't\n * emphasis. Asterisks in quotes, like `'*foo*'`, are a string or glob.\n */\nconst EMPHASIS = /(?:^|[\\s(])(\\*\\*?)(?=[\\p{L}\\p{N}])[^*]+?(?<!\\s)\\1(?=$|[\\s)\"']|[.,;:!?](?:$|[\\s)\"']))/u;\n\nexport const LIST_MARKER = /^(?:[-*+]|\\d+[.)])\\s+/;\n\n/**\n * A code span, as in Markdown, closes with a run of as many backticks as it opened with,\n * like ``` `` `a` `` ```, and may continue on the next line.\n */\nexport const CODE_SPAN = /(?<!`)(`+)(?!`)[\\s\\S]*?(?<!`)\\1(?!`)/g;\n\n/**\n * Matches a colon after a word with more text after it on the same line, like `Optional: skips it`\n * or `...through its presets: the rules that...`. A colon that ends a line introduces what's below\n * it. Markers like `TODO:` or `WARNING:` that start a sentence are conventions read by tooling,\n * and a colon after a code span or `{@link}` (replaced by `code` in prose) introduces a definition,\n * like `` `width`: the maximum width ``, so neither counts. Neither does an identifier in camel or\n * snake case, like `maxRetries: 3`, which is a property.\n */\nconst COLON =\n /(?<=[\\w)\\]'\"’])(?<!(?:^|[.!?]\\s+)(?:[A-Z]{2,}|[Tt]odo|[Ff]ixme|[Hh]ack|[Xx]xx))(?<!\\b(?:code|url|entity|[A-Za-z][a-z\\d]*[A-Z]\\w*|\\w*_\\w*)):\\s+\\S/;\n\nexport function replaceKeepingLines(text: string, pattern: RegExp, replacement: string): string {\n return text.replaceAll(pattern, (match) => `${replacement}${'\\n'.repeat(match.split('\\n').length - 1)}`);\n}\n\nexport function stripNonProse(text: string): string {\n let stripped = replaceKeepingLines(text, CODE_SPAN, 'code');\n\n for (let previous = ''; previous !== stripped; ) {\n previous = stripped;\n stripped = replaceKeepingLines(stripped, /\\{[^{}]*\\}/g, 'code');\n }\n\n return stripped\n .replaceAll(/\\b[a-z][\\w+.-]*:\\/\\/\\S+/gi, 'url')\n .replaceAll(/&(?:[a-z][a-z\\d]*|#\\d+|#x[\\da-f]+);/gi, 'entity');\n}\n\nfunction hasExplanatoryColon(text: string): boolean {\n return COLON.test(text.replace(LIST_MARKER, ''));\n}\n\nexport function isBanner(text: string): boolean {\n const prose = stripNonProse(text.trim());\n\n return BANNER.test(prose) && !COLUMN_RULE.test(prose);\n}\n\nexport function findStyleIssues(lines: readonly StyleLine[]): StyleIssue[] {\n const issues: StyleIssue[] = [];\n const prose = stripNonProse(lines.map((line) => line.text.trim()).join('\\n'));\n\n /**\n * A JSDoc tag's description starts a sentence after its `@param name -` style head.\n * The head, including its type, may wrap across lines.\n */\n const proseLines = replaceKeepingLines(prose, /^@\\w+\\s+(?:code\\s+)?(?:\\S+\\s+-\\s+)?/gm, '').split('\\n');\n\n for (const [position, line] of lines.entries()) {\n const text = proseLines[position].trim();\n\n const report = (type: StyleIssueType): void => {\n issues.push({ type, index: line.index });\n };\n\n if (isBanner(line.text)) {\n report('banner');\n } else {\n if (SEMICOLON.test(text)) {\n report('semicolon');\n }\n\n if (text.includes('—')) {\n report('em-dash');\n }\n\n if (DASH.test(text)) {\n report('dash');\n }\n\n if (hasExplanatoryColon(text)) {\n report('label-colon');\n }\n\n if (EMPHASIS.test(text)) {\n report('emphasis');\n }\n }\n }\n\n return issues;\n}\n","export interface WrapOptions {\n /**\n * Sets whether a line may not end with the first word of a sentence, clause, or parenthetical,\n * like `This`, `the` after a comma, or `(the`. Such a word moves to the next line instead.\n * It also sets whether a line avoids ending with the first two words of a sentence, and whether\n * the first of two lines ends with a sentence instead of the start of the next one, as long as\n * the second line is then no longer than the first.\n */\n readonly avoidSentenceOrphans: boolean;\n\n /**\n * Sets how many columns a line may exceed its limit before it counts as overflowing.\n * A layout also uses those columns when that saves a line.\n */\n readonly tolerance: number;\n\n /**\n * Sets how many extra columns a complete parenthetical may use beyond the preferred line limit.\n */\n readonly parentheticalOverflow?: number;\n}\n\nexport type LineIssueType = 'overflow' | 'underfill' | 'orphan' | 'widow' | 'unbalanced' | 'split-parenthetical';\n\nexport interface LineIssue {\n readonly type: LineIssueType;\n\n /**\n * Holds the index of the offending line within the paragraph.\n */\n readonly line: number;\n}\n\n// punctuation inside a code span, like `,` or `?.`, is code and doesn't end a clause or sentence\nconst CLAUSE_END = /[.!?,;:][\"')\\]*_]*$/;\nconst SENTENCE_END = /[.!?][\"')\\]*_]*$/;\n\n/**\n * Holds only abbreviations that never end a sentence, unlike `no.` or `max.`, which can also be the\n * last word of one.\n */\nconst ABBREVIATIONS = new Set([\n 'e.g.',\n 'i.e.',\n 'etc.',\n 'vs.',\n 'cf.',\n 'approx.',\n 'incl.',\n 'resp.',\n 'al.',\n 'viz.',\n 'esp.',\n 'ca.',\n 'fig.',\n 'figs.',\n 'eq.',\n 'vol.',\n 'pp.',\n 'mr.',\n 'mrs.',\n 'ms.',\n 'dr.',\n 'prof.',\n 'st.',\n]);\n\nexport function isAbbreviation(word: string): boolean {\n const bare = word\n .replace(/^[([\"']+/, '')\n .replace(/[)\\]\"']+$/, '')\n .toLowerCase();\n\n return ABBREVIATIONS.has(bare) || /^(?:[a-z]\\.){2,}$/.test(bare);\n}\n\nfunction findClosingBrace(text: string, start: number): number {\n let depth = 0;\n\n for (let index = start; index < text.length; index += 1) {\n if (text[index] === '{') {\n depth += 1;\n } else if (text[index] === '}') {\n depth -= 1;\n\n if (depth === 0) {\n return index;\n }\n }\n }\n\n return -1;\n}\n\nfunction backtickRun(text: string, start: number): number {\n let end = start;\n\n while (text[end] === '`') {\n end += 1;\n }\n\n return end - start;\n}\n\n/**\n * As in Markdown, a code span opened by a run of backticks only closes with a run of the same\n * length, like the two in ``` `` `a` `` ```.\n */\nfunction findCodeSpanEnd(text: string, start: number): number {\n const run = backtickRun(text, start);\n\n for (let index = text.indexOf('`', start + run); index !== -1; ) {\n const length = backtickRun(text, index);\n\n if (length === run) {\n return index + length - 1;\n }\n\n index = text.indexOf('`', index + length);\n }\n\n return -1;\n}\n\n/**\n * Returns whether a backslash escapes the character at `index`, as in Markdown. A backslash that\n * another one escapes, like the second of `\\\\[`, escapes nothing.\n */\nfunction isEscaped(text: string, index: number): boolean {\n let backslashes = 0;\n\n while (text[index - 1 - backslashes] === '\\\\') {\n backslashes += 1;\n }\n\n return backslashes % 2 === 1;\n}\n\nfunction findUnescaped(text: string, char: string, start: number): number {\n for (let index = start; index < text.length; index += 1) {\n if (text[index] === '\\\\') {\n index += 1;\n } else if (text[index] === char) {\n return index;\n }\n }\n\n return -1;\n}\n\n/**\n * Returns the index of the `]` that closes the `[` at `start`, or -1 when it never closes.\n * Brackets pair up as in Markdown, where a bracket after a backslash or in a code span is no\n * bracket. A brace is no syntax in Markdown, so a `{` after the `[` never hides the `]`.\n */\nfunction findClosingBracket(text: string, start: number): number {\n let depth = 0;\n\n for (let index = start; index < text.length; index += 1) {\n switch (text[index]) {\n case '\\\\': {\n index += 1;\n\n break;\n }\n case '`': {\n const close = findCodeSpanEnd(text, index);\n\n // a run of backticks that never closes is literal text\n index = close === -1 ? index + backtickRun(text, index) - 1 : close;\n\n break;\n }\n case '[': {\n depth += 1;\n\n break;\n }\n case ']': {\n depth -= 1;\n\n if (depth === 0) {\n return index;\n }\n\n break;\n }\n }\n }\n\n return -1;\n}\n\n/**\n * Returns the index of the `)` that closes the `(` at `start`, which opens the destination of a\n * Markdown link, or -1 when it never closes. Parentheses pair up. A destination in angle brackets,\n * like `(<a b>)`, and a title in quotes after the destination, like `(target \"title\")`, can hold a\n * parenthesis that pairs up with nothing.\n */\nfunction findDestinationEnd(text: string, start: number): number {\n let depth = 0;\n\n for (let index = start; index < text.length; index += 1) {\n const char = text[index];\n\n let close = -1;\n\n if (char === '\\\\') {\n index += 1;\n } else if (char === '<' && text.slice(start + 1, index).trim() === '') {\n close = findUnescaped(text, '>', index + 1);\n } else if ((char === '\"' || char === \"'\") && /\\s/.test(text[index - 1])) {\n close = findUnescaped(text, char, index + 1);\n } else if (char === '(') {\n depth += 1;\n } else if (char === ')' && --depth === 0) {\n return index;\n }\n\n index = close === -1 ? index : close;\n }\n\n return -1;\n}\n\n/**\n * Returns the index of the last character of the Markdown link that starts with the `[` at\n * `start`, or -1 when the bracket never closes. A link is the text in brackets, like `[text]`,\n * with the destination right after it, like `[text](target \"title\")`. The label of a reference\n * link, like the second pair of brackets in `[text][label]`, is a link that starts right after\n * the first one, so it needs no check of its own.\n *\n * Only the syntax counts. Whether the text in brackets resolves to anything, like an intra-doc link\n * in Rust or a link reference definition, is something only the renderer knows.\n */\nfunction findLinkEnd(text: string, start: number): number {\n const close = findClosingBracket(text, start);\n\n if (close === -1 || text[close + 1] !== '(') {\n return close;\n }\n\n const end = findDestinationEnd(text, close + 1);\n\n return end === -1 ? close : end;\n}\n\n/**\n * Splits text into words. JSDoc writes types and `{@link ...}` tags as `{...}` groups, so they stay\n * intact like code spans.\n *\n * A Markdown link stays intact too, since a link that continues on the next line can't be followed\n * in an editor. rust-analyzer, for one, only resolves an intra-doc link that sits on one line of\n * a `///` comment. A link is one word, like a code span, but its own words keep one space between\n * them, like the words around it.\n */\nexport function tokenize(text: string): string[] {\n return splitWords(text, true);\n}\n\nfunction splitWords(text: string, keepLinks: boolean): string[] {\n const tokens: string[] = [];\n\n let current = '';\n let index = 0;\n\n while (index < text.length) {\n const char = text[index];\n const linkEnd = keepLinks && char === '[' && !isEscaped(text, index) ? findLinkEnd(text, index) : -1;\n\n if (linkEnd !== -1) {\n current += splitWords(text.slice(index, linkEnd + 1), false).join(' ');\n index = linkEnd + 1;\n\n continue;\n }\n\n const close = char === '`' ? findCodeSpanEnd(text, index) : char === '{' ? findClosingBrace(text, index) : -1;\n\n // a run of backticks that never closes is literal text, and none of its backticks open a span\n if (char === '`' && close === -1) {\n const run = backtickRun(text, index);\n\n current += text.slice(index, index + run);\n index += run;\n\n continue;\n }\n\n if (close !== -1) {\n current += text.slice(index, close + 1);\n index = close + 1;\n\n continue;\n }\n\n if (/\\s/.test(char)) {\n if (current) {\n tokens.push(current);\n\n current = '';\n }\n\n index += 1;\n\n continue;\n }\n\n current += char;\n index += 1;\n }\n\n if (current) {\n tokens.push(current);\n }\n\n return tokens;\n}\n\nfunction parentheticalEnd(text: string): number {\n let depth = 0;\n\n for (let index = 0; index < text.length; index += 1) {\n const char = text[index];\n\n if (char === '\\\\') {\n index += 1;\n\n continue;\n }\n\n const close = char === '`' ? findCodeSpanEnd(text, index) : char === '{' ? findClosingBrace(text, index) : -1;\n\n if (close !== -1) {\n index = close;\n } else if (char === '(') {\n depth += 1;\n } else if (char === ')' && --depth === 0) {\n return index;\n }\n }\n\n return -1;\n}\n\nfunction isParenthetical(token: string): boolean {\n return token.startsWith('(') && parentheticalEnd(token) !== -1;\n}\n\nexport interface ParentheticalGroup {\n readonly start: number;\n readonly end: number;\n}\n\nfunction parentheticalGroups(tokens: readonly string[], limit: number): ParentheticalGroup[] {\n const groups: ParentheticalGroup[] = [];\n\n for (let index = 0; index < tokens.length; index += 1) {\n if (tokens[index].startsWith('(')) {\n const end = parentheticalEnd(tokens.slice(index).join(' '));\n\n let last = index;\n let length = tokens[index].length;\n\n while (length <= end && last + 1 < tokens.length) {\n length += 1 + tokens[++last].length;\n }\n\n if (end !== -1 && length <= limit) {\n groups.push({ start: index, end: last + 1 });\n\n index = last;\n\n continue;\n }\n }\n }\n\n return groups;\n}\n\n/**\n * Returns the parenthetical that a line break before the token at `at` splits, of those that fit\n * in `limit` columns.\n */\nexport function parentheticalAround(\n tokens: readonly string[],\n at: number,\n limit: number,\n): ParentheticalGroup | undefined {\n return parentheticalGroups(tokens, limit).find((group) => group.start < at && at < group.end);\n}\n\nexport function bindParentheticals(tokens: readonly string[], limit: number): string[] {\n const groups = new Map(parentheticalGroups(tokens, limit).map((group) => [group.start, group.end]));\n const units: string[] = [];\n\n for (let index = 0; index < tokens.length; index += 1) {\n const end = groups.get(index) ?? index + 1;\n\n units.push(tokens.slice(index, end).join(' '));\n\n index = end - 1;\n }\n\n return units;\n}\n\nfunction endsClause(token: string): boolean {\n return CLAUSE_END.test(token) && !isAbbreviation(token);\n}\n\nfunction endsSentence(token: string): boolean {\n return SENTENCE_END.test(token) && !isAbbreviation(token);\n}\n\nfunction opensAside(token: string): boolean {\n return /^[([]/.test(token) && !/[)\\]][\"'`.,;:!?]*$/.test(token);\n}\n\nfunction isOrphan(previous: string, token: string): boolean {\n return opensAside(token) || (endsClause(previous) && !CLAUSE_END.test(token));\n}\n\n/**\n * Returns whether Markdown or JSDoc reads a word as the start of a block when it begins a line, like a\n * list marker (`-`, `1.`), a heading (`#`), a quote (any word that starts with `>`, like `>=`), a fence,\n * a table row, an HTML tag, comment, or declaration (`<div>`, `<!--`, `<?`), or a JSDoc tag like `@param`.\n * A run of `-`, `=`, `*`, or `_` on a line of its own is a heading underline or a rule, so it never starts\n * a line either. Wrapping must never move such a word to the start of a line, or the paragraph would turn\n * into something else.\n */\nexport function startsBlock(token: string): boolean {\n return (\n /^(?:\\+|-+|=+|\\*+|_{3,}|\\d+[.)]|#{1,6})$/.test(token) ||\n /^(?:>|@[A-Za-z]|`{3}|~{3}|\\||<(?:\\/?[A-Za-z]|[!?]))/.test(token)\n );\n}\n\nexport function bindLineStarts(\n tokens: readonly string[],\n startsLine: (token: string, next: string | undefined) => boolean = startsBlock,\n): string[] {\n const units: string[] = [];\n\n for (const [index, token] of tokens.entries()) {\n if (units.length > 0 && startsLine(token, tokens[index + 1])) {\n units[units.length - 1] += ` ${token}`;\n } else {\n units.push(token);\n }\n }\n\n return units;\n}\n\nfunction breaksBefore(previous: string, last: string, next: string, options: WrapOptions): boolean {\n return (options.avoidSentenceOrphans && isOrphan(previous, last)) || startsBlock(next);\n}\n\nfunction lineLength(tokens: readonly string[]): number {\n let length = 0;\n\n for (const token of tokens) {\n length += token.length;\n }\n\n return length + Math.max(0, tokens.length - 1);\n}\n\n/**\n * The first line can have a different limit than the rest, e.g. when a list marker takes up space\n * only on the first line.\n */\nexport function wrapTokens(\n tokens: readonly string[],\n firstLimit: number,\n restLimit: number,\n options: WrapOptions,\n): string[][] {\n return wrapFrom(tokens, 0, firstLimit, restLimit, options, Infinity);\n}\n\n/**\n * Wraps the tokens from the one at `from` on, and stops after `maxLines` lines. A line\n * never depends on the lines after it, so the first lines of a wrap are the same with or\n * without the rest.\n */\nfunction wrapFrom(\n tokens: readonly string[],\n from: number,\n firstLimit: number,\n restLimit: number,\n options: WrapOptions,\n maxLines: number,\n): string[][] {\n const lines: string[][] = [];\n\n let line: string[] = [];\n let length = 0;\n\n for (let index = from; index < tokens.length; index += 1) {\n const token = tokens[index];\n\n while (line.length > 0) {\n const limit = lines.length === 0 ? firstLimit : restLimit;\n const nextLength = length + 1 + token.length;\n const extra = isParenthetical(token) ? (options.parentheticalOverflow ?? 0) : 0;\n\n if (nextLength <= limit + extra) {\n break;\n }\n\n const carry: string[] = [];\n\n while (line.length >= 2 && breaksBefore(line.at(-2) ?? '', line.at(-1) ?? '', carry[0] ?? token, options)) {\n carry.unshift(line.pop() ?? '');\n }\n\n lines.push(line);\n\n if (lines.length >= maxLines) {\n return lines;\n }\n\n line = carry;\n length = lineLength(carry);\n }\n\n line.push(token);\n\n length = lineLength(line);\n }\n\n if (line.length > 0) {\n lines.push(line);\n }\n\n return lines;\n}\n\nconst MIN_LAST_LINE_WORDS = 3;\n\nexport function hasWidow(lines: readonly (readonly string[])[]): boolean {\n const last = lines.at(-1) ?? [];\n const words = last.reduce((count, token) => count + (isParenthetical(token) ? tokenize(token).length : 1), 0);\n\n return lines.length >= 2 && words < MIN_LAST_LINE_WORDS;\n}\n\nexport interface LayoutOptions extends WrapOptions {\n /**\n * Keeps the first line out of the balancing, e.g. for a JSDoc tag whose first\n * line holds `@param {Type} name -`.\n */\n readonly keepFirstLine?: boolean;\n\n /**\n * Keeps the fewest lines even when a line sticks out far past the line before it.\n */\n readonly keepSteps?: boolean;\n\n /**\n * Lets a line end with the first few words of a sentence at no extra cost.\n */\n readonly ignoreSentenceStarts?: boolean;\n}\n\n/**\n * Sets how heavily lines that come close to the width count against lines that are even. With too\n * much weight, lines of 86 and 89 columns beat two of 86 at a width of 100, which leaves a word\n * like `as` at the end of the longer line, away from the words that it belongs to.\n */\nconst FULLNESS_WEIGHT = 0.07;\nconst PARENTHETICAL_SPLIT_WEIGHT = 2;\n\n/**\n * Sets how much a line costs when it ends with the first `MAX_SENTENCE_START_WORDS` words or fewer\n * of a sentence, like `It allocates` in `...UDP opens. It allocates`. It costs as much as a line\n * that ends 5 columns before the others, so lines that are only a little less even win.\n */\nconst SENTENCE_START_COST = 25;\nconst MAX_SENTENCE_START_WORDS = 2;\n\n/**\n * Sets how many columns a line may stick out past the line before it.\n */\nconst MAX_STEP = 15;\n\n/**\n * Sets how many lines more than its fewest a layout may take to keep a line from sticking out.\n */\nconst EXTRA_LINES = 2;\n\n/**\n * Holds what the cost of a line depends on, besides the length that the lines aim for.\n */\ninterface BalanceInput {\n /**\n * Holds the column that each token starts at when all tokens share a line, and the column after\n * the last token.\n */\n readonly starts: readonly number[];\n readonly count: number;\n\n /**\n * Holds how many fewer columns the first line has than the other lines.\n */\n readonly offset: number;\n\n /**\n * Holds for each token whether it may not start a line.\n */\n readonly blockStarts: readonly boolean[];\n\n /**\n * Holds for each token whether a line of two or more tokens may not end before it.\n */\n readonly afterOrphan: readonly boolean[];\n readonly splitCosts: ReadonlyMap<number, number>;\n}\n\n/**\n * Holds the cheapest layouts that end one line, one for each token that the line can end before.\n */\ninterface LineCosts {\n /**\n * Holds the token that the first entry of `costs` and `origins` ends before.\n */\n readonly base: number;\n readonly costs: readonly number[];\n\n /**\n * Holds the token that the line starts with in the layout of each entry of `costs`.\n */\n readonly origins: readonly number[];\n\n /**\n * Holds the first token that the line can end before.\n */\n readonly low: number;\n\n /**\n * Holds the last token that the line can end before.\n */\n readonly high: number;\n}\n\n/**\n * Returns, for each token, the fewest lines of at most `target` columns that hold the tokens from\n * it on. No token is longer than `target`.\n */\nfunction fewestLines(starts: readonly number[], target: number): Int32Array {\n const total = starts.length - 1;\n const fewest = new Int32Array(total + 1);\n\n let end = total;\n\n for (let index = total - 1; index >= 0; index -= 1) {\n while (starts[end] - starts[index] - 1 > target) {\n end -= 1;\n }\n\n fewest[index] = fewest[end] + 1;\n }\n\n return fewest;\n}\n\n/**\n * Returns the first index from `from` on whose value is at most `limit`. The values never grow as\n * the index rises, so a binary search can find it.\n */\nfunction firstAtMost(values: Int32Array, limit: number, from: number): number {\n let low = from;\n let high = values.length - 1;\n\n while (low < high) {\n const middle = Math.floor((low + high) / 2);\n\n if (values[middle] <= limit) {\n high = middle;\n } else {\n low = middle + 1;\n }\n }\n\n return low;\n}\n\n/**\n * Returns the cheapest layouts that end the given line, or `undefined` when the line can't end\n * anywhere. It only looks at the ends that a whole layout can use. The lines before it reach up\n * to an end, and the lines after it can hold the rest from an end on, which leaves a narrow band\n * of tokens instead of all of them.\n */\nfunction costsOfLine(\n input: BalanceInput,\n target: number,\n line: number,\n previous: LineCosts,\n fewest: Int32Array,\n): LineCosts | undefined {\n const { starts, afterOrphan } = input;\n const offset = line === 1 ? input.offset : 0;\n const linesLeft = input.count - line;\n\n // each line after this one needs a token\n const last = starts.length - 1 - linesLeft;\n const base = firstAtMost(fewest, linesLeft, Math.max(line, previous.low + 1));\n const costs: number[] = [];\n const origins: number[] = [];\n\n let low = -1;\n let high = -1;\n\n for (let end = base; end <= last; end += 1) {\n const nearest = Math.min(end - 1, previous.high);\n\n // from here on, a line is too long even when it starts at the last end of the line before it\n if (end > previous.high && starts[end] - starts[nearest] - 1 + offset > target) {\n break;\n }\n\n let cost = Infinity;\n let origin = -1;\n\n if (!input.blockStarts[end]) {\n const splitCost = input.splitCosts.get(end) ?? 0;\n\n for (let start = nearest; start >= previous.low; start -= 1) {\n const length = starts[end] - starts[start] - 1 + offset;\n\n if (length > target) {\n break;\n }\n\n const before = previous.costs[start - previous.base];\n\n if (before === Infinity || (afterOrphan[end] && end - start >= 2)) {\n continue;\n }\n\n const candidate = before + (target - length) ** 2 + splitCost;\n\n if (candidate <= cost) {\n cost = candidate;\n origin = start;\n }\n }\n }\n\n costs.push(cost);\n origins.push(origin);\n\n if (cost !== Infinity) {\n low = low === -1 ? end : low;\n high = end;\n }\n }\n\n return low === -1 ? undefined : { base, costs, origins, low, high };\n}\n\n/**\n * Lays the tokens out in `count` lines that are as even as possible. It tries every length that\n * the lines can aim for and keeps the layout with the lowest cost. Of two layouts with the same\n * cost, it keeps the one that aims for longer lines, and then the one with the longer last line.\n */\nfunction balanceTokens(\n tokens: readonly string[],\n firstLimit: number,\n restLimit: number,\n count: number,\n options: LayoutOptions,\n preferredLimit = restLimit,\n splitCosts: ReadonlyMap<number, number> = new Map(),\n): string[][] | undefined {\n const total = tokens.length;\n const starts = [0];\n\n let longest = -Infinity;\n\n for (const token of tokens) {\n starts.push((starts.at(-1) ?? 0) + token.length + 1);\n\n longest = Math.max(longest, token.length);\n }\n\n const input: BalanceInput = {\n starts,\n count,\n offset: restLimit - firstLimit,\n blockStarts: tokens.map((token) => startsBlock(token)),\n afterOrphan: tokens.map((_, index) => {\n return options.avoidSentenceOrphans && index >= 2 && isOrphan(tokens[index - 2], tokens[index - 1]);\n }),\n splitCosts: withSentenceStartCosts(tokens, splitCosts, options),\n };\n\n const targets: number[] = [];\n\n for (let target = restLimit; target >= Math.max(longest, Math.floor(restLimit / 2)); target -= 1) {\n targets.push(target);\n }\n\n // the lines before the last one hold at most this many columns together, which is when the last line is empty\n const most = starts[total] - count + input.offset;\n\n let best: { cost: number; target: number; breaks: number[] } | undefined;\n\n /**\n * Short targets come first. They rule most layouts out, so they are cheap, and their cost lets\n * the loop skip the long targets, whose lines can't all be full.\n */\n for (const target of targets.toReversed()) {\n const fullness = FULLNESS_WEIGHT * (count - 1) * (preferredLimit - target) ** 2;\n const spare = (count - 1) * target - most;\n\n // lines that are `spare` columns short together cost the least when they are all equally short\n const least = count > 1 && spare > 0 ? Math.max(spare ** 2 / (count - 1) - 1, 0) : 0;\n\n if (best !== undefined && least + fullness > best.cost) {\n continue;\n }\n\n const fewest = fewestLines(starts, target);\n const rows: LineCosts[] = [{ base: 0, costs: [0], origins: [-1], low: 0, high: 0 }];\n\n for (let line = 1; line < count; line += 1) {\n const row = costsOfLine(input, target, line, rows[line - 1], fewest);\n\n if (!row) {\n break;\n }\n\n rows.push(row);\n }\n\n if (rows.length < count) {\n continue;\n }\n\n const row = rows[count - 1];\n\n for (let start = row.low; start <= row.high; start += 1) {\n const score = row.costs[start - row.base] + fullness;\n\n const improves = score < (best?.cost ?? Infinity) || (score === best?.cost && target > best.target);\n\n if (!improves || starts[total] - starts[start] - 1 > target || hasWidow([[], tokens.slice(start)])) {\n continue;\n }\n\n const breaks = [start];\n\n for (let line = count - 1; line > 1; line -= 1) {\n breaks.unshift(rows[line].origins[breaks[0] - rows[line].base]);\n }\n\n best = { cost: score, target, breaks };\n }\n }\n\n if (!best) {\n return undefined;\n }\n\n const bounds = [0, ...best.breaks, total];\n\n return bounds.slice(0, -1).map((start, index) => tokens.slice(start, bounds[index + 1]));\n}\n\n/**\n * Returns whether a line sticks out more than `MAX_STEP` columns past the line before it.\n * The first line starts `offset` columns after the others, so it ends that much later too.\n */\nfunction hasStep(lines: readonly (readonly string[])[], offset = 0): boolean {\n return lines.some((line, index) => {\n if (index === 0) {\n return false;\n }\n\n const before = lineLength(lines[index - 1]) + (index === 1 ? offset : 0);\n\n return lineLength(line) - before > MAX_STEP;\n });\n}\n\nfunction splitCostsOf(tokens: readonly string[], limit: number): Map<number, number> {\n const splitCosts = new Map<number, number>();\n\n for (const group of parentheticalGroups(tokens, limit)) {\n const length = lineLength(tokens.slice(group.start, group.end));\n\n for (let at = group.start + 1; at < group.end; at += 1) {\n splitCosts.set(at, length * PARENTHETICAL_SPLIT_WEIGHT);\n }\n }\n\n return splitCosts;\n}\n\n/**\n * Adds `SENTENCE_START_COST` to each line break that leaves the first words of a sentence at the\n * end of the line before it. A break after a whole sentence of that few words costs nothing.\n */\nfunction withSentenceStartCosts(\n tokens: readonly string[],\n splitCosts: ReadonlyMap<number, number>,\n options: LayoutOptions,\n): ReadonlyMap<number, number> {\n if (!options.avoidSentenceOrphans || options.ignoreSentenceStarts) {\n return splitCosts;\n }\n\n const costs = new Map(splitCosts);\n\n for (const [index, token] of tokens.entries()) {\n if (!endsSentence(token)) {\n continue;\n }\n\n const last = Math.min(index + 1 + MAX_SENTENCE_START_WORDS, tokens.length - 1);\n\n for (let end = index + 2; end <= last && !endsSentence(tokens[end - 1]); end += 1) {\n costs.set(end, (costs.get(end) ?? 0) + SENTENCE_START_COST);\n }\n }\n\n return costs;\n}\n\nfunction hasParentheticals(tokens: readonly string[], restLimit: number, options: WrapOptions): boolean {\n if (options.parentheticalOverflow === undefined) {\n return false;\n }\n\n return bindParentheticals(tokens, restLimit + options.parentheticalOverflow).some((token) => isParenthetical(token));\n}\n\n/**\n * Lays the tokens out in even lines. When a line sticks out far past the line before it, like a\n * full second line after a first line that the orphan rule keeps short, the layout takes up to\n * `EXTRA_LINES` more lines if those avoid the step.\n */\nexport function layoutTokens(\n tokens: readonly string[],\n firstLimit: number,\n restLimit: number,\n options: LayoutOptions,\n): string[][] {\n const lines = layoutLines(tokens, firstLimit, restLimit, options);\n const offset = restLimit - firstLimit;\n\n // the first line of a JSDoc tag is never balanced, and the lines after it get their own layout\n if (options.keepFirstLine || options.keepSteps || !hasStep(lines, offset)) {\n return lines;\n }\n\n const overflow = hasParentheticals(tokens, restLimit, options) ? (options.parentheticalOverflow ?? 0) : 0;\n const splitCosts = overflow > 0 ? splitCostsOf(tokens, restLimit + overflow) : new Map<number, number>();\n\n for (let count = lines.length + 1; count <= lines.length + EXTRA_LINES; count += 1) {\n const even = balanceTokens(\n tokens,\n firstLimit + overflow,\n restLimit + overflow,\n count,\n options,\n restLimit,\n splitCosts,\n );\n\n if (even && !hasStep(even, offset)) {\n return even;\n }\n }\n\n return lines;\n}\n\nfunction layoutLines(\n tokens: readonly string[],\n firstLimit: number,\n restLimit: number,\n options: LayoutOptions,\n): string[][] {\n if (options.parentheticalOverflow !== undefined) {\n const grouped = bindParentheticals(tokens, restLimit + options.parentheticalOverflow);\n\n if (grouped.some((token) => isParenthetical(token))) {\n const minimumLines = Math.max(\n 2,\n Math.ceil((lineLength(tokens) + 1 + restLimit - firstLimit) / Math.max(1, restLimit + 1)),\n );\n\n let preserved = wrapTokens(grouped, firstLimit, restLimit, options);\n\n if (minimumLines === 2 && preserved.length > 2) {\n const wide = wrapTokens(\n grouped,\n firstLimit + options.parentheticalOverflow,\n restLimit + options.parentheticalOverflow,\n { ...options, parentheticalOverflow: undefined },\n );\n\n if (wide.length === 2 && !hasWidow(wide)) {\n preserved = wide;\n }\n }\n\n if (preserved.length <= 2 && !hasWidow(preserved)) {\n let cursor = 0;\n\n return preserved.map((line) => {\n const expanded: string[] = [];\n const length = lineLength(line);\n\n let used = -1;\n\n while (cursor < tokens.length && used < length) {\n const token = tokens[cursor++];\n\n expanded.push(token);\n\n used += token.length + 1;\n }\n\n return expanded;\n });\n }\n\n const splitCosts = splitCostsOf(tokens, restLimit + options.parentheticalOverflow);\n const preferred = wrapTokens(tokens, firstLimit, restLimit, { ...options, parentheticalOverflow: undefined });\n\n for (let count = minimumLines; count <= Math.max(preferred.length + 1, preserved.length); count += 1) {\n const balanced = balanceTokens(\n tokens,\n firstLimit + options.parentheticalOverflow,\n restLimit + options.parentheticalOverflow,\n count,\n options,\n restLimit,\n splitCosts,\n );\n\n if (balanced) {\n return balanced;\n }\n }\n }\n }\n\n const greedy = wrapTokens(tokens, firstLimit, restLimit, options);\n\n if (greedy.length < 2) {\n return greedy;\n }\n\n if (options.keepFirstLine) {\n const [head] = greedy;\n const rest = layoutTokens(tokens.slice(head.length), restLimit, restLimit, { ...options, keepFirstLine: false });\n const lines = [head, ...rest];\n\n if (!hasWidow(lines)) {\n return lines;\n }\n }\n\n /**\n * Lines may take up to `tolerance` columns past the limit when that saves a line. A paragraph of\n * two lines keeps them, since its second line would have to join the first.\n */\n if (options.tolerance > 0 && greedy.length > 2) {\n const fewer = balanceTokens(\n tokens,\n firstLimit + options.tolerance,\n restLimit + options.tolerance,\n greedy.length - 1,\n options,\n restLimit,\n );\n\n if (fewer && !hasStep(fewer, restLimit - firstLimit)) {\n return fewer;\n }\n }\n\n const balanced = balanceTokens(tokens, firstLimit, restLimit, greedy.length, options);\n\n if (balanced) {\n return balanced;\n }\n\n const stretched = wrapTokens(tokens, firstLimit + options.tolerance, restLimit + options.tolerance, options);\n\n return hasWidow(stretched) ? greedy : stretched;\n}\n\n/**\n * Ends the first of two lines with a sentence when the rest fits on the second line without\n * sticking out past the first. A second line that would stick out keeps the lines as they are.\n */\nexport function breakAfterSentence(\n lines: string[][],\n firstLimit: number,\n restLimit: number,\n options: WrapOptions,\n): string[][] {\n if (lines.length !== 2 || !options.avoidSentenceOrphans) {\n return lines;\n }\n\n const tokens = lines.flat();\n\n // the first line starts this many columns after the second, so it ends that much later too\n const offset = restLimit - firstLimit;\n\n for (let end = tokens.length - 1; end > 0; end -= 1) {\n const first = tokens.slice(0, end);\n const second = tokens.slice(end);\n\n // a second line that is no longer than a first line within its limit is within its own limit too\n if (\n endsSentence(tokens[end - 1]) &&\n !hasWidow([first, second]) &&\n !breaksBefore(tokens[end - 2] ?? '', tokens[end - 1], tokens[end], options) &&\n lineLength(first) <= firstLimit &&\n lineLength(second) <= lineLength(first) + offset\n ) {\n return [first, second];\n }\n }\n\n return lines;\n}\n\n/**\n * A wrap produced by `wrapTokens` with the same arguments never has issues, unless a single token\n * is wider than the limit.\n */\nexport function findLineIssues(\n lines: readonly (readonly string[])[],\n firstLimit: number,\n restLimit: number,\n options: WrapOptions,\n): LineIssue[] {\n const issues: LineIssue[] = [];\n const tokens = lines.flat();\n\n let next = 0;\n\n for (const [index, line] of lines.entries()) {\n const limit = index === 0 ? firstLimit : restLimit;\n const length = lineLength(line);\n const start = next;\n\n next += line.length;\n\n if (line.length > 1 && length > limit + options.tolerance) {\n issues.push({ type: 'overflow', line: index });\n }\n\n if (index === lines.length - 1 || line.length === 0) {\n continue;\n }\n\n if (options.avoidSentenceOrphans && line.length >= 2 && isOrphan(line.at(-2) ?? '', line.at(-1) ?? '')) {\n issues.push({ type: 'orphan', line: index });\n }\n\n /**\n * Only the first line of a wrap of the rest counts. A line that may end with the start of a\n * sentence is measured too, because the words that fit on it don't depend on its last word.\n */\n const [expected] = wrapFrom(tokens, start, limit, restLimit, options, 1);\n\n if (expected.length > line.length) {\n issues.push({ type: 'underfill', line: index });\n }\n }\n\n return issues;\n}\n","import { spawn } from 'node:child_process';\nimport { readFileSync } from 'node:fs';\nimport path from 'node:path';\nimport type { CommentReport, IssueReport, IssueType, ParagraphReport } from './analyze.js';\nimport type { AgentConfig, AgentRunner } from './config.js';\n\n/**\n * This variable is set in the agent's environment so that the agent's own stop hook doesn't\n * spawn another agent.\n */\nexport const CHILD_ENV = 'COMMENTS_HOOK_CHILD';\n\nexport const DEFAULT_RULES = `\n# Comment rules\n\n## Priorities\n\nWhen rules pull in different directions, the earlier one wins.\n\n1. 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).\n2. Keep the meaning. An edit may make a comment clearer, never different.\n3. Follow the style preferences below.\n4. Change as little as possible. Wording that already reads clearly stays, even when a note points at it.\n\n## Layout\n\n- 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.\n- 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.\n- 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.\n- 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.\n- Separate paragraphs with an empty comment line.\n- Never split a backtick code span across lines.\n- 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.\n- 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.\n- 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.\n- 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.\n- 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.\n- 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.\n\n## Style\n\nThese are preferences for comment prose. They never apply to code or exact text (see the priorities above), and a punctuation character on its own is never a reason to edit prose that already reads clearly.\n\n- Write clear sentences that flow. Descriptions read best as complete sentences with a subject and a verb, so prefer \"Returns the bytes the guest sent\" over a fragment like \\`Bytes from the guest.\\` or \\`Optional.\\` Imperative or third-person verb phrases like \"Resolve a hostname...\" or \"Aborts every channel...\" are fine, since the preference is against verb-less fragments, not against concise sentences.\n- Prefer plain sentence punctuation. Where a semicolon, an em-dash (—), or a spaced hyphen, en-dash, or double hyphen standing in for one only joins two thoughts, a period, a comma, parentheses, or a word like \"and\", \"so\", or \"because\" usually reads better. Keep the punctuation where it carries meaning or reads more clearly than the alternatives.\n- 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.\n- 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*\\` \\`\\`.\n- 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.\n- Block, JSDoc, and Rust doc comments are capitalized and end with punctuation.\n- 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.\n\n## Rust doc comments\n\nRust doc comments are Markdown that rustdoc renders.\n\n- 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)\\`.\n- A link reference definition, like \\`\\` [\\`Foo\\`]: crate::Foo \\`\\`, stays on its own line as it is. Its colon is syntax, not a label.\n- 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.\n\n## Editing\n\n- Keep the wording unless it goes against a rule above. Then reword as little as needed and keep the meaning.\n- Don't reword anything else unless the instructions below allow it.\n`.trim();\n\nconst DEFAULT_TIMEOUT_SECONDS = 300;\nconst DEFAULT_CLAUDE_MODEL = 'haiku';\n\n/**\n * Sets the longest time an agent gets to exit after its timeout before it is killed.\n */\nconst KILL_GRACE_MS = 5000;\n\n/**\n * Sets how long to wait for the rest of the output after the agent exited. A process that the agent\n * left behind keeps the pipes open for as long as it runs.\n */\nconst DRAIN_GRACE_MS = 1000;\n\nconst RUNNER_COMMANDS: Record<AgentRunner, string> = {\n claude: 'claude',\n codex: 'codex',\n cursor: 'cursor-agent',\n};\n\nexport interface AgentInvocation {\n readonly command: string;\n readonly args: readonly string[];\n readonly stdin?: string;\n}\n\nexport interface AgentResult {\n readonly ok: boolean;\n readonly output: string;\n readonly error?: string;\n}\n\n// hints are phrased as possibilities, since they come from heuristics that the agent may reject\nconst ISSUE_DESCRIPTIONS: Record<Exclude<IssueType, 'overflow'>, string> = {\n underfill: 'ends early, and words from the next line fit on it',\n widow: 'leaves fewer than three words on the last line of its paragraph',\n unbalanced:\n 'is in a paragraph whose lines are less even than the suggested rewrap, or that splits a link or code span that the rewrap keeps on one line',\n orphan:\n '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)',\n 'split-parenthetical':\n '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)',\n 'em-dash':\n \"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)\",\n banner:\n \"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)\",\n semicolon:\n \"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)\",\n 'label-colon':\n 'may use a colon after a label (leave it if it introduces a list or code, defines a term or a link like `` [`Foo`]: crate::Foo ``, is notation like a time or `key: value`, or is a `Note:` or `Optional:` that starts the comment)',\n emphasis:\n 'may use asterisk emphasis (leave the asterisks of a name or pattern like `*test*`, which belongs in a code span with its asterisks)',\n dash: \"may use a spaced hyphen, en-dash, or double hyphen as an em-dash (leave it if it's a range, a CLI argument, arithmetic, code, a term's definition, or reads more clearly than the alternatives)\",\n};\n\nconst HINT_MARKER = 'possible:';\n\nfunction describeIssue(issue: IssueReport, width: number, maxWidth?: number): string {\n if (issue.type === 'overflow') {\n const limit = maxWidth ?? width;\n const name = maxWidth === undefined ? 'width' : 'maximum';\n\n return `line ${issue.line} is ${issue.length} columns, over the ${name} of ${limit}`;\n }\n\n const description = `line ${issue.line} ${ISSUE_DESCRIPTIONS[issue.type]}`;\n\n return issue.tier === 'hint' ? `${HINT_MARKER} ${description}` : description;\n}\n\nfunction describeIssues(paragraph: ParagraphReport, width: number, maxWidth?: number): string[] {\n return paragraph.issues.map((issue) => describeIssue(issue, width, maxWidth));\n}\n\nfunction describeComment(comment: CommentReport): string {\n const notes: string[] = [];\n\n for (const paragraph of comment.paragraphs) {\n const issues = describeIssues(paragraph, comment.width, comment.maxWidth);\n\n if (issues.length > 0 && !paragraph.reflowable) {\n const range = `lines ${paragraph.startLine}-${paragraph.endLine}`;\n\n issues.push(\n `${range} are kept as written (${paragraph.reason ?? 'layout'}), so the tool never rewraps them and any fix to them is yours`,\n );\n }\n\n notes.push(...issues);\n }\n\n const fence = comment.text.includes('```') ? '~~~~' : '```';\n\n return [\n `### ${comment.file}:${comment.startLine}-${comment.endLine} (width ${comment.width}${comment.maxWidth === undefined ? '' : `, maximum ${comment.maxWidth}`})`,\n '',\n ...(notes.length > 0 ? notes.map((note) => `- ${note}`) : ['- no measured issues; review the wording and flow']),\n '',\n `${fence}${comment.file.endsWith('.rs') ? 'rust' : 'ts'}`,\n comment.text,\n fence,\n ].join('\\n');\n}\n\nexport function loadRules(agent: AgentConfig, configPath: string | undefined): string {\n let rules = DEFAULT_RULES;\n\n if (agent.rules) {\n const base = configPath ? path.dirname(configPath) : process.cwd();\n\n rules = readFileSync(path.resolve(base, agent.rules), 'utf8');\n }\n\n return agent.instructions ? `${rules.trimEnd()}\\n\\n## Instructions\\n\\n${agent.instructions.trim()}\\n` : rules;\n}\n\nexport function buildPrompt(options: {\n readonly rules: string;\n readonly root: string;\n readonly comments: readonly CommentReport[];\n}): string {\n return [\n options.rules.trimEnd(),\n '',\n '## Task',\n '',\n `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.`,\n '',\n '- 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.',\n '- 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.',\n '- 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.',\n '- 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.',\n `- Notes marked \\`${HINT_MARKER}\\` come from heuristics. If the rule doesn't actually apply, don't change anything for that note.`,\n '- 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.',\n \"- Edit only these comments. Don't change code, other comments, or other files.\",\n '- When done, reply with one line per comment saying what you changed or why you left it.',\n '',\n ...options.comments.map((comment) => describeComment(comment)).flatMap((section) => [section, '']),\n ].join('\\n');\n}\n\nexport function shellQuote(value: string): string {\n return /^[\\w./:@=-]+$/.test(value) ? value : `'${value.replaceAll(\"'\", String.raw`'\\''`)}'`;\n}\n\nexport function buildInvocation(options: {\n readonly agent: AgentConfig;\n readonly runner: AgentRunner;\n readonly prompt: string;\n readonly root: string;\n}): AgentInvocation {\n const { agent, prompt, root } = options;\n const extra = agent.args ?? [];\n\n if (agent.command && agent.command.length > 0) {\n const [command, ...args] = agent.command;\n const hasPlaceholder = args.some((arg) => arg.includes('{prompt}'));\n\n return {\n command,\n // a function keeps the prompt as it is, while a string would expand `$&` or `$'` in a comment's text\n args: args.map((arg) => arg.replaceAll('{prompt}', () => prompt)),\n stdin: hasPlaceholder ? undefined : prompt,\n };\n }\n\n const model = agent.model ? ['--model', agent.model] : [];\n\n // the agent only reads and edits files, so it gets no shell\n if (options.runner === 'claude') {\n return {\n command: 'claude',\n args: [\n '-p',\n '--no-session-persistence',\n '--model',\n agent.model ?? DEFAULT_CLAUDE_MODEL,\n '--permission-mode',\n 'acceptEdits',\n '--allowedTools',\n 'Read',\n 'Edit',\n 'Grep',\n 'Glob',\n ...extra,\n ],\n stdin: prompt,\n };\n }\n\n if (options.runner === 'codex') {\n return {\n command: 'codex',\n args: [\n 'exec',\n '--ephemeral',\n '--sandbox',\n 'workspace-write',\n '--cd',\n root,\n '-c',\n 'features.hooks=false',\n ...model,\n ...extra,\n '-',\n ],\n stdin: prompt,\n };\n }\n\n // cursor-agent has no flag to skip hooks, so the child relies on the `COMMENTS_HOOK_CHILD` guard\n return {\n command: 'cursor-agent',\n args: ['-p', '--trust', '--force', '--workspace', root, ...model, ...extra, prompt],\n };\n}\n\nexport function describeAgent(agent: AgentConfig, runner: AgentRunner): string {\n if (agent.command && agent.command.length > 0) {\n return path.basename(agent.command[0]);\n }\n\n const model = runner === 'claude' ? (agent.model ?? DEFAULT_CLAUDE_MODEL) : agent.model;\n\n return model ? `${RUNNER_COMMANDS[runner]}, model ${model}` : RUNNER_COMMANDS[runner];\n}\n\nexport function formatInvocation(invocation: AgentInvocation): string {\n const args = invocation.args.map((arg) => (arg.length > 80 ? '<prompt>' : shellQuote(arg)));\n\n return [invocation.command, ...args].join(' ');\n}\n\nexport function runAgent(\n invocation: AgentInvocation,\n options: { root: string; timeout?: number },\n): Promise<AgentResult> {\n const timeout = options.timeout ?? DEFAULT_TIMEOUT_SECONDS;\n\n return new Promise((resolve) => {\n const child = spawn(invocation.command, invocation.args, {\n cwd: options.root,\n env: { ...process.env, [CHILD_ENV]: '1' },\n });\n\n const timers: NodeJS.Timeout[] = [];\n\n let stdout = '';\n let stderr = '';\n let timedOut = false;\n let settled = false;\n\n const settle = (result: Omit<AgentResult, 'output'>): void => {\n if (settled) {\n return;\n }\n\n settled = true;\n\n for (const timer of timers) {\n clearTimeout(timer);\n }\n\n // pipes that a process left behind by the agent still holds would keep this process from exiting\n child.stdout.destroy();\n child.stderr.destroy();\n child.unref();\n\n resolve({ ...result, output: `${stdout}${stderr}`.trim() });\n };\n\n const finish = (code: number | null, signal: NodeJS.Signals | null): void => {\n if (timedOut) {\n settle({ ok: false, error: `the agent timed out after ${timeout}s` });\n } else if (code === 0) {\n settle({ ok: true });\n } else {\n settle({ ok: false, error: `the agent exited with code ${code ?? signal}` });\n }\n };\n\n timers.push(\n setTimeout(() => {\n timedOut = true;\n\n child.kill();\n\n // an agent that ignores the signal is killed, and it counts as stopped even if that fails too\n timers.push(\n setTimeout(\n () => {\n child.kill('SIGKILL');\n finish(null, 'SIGKILL');\n },\n Math.min(KILL_GRACE_MS, timeout * 1000),\n ),\n );\n }, timeout * 1000),\n );\n\n child.stdout.setEncoding('utf8').on('data', (chunk: string) => {\n stdout += chunk;\n });\n\n child.stderr.setEncoding('utf8').on('data', (chunk: string) => {\n stderr += chunk;\n });\n\n child.on('error', (error: NodeJS.ErrnoException) => {\n settle({\n ok: false,\n error: error.code === 'ENOENT' ? `\\`${invocation.command}\\` is not installed` : error.message,\n });\n });\n\n child.on('close', finish);\n\n // `close` waits for the pipes, so it may come long after the agent exited, or never\n child.on('exit', (code, signal) => {\n timers.push(\n setTimeout(() => {\n finish(code, signal);\n }, DRAIN_GRACE_MS),\n );\n });\n\n // an agent that exits without reading its stdin would otherwise crash the process with EPIPE\n child.stdin.on('error', () => {});\n child.stdin.end(invocation.stdin ?? '');\n });\n}\n","import { existsSync, readFileSync } from 'node:fs';\nimport { createRequire } from 'node:module';\nimport path from 'node:path';\nimport { pathToFileURL } from 'node:url';\n\nexport type AgentRunner = 'claude' | 'codex' | 'cursor';\nexport type HookMode = 'agent' | 'report' | 'off';\nexport type AgentScope = 'suspicious' | 'failing' | 'all';\n\nexport interface AgentConfig {\n /**\n * Names the agent CLI to spawn. Defaults to the host that triggered the hook, or `claude`\n * when run standalone.\n */\n readonly runner?: AgentRunner;\n\n /**\n * Names the model passed to the runner. Defaults to `haiku` for Claude Code and to the\n * CLI's default otherwise.\n */\n readonly model?: string;\n\n /**\n * Lists extra arguments appended to the runner's command line.\n */\n readonly args?: readonly string[];\n\n /**\n * Sets a fully custom command, e.g. `['my-agent', '--prompt', '{prompt}']`. Without a `{prompt}`\n * placeholder, the prompt is written to stdin. Overrides `runner`, `model`, and `args`.\n */\n readonly command?: readonly string[];\n\n /**\n * Sets how many seconds to wait for each agent before giving up. Defaults to 300.\n */\n readonly timeout?: number;\n\n /**\n * Sets the most comments to hand to one agent. More comments are split across several agents.\n * A file's comments always go to the same agent, since parallel agents editing one file would\n * overwrite each other's edits, so a file with more comments than this gets an agent of its\n * own. Defaults to 20.\n */\n readonly batchSize?: number;\n\n /**\n * Sets the most files to hand to one agent, since an agent editing many files is slower and less\n * careful. More files are split across several agents. Defaults to 10.\n */\n readonly maxFiles?: number;\n\n /**\n * Caps how many agents run at once. The other agents wait, and each one starts when a running\n * agent finishes. Defaults to 6.\n */\n readonly maxAgents?: number;\n\n /**\n * Points to a Markdown file with formatting rules, relative to the config file.\n * Replaces the built-in rules.\n */\n readonly rules?: string;\n\n /**\n * Holds extra instructions appended to the rules.\n */\n readonly instructions?: string;\n\n /**\n * Selects which comments the agent sees. `suspicious` (default) is a triage, not a full review.\n * It only sends comments with hints, like a possible semicolon or a line that may end with the\n * first word of a sentence, and comments with issues in parts the tool never rewraps, so most\n * runs never call the agent. The tool fixes the layout of prose itself. `failing` sends only\n * the comments with verdicts, the ones that fail `comments check`, and no comment that only has\n * hints. `all` sends every comment in changed lines, so that the agent reviews the wording of\n * each against its rules.\n */\n readonly scope?: AgentScope;\n}\n\nexport interface HookConfig {\n /**\n * Sets what the stop hook does with comments in changed lines.\n *\n * - `agent` (default): hand them to an agent and report any that still don't fit afterwards.\n * - `report`: only report suspicious comments.\n * - `off`: do nothing.\n */\n readonly mode?: HookMode;\n}\n\nexport interface CommentsConfig {\n /**\n * Sets the preferred line length for comments, including indentation and the comment\n * prefix. Defaults to prettier's `printWidth`, or 80. In Rust files, it defaults to\n * rustfmt's `comment_width` instead, which is 80 by default too.\n */\n readonly width?: number;\n\n /**\n * Sets the hard limit for measured prose. Defaults to 120, or `width` when it is larger.\n * Parentheticals may use the space between `width` and this limit to stay together.\n */\n readonly maxWidth?: number;\n\n /**\n * Sets the maximum length of the line that holds a comment whose whole text is one prose\n * paragraph. Such a comment stays on one line up to this length plus `tolerance`, even past\n * `width`, and is wrapped to `width` beyond it. Defaults to 120, and the line never exceeds\n * `maxWidth`.\n */\n readonly singleLineWidth?: number;\n\n /**\n * Sets how many columns prose lines may go past `width` when that saves a line in a paragraph of\n * three or more lines. A paragraph that already has the layout for a wider `width`, up to `width`\n * plus this many columns, also passes as it is. It never goes past `maxWidth`. When `maxWidth` is\n * omitted, it also sets a legacy hard limit of `width + tolerance`.\n */\n readonly tolerance?: number;\n\n /**\n * Keep the first word of a new sentence or parenthetical off the end of a line, e.g.\n * `...the module. This` or `...reflows (the` followed by the rest on the next line.\n * A paragraph of two lines also ends its first line with a sentence when the rest fits\n * on the second line and is no longer than the first, instead of with the start of\n * the next sentence, like `...the module. This one`. Other lines avoid ending with the\n * first two words of a sentence, like `...opens. It allocates`, unless that makes them\n * much less even. Defaults to `true`.\n */\n readonly avoidSentenceOrphans?: boolean;\n\n /**\n * Looks for possible style issues (semicolons, em-dashes and dashes, `label: explanation` colons,\n * asterisk emphasis, and section banners) and reports them as hints, so that the `suspicious` scope\n * hands those comments to the agent too. It only controls this search, and the agent's rules apply\n * to every comment the agent sees either way. Defaults to `true`.\n */\n readonly style?: boolean;\n\n /**\n * Lists the file extensions to process. Defaults to JavaScript, TypeScript, and Rust extensions.\n */\n readonly extensions?: readonly string[];\n\n /**\n * Lists glob patterns, relative to the repository root, of the only files to process.\n * Without patterns, every file with one of the `extensions` is processed. `ignore` still\n * skips files that match both.\n */\n readonly include?: readonly string[];\n\n /**\n * Lists glob patterns, relative to the repository root, of files to skip.\n */\n readonly ignore?: readonly string[];\n readonly agent?: AgentConfig;\n readonly hook?: HookConfig;\n}\n\nexport interface LoadedConfig {\n readonly config: CommentsConfig;\n readonly filepath?: string;\n}\n\nexport const DEFAULT_EXTENSIONS = Object.freeze(['.js', '.jsx', '.mjs', '.cjs', '.ts', '.tsx', '.mts', '.cts', '.rs']);\n\nconst CONFIG_FILES = [\n 'comments.config.ts',\n 'comments.config.mts',\n 'comments.config.js',\n 'comments.config.mjs',\n 'comments.config.cjs',\n '.commentsrc',\n '.commentsrc.json',\n];\n\nconst PACKAGE_JSON_KEY = 'comments';\n\nexport function defineConfig(config: CommentsConfig): CommentsConfig {\n return config;\n}\n\nfunction readJson(filepath: string): unknown {\n try {\n return JSON.parse(readFileSync(filepath, 'utf8'));\n } catch (error) {\n throw new Error(`comments: failed to parse ${filepath}: ${(error as Error).message}`, { cause: error });\n }\n}\n\nasync function readConfigFile(filepath: string): Promise<unknown> {\n if (filepath.endsWith('rc') || filepath.endsWith('.json')) {\n return readJson(filepath);\n }\n\n const module = (await import(pathToFileURL(filepath).href)) as { default?: unknown };\n\n return module.default ?? module;\n}\n\nfunction assertType(value: unknown, type: string, key: string, filepath: string): void {\n const actual = Array.isArray(value) ? 'array' : typeof value;\n\n if (value !== undefined && actual !== type) {\n throw new Error(`comments: \\`${key}\\` in ${filepath} must be a ${type}, got ${actual}`);\n }\n}\n\nfunction assertStrings(value: unknown, key: string, filepath: string): void {\n if (Array.isArray(value) && !value.every((item) => typeof item === 'string')) {\n throw new Error(`comments: \\`${key}\\` in ${filepath} must only hold strings`);\n }\n}\n\nexport function validateConfig(raw: unknown, filepath: string): CommentsConfig {\n if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {\n throw new Error(`comments: ${filepath} must export an object`);\n }\n\n const config = raw as Record<string, unknown>;\n\n assertType(config.width, 'number', 'width', filepath);\n assertType(config.maxWidth, 'number', 'maxWidth', filepath);\n assertType(config.singleLineWidth, 'number', 'singleLineWidth', filepath);\n assertType(config.tolerance, 'number', 'tolerance', filepath);\n assertType(config.avoidSentenceOrphans, 'boolean', 'avoidSentenceOrphans', filepath);\n assertType(config.style, 'boolean', 'style', filepath);\n assertType(config.extensions, 'array', 'extensions', filepath);\n assertType(config.include, 'array', 'include', filepath);\n assertType(config.ignore, 'array', 'ignore', filepath);\n assertType(config.agent, 'object', 'agent', filepath);\n assertType(config.hook, 'object', 'hook', filepath);\n assertStrings(config.extensions, 'extensions', filepath);\n assertStrings(config.include, 'include', filepath);\n assertStrings(config.ignore, 'ignore', filepath);\n\n for (const key of ['width', 'maxWidth', 'singleLineWidth', 'tolerance'] as const) {\n const value = config[key];\n const minimum = key === 'tolerance' ? 0 : 1;\n\n if (value !== undefined && (!Number.isInteger(value) || (value as number) < minimum)) {\n throw new Error(`comments: \\`${key}\\` in ${filepath} must be an integer of at least ${minimum}`);\n }\n }\n\n if (typeof config.width === 'number' && typeof config.maxWidth === 'number' && config.maxWidth < config.width) {\n throw new Error(`comments: \\`maxWidth\\` in ${filepath} must be at least \\`width\\``);\n }\n\n const agent = (config.agent ?? {}) as Record<string, unknown>;\n\n if (agent.runner !== undefined && !['claude', 'codex', 'cursor'].includes(agent.runner as string)) {\n throw new Error(`comments: \\`agent.runner\\` in ${filepath} must be one of claude, codex, cursor`);\n }\n\n assertType(agent.model, 'string', 'agent.model', filepath);\n assertType(agent.args, 'array', 'agent.args', filepath);\n assertType(agent.command, 'array', 'agent.command', filepath);\n assertType(agent.timeout, 'number', 'agent.timeout', filepath);\n assertStrings(agent.args, 'agent.args', filepath);\n assertStrings(agent.command, 'agent.command', filepath);\n\n // a timeout of zero would stop every agent right away, and `NaN` is a number too\n if (agent.timeout !== undefined && !((agent.timeout as number) > 0)) {\n throw new Error(`comments: \\`agent.timeout\\` in ${filepath} must be a positive number`);\n }\n\n for (const key of ['batchSize', 'maxFiles', 'maxAgents'] as const) {\n const value = agent[key];\n\n if (value !== undefined && (!Number.isInteger(value) || (value as number) < 1)) {\n throw new Error(`comments: \\`agent.${key}\\` in ${filepath} must be a positive integer`);\n }\n }\n\n assertType(agent.rules, 'string', 'agent.rules', filepath);\n assertType(agent.instructions, 'string', 'agent.instructions', filepath);\n\n if (agent.scope !== undefined && !['suspicious', 'failing', 'all'].includes(agent.scope as string)) {\n throw new Error(`comments: \\`agent.scope\\` in ${filepath} must be one of suspicious, failing, all`);\n }\n\n const hook = (config.hook ?? {}) as Record<string, unknown>;\n\n if (hook.mode !== undefined && !['agent', 'report', 'off'].includes(hook.mode as string)) {\n throw new Error(`comments: \\`hook.mode\\` in ${filepath} must be one of agent, report, off`);\n }\n\n return config as CommentsConfig;\n}\n\nexport async function loadConfig(cwd: string, options: { explicitPath?: string; stopDir?: string } = {}) {\n if (options.explicitPath) {\n const filepath = path.resolve(cwd, options.explicitPath);\n\n return { config: validateConfig(await readConfigFile(filepath), filepath), filepath } satisfies LoadedConfig;\n }\n\n let directory = path.resolve(cwd);\n\n for (;;) {\n for (const name of CONFIG_FILES) {\n const filepath = path.join(directory, name);\n\n if (existsSync(filepath)) {\n return { config: validateConfig(await readConfigFile(filepath), filepath), filepath } satisfies LoadedConfig;\n }\n }\n\n const packageJsonPath = path.join(directory, 'package.json');\n\n if (existsSync(packageJsonPath)) {\n const value = (readJson(packageJsonPath) as Record<string, unknown>)[PACKAGE_JSON_KEY];\n\n if (value !== undefined) {\n return { config: validateConfig(value, packageJsonPath), filepath: packageJsonPath } satisfies LoadedConfig;\n }\n }\n\n const parent = path.dirname(directory);\n\n if (directory === options.stopDir || parent === directory) {\n return { config: {} } satisfies LoadedConfig;\n }\n\n directory = parent;\n }\n}\n\ninterface PrettierOptions {\n readonly printWidth?: number;\n readonly tabWidth?: number;\n}\n\ninterface PrettierModule {\n resolveConfig: (file: string) => Promise<PrettierOptions | null>;\n}\n\nconst PRETTIER_FILES = ['.prettierrc', '.prettierrc.json', '.prettierrc.json5'];\n\nfunction readPrettierFile(directory: string, stopDir: string): PrettierOptions | undefined {\n let current = directory;\n\n for (;;) {\n for (const name of PRETTIER_FILES) {\n const filepath = path.join(current, name);\n\n if (!existsSync(filepath)) {\n continue;\n }\n\n const text = readFileSync(filepath, 'utf8');\n\n // `.prettierrc` may be YAML, so fall back to plain key lookups\n const printWidth = /[\"']?printWidth[\"']?\\s*:\\s*(\\d+)/.exec(text)?.[1];\n const tabWidth = /[\"']?tabWidth[\"']?\\s*:\\s*(\\d+)/.exec(text)?.[1];\n\n return {\n printWidth: printWidth === undefined ? undefined : Number(printWidth),\n tabWidth: tabWidth === undefined ? undefined : Number(tabWidth),\n };\n }\n\n const packageJsonPath = path.join(current, 'package.json');\n\n if (existsSync(packageJsonPath)) {\n const prettier = (readJson(packageJsonPath) as { prettier?: unknown }).prettier;\n\n if (prettier && typeof prettier === 'object') {\n return prettier as PrettierOptions;\n }\n }\n\n const parent = path.dirname(current);\n\n if (current === stopDir || parent === current) {\n return undefined;\n }\n\n current = parent;\n }\n}\n\nexport class PrettierOptionsResolver {\n private _prettier: PrettierModule | null | undefined;\n private readonly _cache = new Map<string, PrettierOptions>();\n\n constructor(private readonly _root: string) {}\n\n private async _loadPrettier(): Promise<PrettierModule | null> {\n if (this._prettier !== undefined) {\n return this._prettier;\n }\n\n try {\n const require = createRequire(path.join(this._root, 'package.json'));\n const resolved = require.resolve('prettier');\n\n this._prettier = (await import(pathToFileURL(resolved).href)) as PrettierModule;\n } catch {\n this._prettier = null;\n }\n\n return this._prettier;\n }\n\n async resolve(file: string): Promise<PrettierOptions> {\n const prettier = await this._loadPrettier();\n\n /**\n * Prettier resolves per file, since `overrides` can give files in the same\n * directory different widths.\n */\n if (prettier) {\n try {\n const options = await prettier.resolveConfig(file);\n\n if (options) {\n return options;\n }\n } catch {\n // fall back to reading the config files directly\n }\n }\n\n const directory = path.dirname(file);\n const cached = this._cache.get(directory);\n\n if (cached) {\n return cached;\n }\n\n const options = readPrettierFile(directory, this._root) ?? {};\n\n this._cache.set(directory, options);\n\n return options;\n }\n}\n\nconst RUSTFMT_FILES = ['rustfmt.toml', '.rustfmt.toml'];\n\n// rustfmt indents with four spaces by default\nconst RUSTFMT_TAB_SPACES = 4;\n\n/**\n * Reads the widths of Rust files from the nearest rustfmt config, like rustfmt does.\n * It maps `comment_width` to `printWidth`, since that is the width rustfmt wraps comments\n * at, and `tab_spaces` to `tabWidth`.\n */\nexport class RustfmtOptionsResolver {\n private readonly _cache = new Map<string, PrettierOptions>();\n\n constructor(private readonly _root: string) {}\n\n private _read(directory: string): PrettierOptions {\n const cached = this._cache.get(directory);\n\n if (cached) {\n return cached;\n }\n\n let options: PrettierOptions | undefined;\n\n for (const name of RUSTFMT_FILES) {\n const filepath = path.join(directory, name);\n\n if (existsSync(filepath)) {\n const text = readFileSync(filepath, 'utf8');\n const commentWidth = /^\\s*comment_width\\s*=\\s*(\\d+)/m.exec(text)?.[1];\n const tabSpaces = /^\\s*tab_spaces\\s*=\\s*(\\d+)/m.exec(text)?.[1];\n\n options = {\n printWidth: commentWidth === undefined ? undefined : Number(commentWidth),\n tabWidth: tabSpaces === undefined ? RUSTFMT_TAB_SPACES : Number(tabSpaces),\n };\n\n break;\n }\n }\n\n const parent = path.dirname(directory);\n\n options ??=\n directory === this._root || parent === directory ? { tabWidth: RUSTFMT_TAB_SPACES } : this._read(parent);\n\n this._cache.set(directory, options);\n\n return options;\n }\n\n resolve(file: string): PrettierOptions {\n return this._read(path.dirname(file));\n }\n}\n"]}
1
+ {"version":3,"sources":["/home/runner/work/lint-preset/lint-preset/dist/comments.cjs","../src/comments/analyze.ts","../src/comments/rust.ts","../src/comments/style.ts","../src/comments/wrap.ts","../src/comments/agent.ts","../src/comments/config.ts"],"names":["startsLine","balanced","structure","wrapped","even","existsSync","createRequire","readFileSync","path"],"mappings":"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","file":"/home/runner/work/lint-preset/lint-preset/dist/comments.cjs","sourcesContent":[null,"import { parseSync, type OxcError, type Statement } from 'oxc-parser';\nimport { isRustCode, rustFingerprint, scanRust } from './rust.js';\nimport {\n CODE_SPAN,\n findStyleIssues,\n isBanner,\n LIST_MARKER,\n replaceKeepingLines,\n stripNonProse,\n type StyleIssueType,\n} from './style.js';\nimport {\n bindLineStarts,\n breakAfterSentence,\n findLineIssues,\n hasWidow,\n layoutTokens,\n parentheticalAround,\n startsBlock,\n tokenize,\n type LayoutOptions,\n type LineIssue,\n type LineIssueType,\n type WrapOptions,\n} from './wrap.js';\n\n/**\n * Describes the syntax of a comment. Rust has doc comments for the item after them (`outer`, like `///` and `/**`)\n * and for the item that holds them (`inner`, like `//!` and `/*!`). A `line` comment is a run of plain line comments\n * (`//`), which is never rewrapped.\n */\nexport type CommentKind =\n | 'jsdoc'\n | 'block'\n | 'line'\n | 'outer-line-doc'\n | 'inner-line-doc'\n | 'outer-block-doc'\n | 'inner-block-doc';\n\nexport type ParagraphKind = 'prose' | 'list' | 'tag' | 'code' | 'other';\n\nexport interface AnalyzeOptions extends WrapOptions {\n /**\n * Sets the preferred line length, including indentation and the comment prefix.\n */\n readonly width: number;\n\n /**\n * Sets the hard limit for measured prose. Parentheticals may exceed `width` up to this limit.\n * When omitted, the existing width and tolerance behavior is retained.\n */\n readonly maxWidth?: number;\n\n /**\n * Sets the maximum length of the line that holds a comment whose whole text is one\n * prose paragraph. Such a comment stays on one line up to this length plus `tolerance`,\n * even past `width`, but never past `maxWidth`. It defaults to `width`.\n */\n readonly singleLineWidth?: number;\n\n readonly tabWidth: number;\n\n /**\n * Whether to report prose style issues like semicolons, em-dashes, and section banners.\n */\n readonly style: boolean;\n}\n\nexport type IssueType = LineIssueType | StyleIssueType;\n\nexport type IssueTier = 'verdict' | 'hint';\n\nconst ISSUE_TIERS: Record<IssueType, IssueTier> = {\n overflow: 'verdict',\n underfill: 'verdict',\n widow: 'verdict',\n unbalanced: 'verdict',\n orphan: 'hint',\n 'split-parenthetical': 'hint',\n banner: 'hint',\n 'em-dash': 'hint',\n semicolon: 'hint',\n 'label-colon': 'hint',\n emphasis: 'hint',\n dash: 'hint',\n};\n\nexport interface IssueReport {\n readonly type: IssueType;\n readonly tier: IssueTier;\n readonly line: number;\n readonly length: number;\n}\n\nexport interface ParagraphReport {\n readonly kind: ParagraphKind;\n readonly startLine: number;\n readonly endLine: number;\n\n /**\n * Whether the paragraph is prose that can be rewrapped. When `false`, `reason` says why.\n */\n readonly reflowable: boolean;\n readonly reason?: string;\n readonly issues: readonly IssueReport[];\n\n /**\n * Holds the exact source lines for `startLine` through `endLine` when the paragraph is greedily\n * wrapped to the width, or `null` when the paragraph isn't prose, e.g. code, a table, a diagram,\n * or `@example`. A paragraph that the tolerance accepts as it is, with the layout for a wider\n * width or with lines close to this one, keeps its lines.\n */\n readonly suggestion: readonly string[] | null;\n}\n\nexport interface CommentReport {\n readonly file: string;\n readonly kind: CommentKind;\n readonly startLine: number;\n readonly endLine: number;\n readonly indent: string;\n readonly width: number;\n readonly maxWidth?: number;\n\n /**\n * Holds the length of each source line from `startLine` through `endLine`, with tabs expanded.\n */\n readonly lengths: readonly number[];\n readonly text: string;\n readonly paragraphs: readonly ParagraphReport[];\n}\n\nexport interface SyntaxFailure {\n readonly message: string;\n\n /**\n * Holds the 1-based line of the error, when the parser names one.\n */\n readonly line?: number;\n}\n\nexport interface SourceAnalysis {\n readonly comments: CommentReport[];\n\n /**\n * Describes the syntax error that the parser stopped at. The parser never sees the comments after\n * one, so such a source has no comments at all instead of only those before the error.\n */\n readonly syntaxError?: SyntaxFailure;\n}\n\ninterface BodyLine {\n /**\n * Holds the 0-based index of the line in the file.\n */\n readonly index: number;\n readonly prefix: string;\n readonly text: string;\n readonly verbatim: boolean;\n\n /**\n * Whether the source line ends with Markdown hard line break syntax, which `text` no longer\n * holds after trimming.\n */\n readonly hardBreak: boolean;\n}\n\ninterface RawParagraph {\n kind: ParagraphKind;\n lines: BodyLine[];\n marker: string;\n reason?: string;\n}\n\ninterface ParsedComment {\n readonly type: 'Line' | 'Block';\n\n /**\n * Holds the text between the comment markers, like the `/ text` of `/// text`.\n */\n readonly value: string;\n readonly start: number;\n readonly end: number;\n readonly doc?: 'outer' | 'inner';\n}\n\n/**\n * Holds what tells code, legal text, and directives apart in the comments of a language.\n */\ninterface Language {\n readonly name: 'javascript' | 'rust';\n\n /**\n * Whether a whole paragraph parses as code of the language.\n */\n readonly isCode: (text: string) => boolean;\n\n /**\n * Whether a single line looks like code of the language. Prose can look like that too.\n */\n readonly isCodeLine: (text: string) => boolean;\n readonly legal: RegExp;\n readonly blockDirective?: RegExp;\n\n /**\n * Whether lines that start with `@`, like `@param`, are JSDoc tags.\n */\n readonly tags: boolean;\n\n /**\n * Whether a code span that continues on the next line may be joined into one line. rustdoc keeps\n * and shows the spaces at the start of the next line inside the code span, and how many there are\n * depends on every line of the item's docs, so Rust keeps such a paragraph as it is.\n */\n readonly joinsCodeSpans: boolean;\n}\n\nconst IGNORE_DIRECTIVE = 'comments-ignore';\n\nconst FENCE = /^(?:`{3,}(?!.*`)|~{3,})/;\nconst JSDOC_TAG = /^@([A-Za-z]+)/;\nconst NESTED_LIST_MARKER = /^\\s+(?:[-*+]|\\d+[.)])\\s+/;\n\n/**\n * Tags whose content is a prose description, while others like `@example` hold code and\n * are never rewrapped.\n */\nconst PROSE_TAGS = new Set([\n 'arg',\n 'argument',\n 'deprecated',\n 'description',\n 'exception',\n 'note',\n 'param',\n 'prop',\n 'property',\n 'remarks',\n 'return',\n 'returns',\n 'see',\n 'since',\n 'summary',\n 'template',\n 'throws',\n 'todo',\n 'typeParam',\n 'yields',\n]);\n\n// tags whose content is code that runs until the next tag, empty lines included, like a multi-part `@example`\nconst CODE_TAGS = new Set(['default', 'defaultValue', 'example']);\n\n// tags followed by a name, as in `@param {Type} name - description`\nconst NAMED_TAGS = new Set(['arg', 'argument', 'param', 'prop', 'property', 'template', 'typeParam']);\n\n/**\n * Tool directives take effect in any comment line, so a JSDoc line that starts with one is kept\n * as it is. Each is the exact name of a directive, since prose can start a line with a package\n * name like `eslint-config-prettier`.\n */\nconst LINE_DIRECTIVE =\n /^\\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)/;\n\n/**\n * These directives are only read from a whole block comment, like `/* global foo *\\/`\n * or `/* eslint quotes: 1 *\\/`. Their markers are ordinary words, so they never count\n * as directives on a single line of a longer comment.\n */\nconst BLOCK_DIRECTIVE = /^\\s*(?:eslint\\s|globals?\\s|exported\\s|jshint\\s|jslint\\s|type\\s*:)/;\n\n/**\n * Legal comments are kept as they are. A comment is legal when it uses the same markers\n * as bundlers like esbuild (`/*!`, `//!`, `@license`, and `@preserve`), or when it is a\n * copyright notice like `Copyright (c) 2020 Foo`.\n */\nconst LEGAL_COMMENT = /^!|@license\\b|@preserve\\b|\\bcopyright\\s+(?:\\(c\\)|©|\\d{4})/i;\n\n/**\n * In Rust, `/*!` and `//!` start inner doc comments, so only the other markers make\n * a comment legal.\n */\nconst RUST_LEGAL_COMMENT = /@license\\b|@preserve\\b|\\bcopyright\\s+(?:\\(c\\)|©|\\d{4})/i;\n\n// a term of one or two words or a code span (stripped to two backticks), then a dash or colon and its definition\nconst DEFINITION = /^(?:``|[\\w$.[\\]]+(?:[ -][\\w$.[\\]]+)?)(?: [-–]|:) \\S/;\n\nconst BOX_DRAWING = /[\\u2500-\\u257F]/;\n\n// a Markdown table delimiter row like `--- | ---` or `|:--|--:|`, which needs no leading pipe\nconst TABLE_DELIMITER = /^\\|?\\s*:?-+:?\\s*(?:\\|\\s*:?-+:?\\s*)+\\|?$/;\n\nconst ARROWS_OR_RULES = /[▲▼◀▶→←↑↓]|-->|<--|\\+-{2,}|-{3,}|={4,}/;\n\n// two or more spaces after text, or a backslash that another backslash doesn't escape\nconst HARD_BREAK = /\\S {2,}$|(?<!\\\\)\\\\(?:\\\\\\\\)*$/;\n\n/**\n * Describes how a paragraph that the tool keeps as written is checked.\n *\n * - `code`: code that the parser accepts, which gets no checks besides banners\n * - `code-like`: a paragraph with a line that may be code, like one that ends with `{`, which gets\n * no style hints, since they would point at the code, but must still fit the width\n * - `layout`: documented layout syntax like a table row, which gets style hints but no verdicts\n * - `manual`: text that is or may be laid out by hand or may be markup, like a Markdown hard line\n * break, arrows, or a heading, which gets style hints and must still fit the width.\n */\ntype StructureTier = 'code' | 'code-like' | 'layout' | 'manual';\n\ninterface Structure {\n readonly tier: StructureTier;\n readonly reason: string;\n}\n\n/**\n * Whether a statement is something prose also parses as, like a single word (`Done`),\n * a code span, a word before a code span (`Returns \\`null\\``, a tagged template), or a\n * label (`Default: refuse`). A label that ends with a semicolon, like the commented-out\n * CSS declaration `display: none;`, is code.\n */\nfunction isProseLike(statement: Statement, source: string): boolean {\n if (statement.type === 'LabeledStatement') {\n return source[statement.end - 1] !== ';';\n }\n\n if (statement.type === 'EmptyStatement') {\n return true;\n }\n\n if (statement.type !== 'ExpressionStatement') {\n return false;\n }\n\n let { expression } = statement;\n\n while (expression.type === 'ParenthesizedExpression') {\n expression = expression.expression;\n }\n\n if (expression.type === 'TaggedTemplateExpression') {\n return expression.tag.type === 'Identifier';\n }\n\n return expression.type === 'Identifier' || expression.type === 'Literal' || expression.type === 'TemplateLiteral';\n}\n\nfunction parsesAsStatements(text: string): boolean {\n if (!/[()[\\]{}=;<>]/.test(text)) {\n return false;\n }\n\n // a generator body accepts `return`, `await`, and `yield`, which are common in commented-out code\n const source = `async function* comment() {\\n${text}\\n}`;\n const { program, errors } = parseSync('comment.tsx', source);\n const wrapper = program.body.at(0);\n\n if (errors.length > 0 || wrapper?.type !== 'FunctionDeclaration' || !wrapper.body) {\n return false;\n }\n\n const statements = wrapper.body.body;\n\n return statements.length > 0 && !statements.every((statement) => isProseLike(statement, source));\n}\n\nconst CODE_VALUES = new Set([\n 'Literal',\n 'ArrayExpression',\n 'ObjectExpression',\n 'CallExpression',\n 'NewExpression',\n 'ArrowFunctionExpression',\n 'FunctionExpression',\n 'UnaryExpression',\n 'MemberExpression',\n]);\n\n/**\n * A lone property whose value is a word or code span, like `Default: refuse`, reads as a label in\n * prose rather than as commented-out options.\n */\nfunction parsesAsProperties(text: string): boolean {\n if (!text.includes(':')) {\n return false;\n }\n\n const { program, errors } = parseSync('comment.ts', `({\\n${text}\\n})`);\n const statement = program.body.at(0);\n\n if (errors.length > 0 || program.body.length !== 1 || statement?.type !== 'ExpressionStatement') {\n return false;\n }\n\n let { expression } = statement;\n\n while (expression.type === 'ParenthesizedExpression') {\n expression = expression.expression;\n }\n\n if (expression.type !== 'ObjectExpression' || expression.properties.length === 0) {\n return false;\n }\n\n const { properties } = expression;\n\n if (!properties.every((property) => property.type === 'Property' && !property.shorthand && !property.method)) {\n return false;\n }\n\n return (\n properties.length > 1 ||\n text.trimEnd().endsWith(',') ||\n properties.some((property) => property.type === 'Property' && CODE_VALUES.has(property.value.type))\n );\n}\n\n/**\n * Prose almost never parses as code, since two words in a row are a syntax error.\n */\nfunction isCode(texts: readonly string[], language: Language): boolean {\n return language.isCode(texts.map((line) => line.replace(LIST_MARKER, '')).join('\\n'));\n}\n\n/**\n * A prose clause can end with a semicolon too, which is a style issue.\n * So `;` only ends code with code around it.\n */\nfunction isCodeLine(text: string): boolean {\n return (\n text.endsWith('{') ||\n (text.endsWith(';') && (/[()[\\]{}=<>]/.test(text) || !/\\s/.test(text))) ||\n /^[}\\])]/.test(text)\n );\n}\n\nconst JAVASCRIPT: Language = {\n name: 'javascript',\n isCode: (text) => parsesAsStatements(text) || parsesAsProperties(text),\n isCodeLine: (text) => {\n return (\n isCodeLine(text) ||\n /^(?:const|let|var)\\s+[\\w$]+\\s*[:=]/.test(text) ||\n /^(?:import|export)\\s.*(?:\\sfrom\\s|[{}])/.test(text)\n );\n },\n legal: LEGAL_COMMENT,\n blockDirective: BLOCK_DIRECTIVE,\n tags: true,\n joinsCodeSpans: true,\n};\n\nconst RUST: Language = {\n name: 'rust',\n isCode: (text) => isRustCode(text),\n isCodeLine: (text) => {\n return (\n isCodeLine(text) ||\n /^let\\s+(?:mut\\s+)?\\w+\\s*[:=]/.test(text) ||\n /^(?:pub(?:\\([\\w:\\s]+\\))?\\s+)?(?:const|static(?:\\s+mut)?)\\s+[A-Z_\\d]+\\s*:/.test(text) ||\n /^(?:pub(?:\\([\\w:\\s]+\\))?\\s+)?use\\s+[\\w:]*::/.test(text) ||\n /^#!?\\[/.test(text)\n );\n },\n legal: RUST_LEGAL_COMMENT,\n tags: false,\n joinsCodeSpans: false,\n};\n\n/**\n * Whether a paragraph parses as JavaScript code, like the check for Rust code in `isRustCode`.\n */\nexport function isJavaScriptCode(text: string): boolean {\n return JAVASCRIPT.isCode(text);\n}\n\nfunction languageOf(filename: string): Language {\n return filename.endsWith('.rs') ? RUST : JAVASCRIPT;\n}\n\nfunction layoutReason(prose: string, raw: string): string | undefined {\n if (prose.startsWith('|') || TABLE_DELIMITER.test(prose)) {\n return 'table row';\n }\n\n if (BOX_DRAWING.test(prose)) {\n return 'box drawing';\n }\n\n // a rule or border, like the `***` lines of a box, would be joined into the prose around it\n if (isBanner(raw)) {\n return 'repeated characters';\n }\n\n return undefined;\n}\n\n// a rule like `***`, `- - -`, or `___`\nconst THEMATIC_BREAK = /^(?:(?:\\*\\s*){3,}|(?:-\\s*){3,}|(?:_\\s*){3,})$/;\n\n/**\n * Headings, link definitions, HTML, quotes, and rules are markup, since joining them with the\n * next line would turn them into prose. That includes the start of a list item, like `- [x]: y`,\n * a quote like `>=`, an HTML comment like `<!--`, and a line of only `=` or `-`, which underlines\n * a heading. Prose can start a line like that too, like `<T> is the type` or `# of retries`,\n * so markup is kept as written, but it still has to fit the width.\n */\nfunction isMarkup(prose: string): boolean {\n const text = prose.replace(LIST_MARKER, '');\n\n return (\n /^#{1,6}(?:\\s|$)/.test(text) ||\n /^\\[[^\\]]+\\]:/.test(text) ||\n /^<(?:\\/?[a-z]|[!?])/i.test(text) ||\n text.startsWith('>') ||\n /^(?:=+|-+)$/.test(prose) ||\n THEMATIC_BREAK.test(prose)\n );\n}\n\nfunction mask(text: string): string {\n return text.replaceAll(/[^\\n]/g, 'x');\n}\n\nfunction maskCode(text: string): string {\n let masked = text.replaceAll(CODE_SPAN, mask);\n\n for (let previous = ''; previous !== masked; ) {\n previous = masked;\n masked = masked.replaceAll(/\\{[^{}]*\\}/g, mask);\n }\n\n return masked;\n}\n\n/**\n * Two spaces after a sentence are an old typing convention, and a gap on a single line is a typo,\n * so neither is a column.\n */\nfunction hasAlignedColumns(lines: readonly BodyLine[]): boolean {\n const masked = maskCode(lines.map((body) => body.text).join('\\n')).split('\\n');\n const seen = new Set<number>();\n\n for (const line of masked) {\n const columns = new Set([...line.matchAll(/[^\\s.!?][^\\S\\n]{2,}(?=\\S)/g)].map((gap) => gap.index + gap[0].length));\n\n if ([...columns].some((column) => seen.has(column))) {\n return true;\n }\n\n for (const column of columns) {\n seen.add(column);\n }\n }\n\n return false;\n}\n\nfunction structureOf(lines: readonly BodyLine[], language: Language): Structure | undefined {\n const trimmed = lines.map((line) => line.text.trim()).join('\\n');\n const texts = replaceKeepingLines(trimmed, CODE_SPAN, '``').split('\\n');\n\n /**\n * Types like `{string | null}`, links like `{@link a | b}`, and URLs hold pipes and dashes\n * that aren't layout.\n */\n const proseLines = stripNonProse(trimmed).split('\\n');\n\n // the parser only accepts a whole paragraph as code when it is code, since two words in a row are a syntax error\n if (\n isCode(\n lines.map((line) => line.text.trim()),\n language,\n )\n ) {\n return { tier: 'code', reason: 'code' };\n }\n\n let layout: string | undefined;\n let codeLike = false;\n\n // a hard break on the last line of a paragraph ends the paragraph instead, as in Markdown\n let manual = lines.slice(0, -1).some((line) => line.hardBreak) ? 'hard line break' : undefined;\n\n for (const [position, line] of texts.entries()) {\n const prose = proseLines[position].trim();\n\n layout ??= layoutReason(prose, lines[position]?.text ?? '');\n\n // a single line that looks like code may be prose, like one that starts with `import them from`\n if (language.isCodeLine(line.trim())) {\n codeLike = true;\n } else if (isMarkup(prose)) {\n manual ??= 'markup';\n } else if ((prose.match(/\\|/g)?.length ?? 0) >= 2) {\n manual ??= 'pipes';\n } else if (ARROWS_OR_RULES.test(prose)) {\n manual ??= 'arrows or rules';\n }\n }\n\n layout ??= hasAlignedColumns(lines) ? 'aligned columns' : undefined;\n\n if (layout) {\n return { tier: 'layout', reason: layout };\n }\n\n if (codeLike) {\n return { tier: 'code-like', reason: 'code-like line' };\n }\n\n /**\n * One term per line, like `width - the maximum width` above `height - the maximum height`, may be\n * a definition list.\n */\n if (texts.filter((text) => DEFINITION.test(text.trim())).length >= 2) {\n manual ??= 'definition-like lines';\n }\n\n return manual ? { tier: 'manual', reason: manual } : undefined;\n}\n\n/**\n * A Markdown code fence closes only with the character it opened with, repeated at least as\n * often. A tilde fence inside a backtick block, or three backticks inside a four-backtick\n * block, doesn't end it.\n */\nclass FenceTracker {\n private _open: string | undefined;\n\n isInside(): boolean {\n return this._open !== undefined;\n }\n\n next(text: string): boolean {\n const trimmed = text.trim();\n\n if (this._open === undefined) {\n this._open = FENCE.exec(trimmed)?.[0];\n\n return this._open !== undefined;\n }\n\n if (trimmed.startsWith(this._open) && new RegExp(`^\\\\${this._open[0]}+$`).test(trimmed)) {\n this._open = undefined;\n }\n\n return true;\n }\n}\n\nfunction segment(lines: readonly BodyLine[], language: Language): RawParagraph[] {\n const paragraphs: RawParagraph[] = [];\n const fence = new FenceTracker();\n\n let current: RawParagraph | undefined;\n let inCodeTag = false;\n\n const flush = (): void => {\n if (current) {\n while (current.lines.length > 1 && !current.lines.at(-1)?.text.trim()) {\n current.lines.pop();\n }\n\n paragraphs.push(current);\n\n current = undefined;\n }\n };\n\n const start = (kind: ParagraphKind, line: BodyLine, marker = '', reason?: string): void => {\n flush();\n\n current = { kind, lines: [line], marker, reason };\n };\n\n for (const line of lines) {\n const { text } = line;\n\n if (inCodeTag) {\n /**\n * A fence inside the example may hold a line like `@decorator()`, which only starts a tag\n * outside of it.\n */\n if (fence.isInside() || !JSDOC_TAG.test(text)) {\n fence.next(text);\n current?.lines.push(line);\n\n continue;\n }\n\n inCodeTag = false;\n }\n\n if (fence.isInside()) {\n current?.lines.push(line);\n fence.next(text);\n\n if (!fence.isInside()) {\n flush();\n }\n\n continue;\n }\n\n if (line.verbatim) {\n // a fence without a space after the comment prefix, like `//!```toml`, still opens a code block\n if (fence.next(text)) {\n start('code', line, '', 'code block');\n\n continue;\n }\n\n start('code', line, '', 'directive');\n flush();\n\n continue;\n }\n\n if (!text.trim()) {\n flush();\n\n continue;\n }\n\n if (fence.next(text)) {\n start('code', line, '', 'code block');\n\n continue;\n }\n\n if (/^\\s/.test(text)) {\n const holdsProse = current?.kind === 'list' || (current?.kind === 'tag' && !current.reason);\n const nested = holdsProse ? NESTED_LIST_MARKER.exec(text) : null;\n\n if (nested) {\n start('list', line, nested[0]);\n } else if (current && (current.kind === 'list' || current.kind === 'tag' || current.reason === 'indented')) {\n current.lines.push(line);\n } else {\n start('code', line, '', 'indented');\n }\n\n continue;\n }\n\n const tag = language.tags ? JSDOC_TAG.exec(text) : null;\n\n if (tag) {\n start('tag', line, '', PROSE_TAGS.has(tag[1]) ? undefined : `@${tag[1]} tag`);\n\n inCodeTag = CODE_TAGS.has(tag[1]);\n\n continue;\n }\n\n const marker = LIST_MARKER.exec(text);\n\n if (marker) {\n start('list', line, marker[0]);\n\n continue;\n }\n\n // like Markdown, an unindented line right after a list item lazily continues it\n if (current && (current.kind === 'prose' || current.kind === 'tag' || current.kind === 'list')) {\n current.lines.push(line);\n\n continue;\n }\n\n start('prose', line);\n }\n\n flush();\n\n return paragraphs;\n}\n\nfunction startsLine(token: string, next: string | undefined): boolean {\n return startsBlock(token) || LINE_DIRECTIVE.test(next === undefined ? token : `${token} ${next}`);\n}\n\nfunction visualLength(text: string, tabWidth: number): number {\n return text.replaceAll('\\t', ' '.repeat(tabWidth)).length;\n}\n\nfunction sourceLineLength(line: BodyLine, tabWidth: number): number {\n // the text holds the indentation for comments without a prefix, like `other` block comments\n return visualLength(`${line.prefix}${line.text.trimEnd()}`, tabWidth);\n}\n\n/**\n * In legacy width/tolerance mode, an indivisible token is exempt because wrapping cannot\n * make it fit.\n */\nfunction overflowIssues(lines: readonly BodyLine[], options: AnalyzeOptions): IssueReport[] {\n const issues: IssueReport[] = [];\n const limit = options.maxWidth ?? options.width + options.tolerance;\n\n for (const line of lines) {\n const length = sourceLineLength(line, options.tabWidth);\n\n if (length <= limit) {\n continue;\n }\n\n if (options.maxWidth !== undefined) {\n issues.push(lineIssue('overflow', line, options));\n\n continue;\n }\n\n const prefixLength = visualLength(`${line.prefix}${/^\\s*/.exec(line.text)?.[0] ?? ''}`, options.tabWidth);\n const tokens = bindLineStarts(tokenize(line.text), startsLine);\n\n if (tokens.length > 1 && tokens.every((token) => prefixLength + token.length <= limit)) {\n issues.push(lineIssue('overflow', line, options));\n }\n }\n\n return issues;\n}\n\nfunction lineIssue(type: IssueType, line: BodyLine, options: AnalyzeOptions): IssueReport {\n return { type, tier: ISSUE_TIERS[type], line: line.index + 1, length: sourceLineLength(line, options.tabWidth) };\n}\n\nfunction stripBlockSyntax(text: string): string {\n return text\n .replace(/^\\s*\\/\\*+/, '')\n .replace(/\\*+\\/\\s*$/, '')\n .replace(/^\\s*\\*(?=\\s|$)/, '');\n}\n\nfunction bannerIssues(paragraph: RawParagraph, options: AnalyzeOptions): IssueReport[] {\n const verbatim = ['code block', 'indented', 'legal comment'];\n\n if (!options.style || (paragraph.reason !== undefined && verbatim.includes(paragraph.reason))) {\n return [];\n }\n\n const banners = paragraph.lines.filter((line) => {\n const text = paragraph.kind === 'other' ? stripBlockSyntax(line.text) : line.text;\n\n return isBanner(text) || (paragraph.kind === 'other' && /^\\s*\\/\\*{3,}|\\*{3,}\\/\\s*$/.test(line.text));\n });\n\n return banners.map((line) => lineIssue('banner', line, options));\n}\n\nfunction styleIssues(paragraph: RawParagraph, options: AnalyzeOptions): IssueReport[] {\n if (!options.style) {\n return [];\n }\n\n const byIndex = new Map(paragraph.lines.map((line) => [line.index, line]));\n\n const lines = paragraph.lines.map((line) => {\n return { index: line.index, text: paragraph.kind === 'other' ? stripBlockSyntax(line.text) : line.text };\n });\n\n return findStyleIssues(lines).flatMap((issue) => {\n const line = byIndex.get(issue.index);\n\n return line ? [lineIssue(issue.type, line, options)] : [];\n });\n}\n\nfunction bindTagHead(tokens: readonly string[]): string[] {\n let end = 1;\n\n if (tokens[end]?.startsWith('{')) {\n end += 1;\n }\n\n if (NAMED_TAGS.has(tokens[0].slice(1)) && end < tokens.length) {\n end += 1;\n }\n\n if (tokens[end] === '-') {\n end += 1;\n }\n\n return [tokens.slice(0, end).join(' '), ...tokens.slice(end)];\n}\n\n/**\n * A paragraph without a structure, like a single-line block comment, is prose that just can't be\n * rewrapped, so it's checked like text laid out by hand.\n */\nfunction keptIssues(paragraph: RawParagraph, structure: Structure | undefined, options: AnalyzeOptions): IssueReport[] {\n const tier = structure?.tier ?? 'manual';\n\n return byLine([\n ...(tier === 'manual' || tier === 'code-like' ? overflowIssues(paragraph.lines, options) : []),\n ...(tier === 'code' || tier === 'code-like' ? [] : styleIssues(paragraph, options)),\n ...bannerIssues(paragraph, options),\n ]);\n}\n\nfunction byLine(issues: readonly IssueReport[]): IssueReport[] {\n const unique = new Map(issues.map((issue) => [`${issue.line}:${issue.type}`, issue]));\n\n return [...unique.values()].toSorted((a, b) => a.line - b.line);\n}\n\nfunction sameLayout(a: readonly (readonly string[])[], b: readonly (readonly string[])[]): boolean {\n return a.length === b.length && a.every((words, position) => words.join(' ') === b[position].join(' '));\n}\n\n/**\n * Returns the index of the first line that ends inside a token, like a link or a code span that\n * continues on the next line, or -1 when every line ends between two tokens. The lines hold the\n * text of the tokens.\n */\nfunction splitTokenLine(lines: readonly (readonly string[])[], tokens: readonly string[]): number {\n let next = 0;\n\n return lines.findIndex((words) => {\n const line = words.join(' ');\n\n let text = '';\n\n while (text.length < line.length && next < tokens.length) {\n text += `${text ? ' ' : ''}${tokens[next]}`;\n next += 1;\n }\n\n return text !== line;\n });\n}\n\nfunction analyzeParagraph(paragraph: RawParagraph, options: AnalyzeOptions, language: Language): ParagraphReport {\n const first = paragraph.lines[0];\n const last = paragraph.lines.at(-1) ?? first;\n const base = { kind: paragraph.kind, startLine: first.index + 1, endLine: last.index + 1 };\n\n if (paragraph.kind === 'code') {\n return {\n ...base,\n reflowable: false,\n reason: paragraph.reason,\n issues: bannerIssues(paragraph, options),\n suggestion: null,\n };\n }\n\n if (paragraph.kind === 'tag' && paragraph.reason) {\n const codeTag = CODE_TAGS.has(JSDOC_TAG.exec(first.text)?.[1] ?? '');\n const issues = codeTag ? [] : overflowIssues(paragraph.lines, options);\n\n return { ...base, reflowable: false, reason: paragraph.reason, issues, suggestion: null };\n }\n\n if (paragraph.kind === 'other') {\n const fence = new FenceTracker();\n\n const prose = {\n ...paragraph,\n lines: paragraph.lines.filter((line) => !fence.next(stripBlockSyntax(line.text))),\n };\n\n const structure = structureOf(\n prose.lines.map((line) => {\n return { ...line, text: stripBlockSyntax(line.text) };\n }),\n language,\n );\n\n const issues = keptIssues(prose, structure, options);\n\n return { ...base, reflowable: false, reason: paragraph.reason, issues, suggestion: null };\n }\n\n const keep = (structure: Structure): ParagraphReport => {\n const issues = keptIssues(paragraph, structure, options);\n\n return { ...base, reflowable: false, reason: structure.reason, issues, suggestion: null };\n };\n\n const structure = structureOf(paragraph.lines, language);\n\n if (structure) {\n return keep(structure);\n }\n\n const prefix = first.prefix;\n const prefixLength = visualLength(prefix, options.tabWidth);\n const hang = ' '.repeat(paragraph.marker.length);\n const firstLimit = options.width - prefixLength - paragraph.marker.length;\n const restLimit = options.width - prefixLength - hang.length;\n\n const currentLines = paragraph.lines.map((line, position) =>\n bindLineStarts(tokenize(position === 0 ? line.text.slice(paragraph.marker.length) : line.text), startsLine),\n );\n\n const tokens = tokenize(currentLines.flat().join(' '));\n\n const layoutOptions = {\n ...options,\n tolerance:\n options.maxWidth === undefined\n ? options.tolerance\n : Math.min(options.tolerance, options.maxWidth - options.width),\n parentheticalOverflow: options.maxWidth === undefined ? undefined : options.maxWidth - options.width,\n keepFirstLine: paragraph.kind === 'tag',\n };\n\n const layOut = (from: readonly string[], settings: LayoutOptions, extra: number) => {\n const even = layoutTokens(from, firstLimit + extra, restLimit + extra, settings);\n\n return { even, wrapped: breakAfterSentence(even, firstLimit + extra, restLimit + extra, settings) };\n };\n\n const units = bindLineStarts(paragraph.kind === 'tag' ? bindTagHead(tokens) : tokens, startsLine);\n\n // the first and the other lines have the same limit, since the hanging indent is as long as the marker\n const hardLimit = restLimit + (options.maxWidth === undefined ? options.tolerance : options.maxWidth - options.width);\n const written = currentLines.map((words) => ` ${words.join(' ')} `);\n\n /**\n * Text that stays on one line, like a code span or a link, may only be longer than the limit when\n * it already is on one line, so a rewrap never adds a line that is too long.\n */\n const staysInLimit = (unit: string): boolean => {\n return unit.length <= hardLimit || written.some((line) => line.includes(` ${unit} `));\n };\n\n // a code span, link, or `{...}` group that continues on the next line would be joined into one line that is too long\n if (!units.every((unit) => staysInLimit(unit))) {\n return keep({ tier: 'manual', reason: 'unbreakable text across lines' });\n }\n\n const text = currentLines.map((words) => words.join(' ')).join('\\n');\n\n if (!language.joinsCodeSpans && [...text.matchAll(CODE_SPAN)].some((span) => span[0].includes('\\n'))) {\n return keep({ tier: 'manual', reason: 'code span across lines' });\n }\n\n const bodyOf = (lines: readonly (readonly string[])[]): BodyLine[] => {\n return lines.map((words, position) => {\n const text = `${position === 0 ? paragraph.marker : hang}${words.join(' ')}`;\n\n return { ...first, text, verbatim: false, hardBreak: HARD_BREAK.test(text) };\n });\n };\n\n /**\n * Analyzing the rewrap must find the same prose, so when a new line's start or end would read as\n * structure, like two lines that start with `term - `, the first word of the line that tips it\n * moves up a line. Without such a layout, or when the text reads as structure on a single line,\n * no rewrap reads as the same prose, and the paragraph is kept as written. The lines get `extra`\n * columns more than the width allows.\n */\n const settle = (settings: LayoutOptions, extra = 0): { even: string[][]; wrapped: string[][] } | Structure => {\n let current = units;\n let { even, wrapped } = layOut(current, settings, extra);\n\n for (;;) {\n const body = bodyOf(wrapped);\n const rewrap = structureOf(body, language);\n\n if (!rewrap) {\n return { even, wrapped };\n }\n\n // the first line has no line to move a word up to, so the first word of the second line joins it instead\n const tipping = body.findIndex((_, position) => structureOf(body.slice(0, position + 1), language) !== undefined);\n const start = wrapped.slice(0, Math.max(tipping, 1)).flat().length;\n const pair = `${current[start - 1]} ${current[start]}`;\n\n if (body.length < 2 || !staysInLimit(pair)) {\n const reading = rewrap.tier === 'code-like' ? 'code' : rewrap.reason;\n\n return { tier: 'manual', reason: `rewrap would read as ${reading}` };\n }\n\n current = current.toSpliced(start - 1, 2, pair);\n ({ even, wrapped } = layOut(current, settings, extra));\n }\n };\n\n /**\n * Lays the paragraph out for a width that is `extra` columns wider. The columns come out of the\n * tolerance and out of the room that parentheticals have, so the hard limit stays where it is.\n */\n const layoutAt = (extra: number): { even: string[][]; wrapped: string[][] } | Structure => {\n const settings = {\n ...layoutOptions,\n tolerance: layoutOptions.tolerance - extra,\n parentheticalOverflow:\n layoutOptions.parentheticalOverflow === undefined ? undefined : layoutOptions.parentheticalOverflow - extra,\n };\n\n const settled = settle(settings, extra);\n\n // the extra lines that keep a line from sticking out can read as structure where the fewest lines don't\n return 'wrapped' in settled ? settled : settle({ ...settings, keepSteps: true }, extra);\n };\n\n const layout = layoutAt(0);\n\n if (!('wrapped' in layout)) {\n return keep(layout);\n }\n\n const { even, wrapped } = layout;\n const matchesLayout = sameLayout(wrapped, currentLines);\n const isVerdict = (issue: LineIssue): boolean => ISSUE_TIERS[issue.type] === 'verdict';\n\n const lineIssues: LineIssue[] = [];\n const overflow: IssueReport[] = options.maxWidth === undefined ? [] : overflowIssues(paragraph.lines, options);\n\n if (!matchesLayout) {\n const hasExtraLines = currentLines.length > wrapped.length;\n\n if (options.maxWidth === undefined) {\n overflow.push(...overflowIssues(paragraph.lines, options));\n }\n\n lineIssues.push(\n ...findLineIssues(currentLines, firstLimit, restLimit, layoutOptions).filter(\n (issue) => issue.type !== 'overflow' && (issue.type !== 'underfill' || hasExtraLines),\n ),\n );\n\n if (hasWidow(currentLines) && !hasWidow(wrapped)) {\n lineIssues.push({ type: 'widow', line: currentLines.length - 1 });\n }\n\n /**\n * The agent may keep a word that a possible orphan is about where it is, so a layout that only\n * differs from the suggestion by those words is no verdict. Anything else about it still is.\n */\n const keepsOnlyOrphans = (): boolean => {\n const free = settle({ ...layoutOptions, avoidSentenceOrphans: false });\n\n return !('wrapped' in free) || sameLayout(free.wrapped, currentLines);\n };\n\n const keepsOnlySentenceStarts = (): boolean => {\n const free = settle({ ...layoutOptions, ignoreSentenceStarts: true });\n\n return 'wrapped' in free && (sameLayout(free.even, currentLines) || sameLayout(free.wrapped, currentLines));\n };\n\n /**\n * Whether a parenthetical stays on one line is for the agent to decide, so a layout that\n * matches the suggestion up to a line that ends inside a parenthetical that the suggestion\n * keeps whole, and the rewrap of the rest after it, is no verdict.\n */\n const splitsOnlyParenthetical = (line: number): boolean => {\n if (layoutOptions.parentheticalOverflow === undefined) {\n return false;\n }\n\n const tokenCount = (lines: readonly (readonly string[])[]) => tokenize(lines.flat().join(' ')).length;\n const end = tokenCount(currentLines.slice(0, line + 1));\n const group = parentheticalAround(tokens, end, restLimit + layoutOptions.parentheticalOverflow);\n\n if (!group) {\n return false;\n }\n\n const keepsGroup = wrapped.every((_, position) => {\n const breakAt = tokenCount(wrapped.slice(0, position + 1));\n\n return breakAt <= group.start || breakAt >= group.end;\n });\n\n if (!keepsGroup) {\n return false;\n }\n\n const restOptions = { ...layoutOptions, keepFirstLine: false };\n const rest = bindLineStarts(tokens.slice(end), startsLine);\n const restLines = layoutTokens(rest, restLimit, restLimit, restOptions);\n\n return sameLayout(breakAfterSentence(restLines, restLimit, restLimit, restOptions), currentLines.slice(line + 1));\n };\n\n if (overflow.length === 0 && (currentLines.length === wrapped.length || options.maxWidth !== undefined)) {\n const line = Math.max(\n wrapped.findIndex((words, position) => words.join(' ') !== currentLines[position]?.join(' ')),\n 0,\n );\n\n /**\n * Lines that only miss the break after a sentence, or end with the first words of one, are a\n * hint, since a heuristic decides where a sentence ends.\n */\n if (lineIssues.length === 0 && (sameLayout(even, currentLines) || keepsOnlySentenceStarts())) {\n lineIssues.push({ type: 'orphan', line });\n } else if (lineIssues.length === 0) {\n lineIssues.push({ type: splitsOnlyParenthetical(line) ? 'split-parenthetical' : 'unbalanced', line });\n } else if (\n lineIssues.every((issue) => issue.type === 'orphan') &&\n !sameLayout(even, currentLines) &&\n !keepsOnlyOrphans()\n ) {\n lineIssues.push({ type: 'unbalanced', line });\n }\n }\n\n /**\n * The rewrap keeps a link, a code span, and a `{...}` group on one line. A line that ends\n * inside one is a verdict whatever else holds for the paragraph, so the rewrap always joins it.\n */\n const splitLine = splitTokenLine(currentLines, tokens);\n\n if (splitLine !== -1 && overflow.length === 0 && !lineIssues.some((issue) => isVerdict(issue))) {\n lineIssues.push({ type: 'unbalanced', line: splitLine });\n }\n }\n\n /**\n * The tolerance is room past the width, so a paragraph that already has the layout for a width\n * within the tolerance is fine as it is.\n */\n const matchesWiderLayout = (): boolean => {\n for (let extra = 1; extra <= layoutOptions.tolerance; extra += 1) {\n const wider = layoutAt(extra);\n\n if ('wrapped' in wider && sameLayout(wider.wrapped, currentLines)) {\n return true;\n }\n }\n\n return false;\n };\n\n /**\n * The tolerance holds for each line on its own too, in both directions. A paragraph with as many\n * lines as the suggestion is fine as it is when no line is more than the tolerance shorter or\n * longer than the same line of the suggestion. A longer line still ends within the tolerance past\n * the width, unless the line of the suggestion already ends past it. No line may end inside text\n * that the rewrap keeps on one line, like a link or the head of a JSDoc tag.\n */\n const staysNearLayout = (): boolean => {\n if (currentLines.length !== wrapped.length || splitTokenLine(currentLines, units) !== -1) {\n return false;\n }\n\n return currentLines.every((words, position) => {\n const length = words.join(' ').length;\n const suggestedLength = wrapped[position].join(' ').length;\n const limit = (position === 0 ? firstLimit : restLimit) + layoutOptions.tolerance;\n\n return (\n Math.abs(length - suggestedLength) <= layoutOptions.tolerance && length <= Math.max(suggestedLength, limit)\n );\n });\n };\n\n const hasVerdict = lineIssues.some((issue) => isVerdict(issue));\n\n /**\n * Lines that are close to the suggestion are even enough, which says nothing about a widow, and a\n * possible orphan in a paragraph that is `unbalanced` too is still for the rewrap to move.\n */\n const onlyUnbalanced = lineIssues.every((issue) => issue.type === 'unbalanced');\n\n /**\n * A paragraph that only has hints keeps them and its suggestion, since a hint is for the agent to\n * decide. Only verdicts go, and the suggestion then keeps the lines as they are.\n */\n const accepted = hasVerdict && (matchesWiderLayout() || (onlyUnbalanced && staysNearLayout()));\n const suggested = accepted ? currentLines : wrapped;\n\n const suggestion = suggested.map(\n (words, position) => `${prefix}${position === 0 ? paragraph.marker : hang}${words.join(' ')}`,\n );\n\n const wrapIssues = lineIssues\n .filter((issue) => !accepted || !isVerdict(issue))\n .map((issue) => lineIssue(issue.type, paragraph.lines[issue.line], options));\n\n const issues = byLine([...overflow, ...wrapIssues, ...styleIssues(paragraph, options)]);\n\n return { ...base, reflowable: true, issues, suggestion };\n}\n\nconst LINE_COMMENT = 'line comment';\n\n/**\n * Rewrapping a line comment would make a run of `//` lines, so the tool keeps it as written and\n * only reports the lines that are longer than the maximum. Code and layout syntax are exempt,\n * like in other text kept as written.\n */\nfunction analyzeLineParagraph(paragraph: RawParagraph, options: AnalyzeOptions, language: Language): ParagraphReport {\n const first = paragraph.lines[0];\n const last = paragraph.lines.at(-1) ?? first;\n const base = { kind: paragraph.kind, startLine: first.index + 1, endLine: last.index + 1 };\n\n if (paragraph.kind === 'code') {\n return { ...base, reflowable: false, reason: paragraph.reason, issues: [], suggestion: null };\n }\n\n const tier = structureOf(paragraph.lines, language)?.tier;\n const issues = tier === 'code' || tier === 'layout' ? [] : overflowIssues(paragraph.lines, options);\n\n return { ...base, reflowable: false, reason: LINE_COMMENT, issues, suggestion: null };\n}\n\nfunction computeLineStarts(source: string): number[] {\n const starts = [0];\n\n for (let index = 0; index < source.length; index += 1) {\n if (source.codePointAt(index) === 0x0a) {\n starts.push(index + 1);\n }\n }\n\n return starts;\n}\n\nfunction lineOf(lineStarts: readonly number[], offset: number): number {\n let low = 0;\n let high = lineStarts.length - 1;\n\n while (low < high) {\n const middle = Math.ceil((low + high) / 2);\n\n if (lineStarts[middle] <= offset) {\n low = middle;\n } else {\n high = middle - 1;\n }\n }\n\n return low;\n}\n\ninterface CommentShape {\n readonly kind: CommentKind;\n readonly startLine: number;\n readonly endLine: number;\n readonly paragraphs: RawParagraph[];\n}\n\nfunction stripCarriageReturn(line: string): string {\n return line.endsWith('\\r') ? line.slice(0, -1) : line;\n}\n\nfunction blockKind(comment: ParsedComment, language: Language): CommentKind {\n if (language.name === 'rust') {\n return comment.doc ? `${comment.doc}-block-doc` : 'block';\n }\n\n return comment.value.startsWith('*') ? 'jsdoc' : 'block';\n}\n\nfunction wholeComment(\n shape: Omit<CommentShape, 'paragraphs'>,\n lines: readonly string[],\n paragraphKind: ParagraphKind,\n reason: string,\n): CommentShape {\n const otherLines = lines.map((text, offset) => {\n return { index: shape.startLine + offset, prefix: '', text, verbatim: false, hardBreak: false };\n });\n\n return { ...shape, paragraphs: [{ kind: paragraphKind, marker: '', reason, lines: otherLines }] };\n}\n\n// a code span only names a marker, like the `@license` in a comment that explains legal comments\nfunction isLegal(value: string, language: Language): boolean {\n return language.legal.test(replaceKeepingLines(value, CODE_SPAN, '``'));\n}\n\nfunction shapeBlock(\n comment: ParsedComment,\n sourceLines: readonly string[],\n lineStarts: readonly number[],\n language: Language,\n): CommentShape {\n const startLine = lineOf(lineStarts, comment.start);\n const endLine = lineOf(lineStarts, comment.end - 1);\n const shape = { kind: blockKind(comment, language), startLine, endLine };\n const lines = sourceLines.slice(startLine, endLine + 1);\n\n // the `!` of a Rust inner doc comment marks it like the second `*` of a JSDoc comment\n const value = comment.doc ? comment.value.slice(1) : comment.value.replace(/^\\*/, '');\n\n if (isLegal(comment.value, language)) {\n return wholeComment(shape, lines, 'code', 'legal comment');\n }\n\n if (LINE_DIRECTIVE.test(value) || language.blockDirective?.test(value)) {\n return wholeComment(shape, lines, 'code', 'directive');\n }\n\n const other = (): CommentShape => {\n return wholeComment(\n shape,\n lines,\n 'other',\n startLine === endLine ? 'single-line block comment' : 'unsupported block comment layout',\n );\n };\n\n if (startLine === endLine || !/^\\s*\\/\\*[*!]?$/.test(lines[0]) || !/^\\s*\\*\\/$/.test(lines.at(-1) ?? '')) {\n return other();\n }\n\n const body: BodyLine[] = [];\n const inner = lines.slice(1, -1);\n\n for (const [offset, line] of inner.entries()) {\n const match = /^(\\s*\\*)( ?)(.*)$/.exec(line);\n\n if (!match) {\n return other();\n }\n\n const [, star, space, text] = match;\n\n body.push({\n index: startLine + 1 + offset,\n prefix: `${star} `,\n text: text.trimEnd(),\n verbatim: (!space && text.length > 0) || LINE_DIRECTIVE.test(text),\n hardBreak: HARD_BREAK.test(text),\n });\n }\n\n return { ...shape, paragraphs: segment(body, language) };\n}\n\n/**\n * Shapes a run of Rust line doc comments, like `///` or `//!` on consecutive lines, as one comment.\n * Each line holds a prefix like the ` * ` of a block comment.\n */\nfunction shapeLineRun(\n group: readonly ParsedComment[],\n sourceLines: readonly string[],\n lineStarts: readonly number[],\n language: Language,\n): CommentShape {\n const startLine = lineOf(lineStarts, group[0].start);\n const endLine = startLine + group.length - 1;\n const kind: CommentKind = group[0].doc === 'inner' ? 'inner-line-doc' : 'outer-line-doc';\n const shape = { kind, startLine, endLine };\n const lines = sourceLines.slice(startLine, endLine + 1);\n const values = group.map((comment) => comment.value.slice(1));\n\n if (isLegal(values.join('\\n'), language)) {\n return wholeComment(shape, lines, 'code', 'legal comment');\n }\n\n if (LINE_DIRECTIVE.test(values[0])) {\n return wholeComment(shape, lines, 'code', 'directive');\n }\n\n const body = lines.map((line, offset): BodyLine => {\n const [, marker, space, text] = /^(\\s*\\/\\/[/!])( ?)(.*)$/.exec(line) ?? ['', '', '', line];\n\n return {\n index: startLine + offset,\n prefix: `${marker} `,\n text: text.trimEnd(),\n verbatim: (!space && text.length > 0) || LINE_DIRECTIVE.test(text),\n hardBreak: HARD_BREAK.test(text),\n };\n });\n\n return { ...shape, paragraphs: segment(body, language) };\n}\n\n/**\n * Shapes a run of plain line comments (`//`) on consecutive lines as one comment. Each line is\n * measured with its own prefix, like `//` without a space or the `///` of a TypeScript directive.\n * Only the lines with a tool directive are directives, since a directive in a line comment covers\n * its own line.\n */\nfunction shapeLineComment(\n group: readonly ParsedComment[],\n sourceLines: readonly string[],\n lineStarts: readonly number[],\n language: Language,\n): CommentShape {\n const startLine = lineOf(lineStarts, group[0].start);\n const endLine = startLine + group.length - 1;\n const shape = { kind: 'line' as const, startLine, endLine };\n const lines = sourceLines.slice(startLine, endLine + 1);\n\n if (isLegal(group.map((comment) => comment.value).join('\\n'), language)) {\n return wholeComment(shape, lines, 'code', 'legal comment');\n }\n\n const body = lines.map((line, offset): BodyLine => {\n const [, marker, space, text] = /^(\\s*\\/\\/[/!]*)( ?)(.*)$/.exec(line) ?? ['', '', '', line];\n\n return {\n index: startLine + offset,\n prefix: `${marker}${space}`,\n text: text.trimEnd(),\n verbatim: LINE_DIRECTIVE.test(text),\n hardBreak: HARD_BREAK.test(text),\n };\n });\n\n // line comments have no JSDoc tags, so a line that starts with `@` is prose\n return { ...shape, paragraphs: segment(body, { ...language, tags: false }) };\n}\n\nfunction ownsLines(comment: ParsedComment, source: string, lineStarts: readonly number[]): boolean {\n const startLine = lineOf(lineStarts, comment.start);\n const before = source.slice(lineStarts[startLine], comment.start);\n const endLine = lineOf(lineStarts, comment.end - 1);\n const lineEnd = lineStarts[endLine + 1] ?? source.length;\n const after = source.slice(comment.end, lineEnd);\n\n return /^[\\t ]*$/.test(before) && /^[\\t ]*\\r?\\n?$/.test(after);\n}\n\n/**\n * Only a single-line block comment or prose without structure can stay on one line past the width.\n */\nfunction singleLineLength(paragraph: RawParagraph, options: AnalyzeOptions, language: Language): number {\n const [first] = paragraph.lines;\n\n if (paragraph.kind === 'other' && paragraph.reason === 'single-line block comment') {\n return sourceLineLength(first, options.tabWidth);\n }\n\n if (paragraph.kind !== 'prose' || structureOf(paragraph.lines, language)) {\n return Infinity;\n }\n\n const text = tokenize(paragraph.lines.map((line) => line.text).join(' ')).join(' ');\n\n return visualLength(first.prefix, options.tabWidth) + text.length;\n}\n\nexport const BYTE_ORDER_MARK = '\\uFEFF';\n\n/**\n * Most toolchains accept JSX in a `.js` file, and a file without JSX parses the same either way.\n */\nfunction parseSource(filename: string, source: string): ReturnType<typeof parseSync> {\n return parseSync(filename, source, filename.endsWith('.js') ? { lang: 'jsx' } : undefined);\n}\n\nfunction describeFailure(errors: readonly OxcError[], lineStarts: readonly number[]): SyntaxFailure {\n const [first] = errors;\n const offset = first.labels.at(0)?.start;\n\n return { message: first.message, line: offset === undefined ? undefined : lineOf(lineStarts, offset) + 1 };\n}\n\ninterface ScannedSource {\n readonly comments: readonly ParsedComment[];\n readonly syntaxError?: SyntaxFailure;\n}\n\nfunction scanJavaScript(filename: string, text: string, lineStarts: readonly number[]): ScannedSource {\n const { program, comments, errors } = parseSource(filename, text);\n\n /**\n * The parser returns an empty program when it stops at a syntax error, and it has only seen\n * the comments before that error. An error that it recovers from, like a `return` outside of\n * a function, leaves the program and its comments complete.\n */\n if (errors.length > 0 && program.body.length === 0) {\n return { comments: [], syntaxError: describeFailure(errors, lineStarts) };\n }\n\n // drop a leading hashbang, which some parser versions report as a comment\n return {\n comments: (comments as ParsedComment[]).filter((comment) => !(comment.start === 0 && text.startsWith('#!'))),\n };\n}\n\n/**\n * Rust is read by rustc's lexer, which never stops early. A token that never ends, like an\n * unterminated block comment or string, holds every comment after it, so such a source has\n * no comments, like a JavaScript source that the parser stops at.\n */\nfunction scanRustSource(text: string, lineStarts: readonly number[]): ScannedSource {\n const { comments, error } = scanRust(text);\n\n if (error) {\n return { comments: [], syntaxError: { message: error.message, line: lineOf(lineStarts, error.offset) + 1 } };\n }\n\n return {\n comments: comments.map((comment): ParsedComment => {\n return {\n type: comment.block ? 'Block' : 'Line',\n value: text.slice(comment.start + 2, comment.block ? comment.end - 2 : comment.end),\n start: comment.start,\n end: comment.end,\n doc: comment.doc,\n };\n }),\n };\n}\n\n/**\n * Groups the line comments that own their lines into runs of consecutive lines with the same\n * marker and indentation, since each run reads as one comment. `markerOf` returns `undefined`\n * for a comment that belongs to no run, which ends the run before it.\n */\nfunction groupLineRuns(\n comments: readonly ParsedComment[],\n text: string,\n lineStarts: readonly number[],\n markerOf: (comment: ParsedComment) => string | undefined,\n): ParsedComment[][] {\n const groups: ParsedComment[][] = [];\n\n let current: ParsedComment[] | undefined;\n let previousLine = -1;\n let indent = '';\n let marker: string | undefined;\n\n for (const comment of comments) {\n const next = comment.type === 'Line' ? markerOf(comment) : undefined;\n\n if (next === undefined || !ownsLines(comment, text, lineStarts)) {\n current = undefined;\n\n continue;\n }\n\n const line = lineOf(lineStarts, comment.start);\n const before = text.slice(lineStarts[line], comment.start);\n\n if (current && line === previousLine + 1 && next === marker && before === indent) {\n current.push(comment);\n } else {\n current = [comment];\n indent = before;\n marker = next;\n\n groups.push(current);\n }\n\n previousLine = line;\n }\n\n return groups;\n}\n\n/**\n * Finds all own-line block and JSDoc comments in a JavaScript or TypeScript source file, or block\n * and doc comments in a Rust source file (`.rs`), and reports how each paragraph deviates from a\n * greedy wrap at `options.width`. A run of Rust line doc comments (`///` or `//!`) counts as one\n * comment. A run of other line comments (`//`) on their own lines is never rewrapped, and it is\n * only reported when a line is longer than the maximum. A source with a syntax error that stops\n * the parser has no reports.\n */\nexport function analyzeSource(source: string, filename: string, options: AnalyzeOptions): CommentReport[] {\n return inspectSource(source, filename, options).comments;\n}\n\n/**\n * Analyzes a source like `analyzeSource`, and also tells a source without comments apart from one\n * that the parser can't read.\n */\nexport function inspectSource(source: string, filename: string, options: AnalyzeOptions): SourceAnalysis {\n if (options.maxWidth !== undefined && (!Number.isInteger(options.maxWidth) || options.maxWidth < options.width)) {\n throw new Error('comments: maxWidth must be an integer at least as large as width');\n }\n\n // a byte order mark is no text, so a comment on the first line owns its lines and is measured without the mark\n const text = source.startsWith(BYTE_ORDER_MARK) ? source.slice(1) : source;\n const language = languageOf(filename);\n const lineStarts = computeLineStarts(text);\n\n const scanned =\n language.name === 'rust' ? scanRustSource(text, lineStarts) : scanJavaScript(filename, text, lineStarts);\n\n if (scanned.syntaxError) {\n return { comments: [], syntaxError: scanned.syntaxError };\n }\n\n const sourceLines = text.split('\\n').map((line) => stripCarriageReturn(line));\n const reports: CommentReport[] = [];\n\n const report = (shape: CommentShape, lines: readonly string[], paragraphs: ParagraphReport[]): CommentReport => {\n return {\n file: filename,\n kind: shape.kind,\n startLine: shape.startLine + 1,\n endLine: shape.endLine + 1,\n indent: /^[\\t ]*/.exec(lines[0])?.[0] ?? '',\n width: options.width,\n maxWidth: options.maxWidth,\n lengths: lines.map((line) => visualLength(line.trimEnd(), options.tabWidth)),\n text: lines.join('\\n'),\n paragraphs,\n };\n };\n\n const blocks = scanned.comments.filter((comment) => {\n return (\n comment.type === 'Block' && ownsLines(comment, text, lineStarts) && !comment.value.includes(IGNORE_DIRECTIVE)\n );\n });\n\n const runs = (markerOf: (comment: ParsedComment) => string | undefined): ParsedComment[][] => {\n return groupLineRuns(scanned.comments, text, lineStarts, markerOf).filter((group) => {\n return !group.some((comment) => comment.value.includes(IGNORE_DIRECTIVE));\n });\n };\n\n const shapes = [\n ...blocks.map((block) => shapeBlock(block, sourceLines, lineStarts, language)),\n ...runs((comment) => comment.doc).map((group) => shapeLineRun(group, sourceLines, lineStarts, language)),\n ...runs((comment) => (comment.doc ? undefined : '//')).map((group) =>\n shapeLineComment(group, sourceLines, lineStarts, language),\n ),\n ].toSorted((a, b) => a.startLine - b.startLine);\n\n for (const shape of shapes) {\n const lines = sourceLines.slice(shape.startLine, shape.endLine + 1);\n\n if (shape.kind === 'line') {\n const paragraphs = shape.paragraphs.map((paragraph) => analyzeLineParagraph(paragraph, options, language));\n\n // a line comment only has overflow to report, so one that fits is no comment of interest\n if (paragraphs.some((paragraph) => paragraph.issues.length > 0)) {\n reports.push(report(shape, lines, paragraphs));\n }\n\n continue;\n }\n\n const singleLineWidth = Math.max(\n options.width,\n Math.min(options.singleLineWidth ?? options.width, options.maxWidth ?? Infinity),\n );\n\n // staying on one line always saves a line, so the tolerance applies on top of the single-line width\n const singleLineLimit = Math.min(singleLineWidth + options.tolerance, options.maxWidth ?? Infinity);\n\n const singleLine =\n shape.paragraphs.length === 1 ? singleLineLength(shape.paragraphs[0], options, language) : Infinity;\n\n const paragraphOptions =\n singleLine <= singleLineLimit ? { ...options, width: Math.max(singleLineWidth, singleLine) } : options;\n\n reports.push(\n report(\n shape,\n lines,\n shape.paragraphs.map((paragraph) => analyzeParagraph(paragraph, paragraphOptions, language)),\n ),\n );\n }\n\n return { comments: reports };\n}\n\n/**\n * Whether the comment has any issue, including hints, which makes it worth handing to the agent.\n */\nexport function isSuspicious(report: CommentReport): boolean {\n return report.paragraphs.some((paragraph) => paragraph.issues.length > 0);\n}\n\n/**\n * Whether the comment has an issue that isn't a hint, so it breaks the rules no matter what\n * the agent decided.\n */\nexport function hasVerdictIssues(report: CommentReport): boolean {\n return report.paragraphs.some((paragraph) => paragraph.issues.some((issue) => issue.tier === 'verdict'));\n}\n\n/**\n * Returns the comment's source text with every prose paragraph replaced by its suggestion.\n * Code, tables, diagrams, and similar paragraphs are kept as they are.\n */\nexport function rewrapComment(report: CommentReport): string {\n const lines = report.text.split('\\n');\n\n const paragraphs = report.paragraphs\n .filter((paragraph) => paragraph.suggestion !== null)\n .toSorted((a, b) => b.startLine - a.startLine);\n\n for (const paragraph of paragraphs) {\n const start = paragraph.startLine - report.startLine;\n\n lines.splice(start, paragraph.endLine - paragraph.startLine + 1, ...(paragraph.suggestion ?? []));\n }\n\n return lines.join('\\n');\n}\n\nconst POSITION_KEYS = new Set(['start', 'end', 'range', 'loc']);\n\n/**\n * Returns the syntax tree of the source as JSON, without positions or comments, used to verify that\n * only comments changed. Moving or rewording a comment leaves the tree the same, while any change to\n * a string, template, regex, JSX text, or where a statement ends changes it. A source that doesn't\n * parse has no tree to compare, so all of its text counts. A Rust source is compared by its tokens\n * instead, with each run of doc comments as one marker (see `rustFingerprint`).\n */\nexport function codeFingerprint(source: string, filename: string): string {\n if (languageOf(filename).name === 'rust') {\n return rustFingerprint(source);\n }\n\n const { program, errors } = parseSource(filename, source);\n\n if (errors.length > 0) {\n return JSON.stringify({ source, errors: errors.map((error) => error.message) });\n }\n\n // a BigInt literal's value is a `bigint`, which JSON can't hold, but its `raw` text already tells it apart\n return JSON.stringify(program, (key, value: unknown) =>\n POSITION_KEYS.has(key) || typeof value === 'bigint' ? undefined : value,\n );\n}\n","import { existsSync } from 'node:fs';\nimport { createRequire } from 'node:module';\nimport path from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nexport interface RustComment {\n /**\n * Holds the UTF-16 offset of the first character of the comment, like the `/` of `///`.\n */\n readonly start: number;\n\n /**\n * Holds the UTF-16 offset after the last character. A line comment ends before its line break.\n */\n readonly end: number;\n readonly block: boolean;\n readonly doc?: 'outer' | 'inner';\n}\n\nexport interface RustScan {\n readonly comments: readonly RustComment[];\n\n /**\n * Describes the first token that never ends, like an unterminated block comment or string.\n */\n readonly error?: { readonly message: string; readonly offset: number };\n}\n\ninterface NativeBinding {\n scanRust(source: string): RustScan;\n rustFingerprint(source: string): string;\n isRustCode(text: string): boolean;\n}\n\nconst BINARY_NAME = 'comments';\n\n/**\n * Lists the platform suffixes that `napi build --platform` gives the binary, in the order to try\n * them. A Linux binary is built for either glibc or musl, so both are candidates.\n */\nfunction platformSuffixes(): string[] {\n const { platform, arch } = process;\n\n switch (platform) {\n case 'darwin': {\n return [`darwin-${arch}`, 'darwin-universal'];\n }\n\n case 'linux': {\n return arch === 'arm'\n ? ['linux-arm-gnueabihf', 'linux-arm-musleabihf']\n : [`linux-${arch}-gnu`, `linux-${arch}-musl`];\n }\n\n case 'win32': {\n return [`win32-${arch}-msvc`, `win32-${arch}-gnu`];\n }\n\n default: {\n return [`${platform}-${arch}`];\n }\n }\n}\n\n/**\n * The module runs from `src/comments`, `dist`, or `dist/bin`, so the `native` directory is looked\n * up in the directories above it.\n */\nfunction findBinaries(): string[] {\n const names = platformSuffixes().map((suffix) => `${BINARY_NAME}.${suffix}.node`);\n\n for (let directory = path.dirname(fileURLToPath(import.meta.url)); ; directory = path.dirname(directory)) {\n const found = names.map((name) => path.join(directory, 'native', name)).filter((file) => existsSync(file));\n\n if (found.length > 0) {\n return found;\n }\n\n if (path.dirname(directory) === directory) {\n return [];\n }\n }\n}\n\nlet binding: NativeBinding | undefined;\n\nfunction loadBinding(): NativeBinding {\n if (binding) {\n return binding;\n }\n\n const require = createRequire(import.meta.url);\n const errors: string[] = [];\n\n for (const file of findBinaries()) {\n try {\n binding = require(file) as NativeBinding;\n\n return binding;\n } catch (error) {\n errors.push(`${file}: ${(error as Error).message}`);\n }\n }\n\n const reason = errors.length > 0 ? `, and loading it failed (${errors.join('; ')})` : '';\n\n throw new Error(\n `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\\`.`,\n );\n}\n\nexport function scanRust(source: string): RustScan {\n return loadBinding().scanRust(source);\n}\n\nexport function rustFingerprint(source: string): string {\n return loadBinding().rustFingerprint(source);\n}\n\nexport function isRustCode(text: string): boolean {\n return loadBinding().isRustCode(text);\n}\n","export type StyleIssueType = 'semicolon' | 'em-dash' | 'dash' | 'label-colon' | 'emphasis' | 'banner';\n\nexport interface StyleLine {\n /**\n * This is the 0-based line in the file.\n */\n readonly index: number;\n readonly text: string;\n}\n\nexport interface StyleIssue {\n readonly type: StyleIssueType;\n readonly index: number;\n}\n\n/**\n * Matches four or more repeated characters at the start or end of a line, since three like `===`,\n * `---`, or `~~~` are operators, rules, and fences in prose. A run in the middle of a line is a\n * table rule like `| ---- |` or an arrow like `---->`, and a Markdown heading like `#### Examples`\n * isn't a banner either.\n */\nconst BANNER = /^(?!#{1,6}\\s)([-=*#_~+/\\\\])\\1{3,}|([-=*#_~])\\2{3,}\\s*$/;\n\n/**\n * Runs of the same character separated only by spaces, like `-------- -----`, underline the\n * columns of a table.\n */\nconst COLUMN_RULE = /^([-=_~])\\1*(?:\\s+\\1+)+$/;\n\n/**\n * Matches an en-dash, a spaced hyphen, or a double hyphen between words, which may stand in for an\n * em-dash. An en-dash or hyphen between numbers, like `10 – 20`, is a range, and a double hyphen\n * next to a CLI flag, like `--watch -- file` or `npm test -- --watch`, separates arguments.\n */\nconst DASH = /(?<!\\d)\\s[–-]\\s(?!\\d)|(?<!(?:^|\\s)-[\\w-]*)\\s--\\s(?!-)/;\nconst SEMICOLON = /;(?:\\s|$)/;\n\n/**\n * Matches `*emphasis*` or `**bold**`, which starts with a letter or digit and whose\n * closing asterisks end the word, so that globs like `*foo*.ts` or `*.test.*` aren't\n * emphasis. Asterisks in quotes, like `'*foo*'`, are a string or glob.\n */\nconst EMPHASIS = /(?:^|[\\s(])(\\*\\*?)(?=[\\p{L}\\p{N}])[^*]+?(?<!\\s)\\1(?=$|[\\s)\"']|[.,;:!?](?:$|[\\s)\"']))/u;\n\nexport const LIST_MARKER = /^(?:[-*+]|\\d+[.)])\\s+/;\n\n/**\n * A code span, as in Markdown, closes with a run of as many backticks as it opened with,\n * like ``` `` `a` `` ```, and may continue on the next line.\n */\nexport const CODE_SPAN = /(?<!`)(`+)(?!`)[\\s\\S]*?(?<!`)\\1(?!`)/g;\n\n/**\n * Matches a colon after a word with more text after it on the same line, like `Optional: skips it`\n * or `...through its presets: the rules that...`. A colon that ends a line introduces what's below\n * it. Markers like `TODO:` or `WARNING:` that start a sentence are conventions read by tooling,\n * and a colon after a code span or `{@link}` (replaced by `code` in prose) introduces a definition,\n * like `` `width`: the maximum width ``, so neither counts. Neither does an identifier in camel or\n * snake case, like `maxRetries: 3`, which is a property.\n */\nconst COLON =\n /(?<=[\\w)\\]'\"’])(?<!(?:^|[.!?]\\s+)(?:[A-Z]{2,}|[Tt]odo|[Ff]ixme|[Hh]ack|[Xx]xx))(?<!\\b(?:code|url|entity|[A-Za-z][a-z\\d]*[A-Z]\\w*|\\w*_\\w*)):\\s+\\S/;\n\nexport function replaceKeepingLines(text: string, pattern: RegExp, replacement: string): string {\n return text.replaceAll(pattern, (match) => `${replacement}${'\\n'.repeat(match.split('\\n').length - 1)}`);\n}\n\nexport function stripNonProse(text: string): string {\n let stripped = replaceKeepingLines(text, CODE_SPAN, 'code');\n\n for (let previous = ''; previous !== stripped; ) {\n previous = stripped;\n stripped = replaceKeepingLines(stripped, /\\{[^{}]*\\}/g, 'code');\n }\n\n return stripped\n .replaceAll(/\\b[a-z][\\w+.-]*:\\/\\/\\S+/gi, 'url')\n .replaceAll(/&(?:[a-z][a-z\\d]*|#\\d+|#x[\\da-f]+);/gi, 'entity');\n}\n\nfunction hasExplanatoryColon(text: string): boolean {\n return COLON.test(text.replace(LIST_MARKER, ''));\n}\n\nexport function isBanner(text: string): boolean {\n const prose = stripNonProse(text.trim());\n\n return BANNER.test(prose) && !COLUMN_RULE.test(prose);\n}\n\nexport function findStyleIssues(lines: readonly StyleLine[]): StyleIssue[] {\n const issues: StyleIssue[] = [];\n const prose = stripNonProse(lines.map((line) => line.text.trim()).join('\\n'));\n\n /**\n * A JSDoc tag's description starts a sentence after its `@param name -` style head.\n * The head, including its type, may wrap across lines.\n */\n const proseLines = replaceKeepingLines(prose, /^@\\w+\\s+(?:code\\s+)?(?:\\S+\\s+-\\s+)?/gm, '').split('\\n');\n\n for (const [position, line] of lines.entries()) {\n const text = proseLines[position].trim();\n\n const report = (type: StyleIssueType): void => {\n issues.push({ type, index: line.index });\n };\n\n if (isBanner(line.text)) {\n report('banner');\n } else {\n if (SEMICOLON.test(text)) {\n report('semicolon');\n }\n\n if (text.includes('—')) {\n report('em-dash');\n }\n\n if (DASH.test(text)) {\n report('dash');\n }\n\n if (hasExplanatoryColon(text)) {\n report('label-colon');\n }\n\n if (EMPHASIS.test(text)) {\n report('emphasis');\n }\n }\n }\n\n return issues;\n}\n","export interface WrapOptions {\n /**\n * Sets whether a line may not end with the first word of a sentence, clause, or parenthetical,\n * like `This`, `the` after a comma, or `(the`. Such a word moves to the next line instead.\n * It also sets whether a line avoids ending with the first two words of a sentence, and whether\n * the first of two lines ends with a sentence instead of the start of the next one, as long as\n * the second line is then no longer than the first.\n */\n readonly avoidSentenceOrphans: boolean;\n\n /**\n * Sets how many columns a line may exceed its limit before it counts as overflowing.\n * A layout also uses those columns when that saves a line.\n */\n readonly tolerance: number;\n\n /**\n * Sets how many extra columns a complete parenthetical may use beyond the preferred line limit.\n */\n readonly parentheticalOverflow?: number;\n}\n\nexport type LineIssueType = 'overflow' | 'underfill' | 'orphan' | 'widow' | 'unbalanced' | 'split-parenthetical';\n\nexport interface LineIssue {\n readonly type: LineIssueType;\n\n /**\n * Holds the index of the offending line within the paragraph.\n */\n readonly line: number;\n}\n\n// punctuation inside a code span, like `,` or `?.`, is code and doesn't end a clause or sentence\nconst CLAUSE_END = /[.!?,;:][\"')\\]*_]*$/;\nconst SENTENCE_END = /[.!?][\"')\\]*_]*$/;\n\n/**\n * Holds only abbreviations that never end a sentence, unlike `no.` or `max.`, which can also be the\n * last word of one.\n */\nconst ABBREVIATIONS = new Set([\n 'e.g.',\n 'i.e.',\n 'etc.',\n 'vs.',\n 'cf.',\n 'approx.',\n 'incl.',\n 'resp.',\n 'al.',\n 'viz.',\n 'esp.',\n 'ca.',\n 'fig.',\n 'figs.',\n 'eq.',\n 'vol.',\n 'pp.',\n 'mr.',\n 'mrs.',\n 'ms.',\n 'dr.',\n 'prof.',\n 'st.',\n]);\n\nexport function isAbbreviation(word: string): boolean {\n const bare = word\n .replace(/^[([\"']+/, '')\n .replace(/[)\\]\"']+$/, '')\n .toLowerCase();\n\n return ABBREVIATIONS.has(bare) || /^(?:[a-z]\\.){2,}$/.test(bare);\n}\n\nfunction findClosingBrace(text: string, start: number): number {\n let depth = 0;\n\n for (let index = start; index < text.length; index += 1) {\n if (text[index] === '{') {\n depth += 1;\n } else if (text[index] === '}') {\n depth -= 1;\n\n if (depth === 0) {\n return index;\n }\n }\n }\n\n return -1;\n}\n\nfunction backtickRun(text: string, start: number): number {\n let end = start;\n\n while (text[end] === '`') {\n end += 1;\n }\n\n return end - start;\n}\n\n/**\n * As in Markdown, a code span opened by a run of backticks only closes with a run of the same\n * length, like the two in ``` `` `a` `` ```.\n */\nfunction findCodeSpanEnd(text: string, start: number): number {\n const run = backtickRun(text, start);\n\n for (let index = text.indexOf('`', start + run); index !== -1; ) {\n const length = backtickRun(text, index);\n\n if (length === run) {\n return index + length - 1;\n }\n\n index = text.indexOf('`', index + length);\n }\n\n return -1;\n}\n\n/**\n * Returns whether a backslash escapes the character at `index`, as in Markdown. A backslash that\n * another one escapes, like the second of `\\\\[`, escapes nothing.\n */\nfunction isEscaped(text: string, index: number): boolean {\n let backslashes = 0;\n\n while (text[index - 1 - backslashes] === '\\\\') {\n backslashes += 1;\n }\n\n return backslashes % 2 === 1;\n}\n\nfunction findUnescaped(text: string, char: string, start: number): number {\n for (let index = start; index < text.length; index += 1) {\n if (text[index] === '\\\\') {\n index += 1;\n } else if (text[index] === char) {\n return index;\n }\n }\n\n return -1;\n}\n\n/**\n * Returns the index of the `]` that closes the `[` at `start`, or -1 when it never closes.\n * Brackets pair up as in Markdown, where a bracket after a backslash or in a code span is no\n * bracket. A brace is no syntax in Markdown, so a `{` after the `[` never hides the `]`.\n */\nfunction findClosingBracket(text: string, start: number): number {\n let depth = 0;\n\n for (let index = start; index < text.length; index += 1) {\n switch (text[index]) {\n case '\\\\': {\n index += 1;\n\n break;\n }\n case '`': {\n const close = findCodeSpanEnd(text, index);\n\n // a run of backticks that never closes is literal text\n index = close === -1 ? index + backtickRun(text, index) - 1 : close;\n\n break;\n }\n case '[': {\n depth += 1;\n\n break;\n }\n case ']': {\n depth -= 1;\n\n if (depth === 0) {\n return index;\n }\n\n break;\n }\n }\n }\n\n return -1;\n}\n\n/**\n * Returns the index of the `)` that closes the `(` at `start`, which opens the destination of a\n * Markdown link, or -1 when it never closes. Parentheses pair up. A destination in angle brackets,\n * like `(<a b>)`, and a title in quotes after the destination, like `(target \"title\")`, can hold a\n * parenthesis that pairs up with nothing.\n */\nfunction findDestinationEnd(text: string, start: number): number {\n let depth = 0;\n\n for (let index = start; index < text.length; index += 1) {\n const char = text[index];\n\n let close = -1;\n\n if (char === '\\\\') {\n index += 1;\n } else if (char === '<' && text.slice(start + 1, index).trim() === '') {\n close = findUnescaped(text, '>', index + 1);\n } else if ((char === '\"' || char === \"'\") && /\\s/.test(text[index - 1])) {\n close = findUnescaped(text, char, index + 1);\n } else if (char === '(') {\n depth += 1;\n } else if (char === ')' && --depth === 0) {\n return index;\n }\n\n index = close === -1 ? index : close;\n }\n\n return -1;\n}\n\n/**\n * Returns the index of the last character of the Markdown link that starts with the `[` at\n * `start`, or -1 when the bracket never closes. A link is the text in brackets, like `[text]`,\n * with the destination right after it, like `[text](target \"title\")`. The label of a reference\n * link, like the second pair of brackets in `[text][label]`, is a link that starts right after\n * the first one, so it needs no check of its own.\n *\n * Only the syntax counts. Whether the text in brackets resolves to anything, like an intra-doc link\n * in Rust or a link reference definition, is something only the renderer knows.\n */\nfunction findLinkEnd(text: string, start: number): number {\n const close = findClosingBracket(text, start);\n\n if (close === -1 || text[close + 1] !== '(') {\n return close;\n }\n\n const end = findDestinationEnd(text, close + 1);\n\n return end === -1 ? close : end;\n}\n\n/**\n * Splits text into words. JSDoc writes types and `{@link ...}` tags as `{...}` groups, so they stay\n * intact like code spans.\n *\n * A Markdown link stays intact too, since a link that continues on the next line can't be followed\n * in an editor. rust-analyzer, for one, only resolves an intra-doc link that sits on one line of\n * a `///` comment. A link is one word, like a code span, but its own words keep one space between\n * them, like the words around it.\n */\nexport function tokenize(text: string): string[] {\n return splitWords(text, true);\n}\n\nfunction splitWords(text: string, keepLinks: boolean): string[] {\n const tokens: string[] = [];\n\n let current = '';\n let index = 0;\n\n while (index < text.length) {\n const char = text[index];\n const linkEnd = keepLinks && char === '[' && !isEscaped(text, index) ? findLinkEnd(text, index) : -1;\n\n if (linkEnd !== -1) {\n current += splitWords(text.slice(index, linkEnd + 1), false).join(' ');\n index = linkEnd + 1;\n\n continue;\n }\n\n const close = char === '`' ? findCodeSpanEnd(text, index) : char === '{' ? findClosingBrace(text, index) : -1;\n\n // a run of backticks that never closes is literal text, and none of its backticks open a span\n if (char === '`' && close === -1) {\n const run = backtickRun(text, index);\n\n current += text.slice(index, index + run);\n index += run;\n\n continue;\n }\n\n if (close !== -1) {\n current += text.slice(index, close + 1);\n index = close + 1;\n\n continue;\n }\n\n if (/\\s/.test(char)) {\n if (current) {\n tokens.push(current);\n\n current = '';\n }\n\n index += 1;\n\n continue;\n }\n\n current += char;\n index += 1;\n }\n\n if (current) {\n tokens.push(current);\n }\n\n return tokens;\n}\n\nfunction parentheticalEnd(text: string): number {\n let depth = 0;\n\n for (let index = 0; index < text.length; index += 1) {\n const char = text[index];\n\n if (char === '\\\\') {\n index += 1;\n\n continue;\n }\n\n const close = char === '`' ? findCodeSpanEnd(text, index) : char === '{' ? findClosingBrace(text, index) : -1;\n\n if (close !== -1) {\n index = close;\n } else if (char === '(') {\n depth += 1;\n } else if (char === ')' && --depth === 0) {\n return index;\n }\n }\n\n return -1;\n}\n\nfunction isParenthetical(token: string): boolean {\n return token.startsWith('(') && parentheticalEnd(token) !== -1;\n}\n\nexport interface ParentheticalGroup {\n readonly start: number;\n readonly end: number;\n}\n\nfunction parentheticalGroups(tokens: readonly string[], limit: number): ParentheticalGroup[] {\n const groups: ParentheticalGroup[] = [];\n\n for (let index = 0; index < tokens.length; index += 1) {\n if (tokens[index].startsWith('(')) {\n const end = parentheticalEnd(tokens.slice(index).join(' '));\n\n let last = index;\n let length = tokens[index].length;\n\n while (length <= end && last + 1 < tokens.length) {\n length += 1 + tokens[++last].length;\n }\n\n if (end !== -1 && length <= limit) {\n groups.push({ start: index, end: last + 1 });\n\n index = last;\n\n continue;\n }\n }\n }\n\n return groups;\n}\n\n/**\n * Returns the parenthetical that a line break before the token at `at` splits, of those that fit\n * in `limit` columns.\n */\nexport function parentheticalAround(\n tokens: readonly string[],\n at: number,\n limit: number,\n): ParentheticalGroup | undefined {\n return parentheticalGroups(tokens, limit).find((group) => group.start < at && at < group.end);\n}\n\nexport function bindParentheticals(tokens: readonly string[], limit: number): string[] {\n const groups = new Map(parentheticalGroups(tokens, limit).map((group) => [group.start, group.end]));\n const units: string[] = [];\n\n for (let index = 0; index < tokens.length; index += 1) {\n const end = groups.get(index) ?? index + 1;\n\n units.push(tokens.slice(index, end).join(' '));\n\n index = end - 1;\n }\n\n return units;\n}\n\nfunction endsClause(token: string): boolean {\n return CLAUSE_END.test(token) && !isAbbreviation(token);\n}\n\nfunction endsSentence(token: string): boolean {\n return SENTENCE_END.test(token) && !isAbbreviation(token);\n}\n\nfunction opensAside(token: string): boolean {\n return /^[([]/.test(token) && !/[)\\]][\"'`.,;:!?]*$/.test(token);\n}\n\nfunction isOrphan(previous: string, token: string): boolean {\n return opensAside(token) || (endsClause(previous) && !CLAUSE_END.test(token));\n}\n\n/**\n * Returns whether Markdown or JSDoc reads a word as the start of a block when it begins a line, like a\n * list marker (`-`, `1.`), a heading (`#`), a quote (any word that starts with `>`, like `>=`), a fence,\n * a table row, an HTML tag, comment, or declaration (`<div>`, `<!--`, `<?`), or a JSDoc tag like `@param`.\n * A run of `-`, `=`, `*`, or `_` on a line of its own is a heading underline or a rule, so it never starts\n * a line either. Wrapping must never move such a word to the start of a line, or the paragraph would turn\n * into something else.\n */\nexport function startsBlock(token: string): boolean {\n return (\n /^(?:\\+|-+|=+|\\*+|_{3,}|\\d+[.)]|#{1,6})$/.test(token) ||\n /^(?:>|@[A-Za-z]|`{3}|~{3}|\\||<(?:\\/?[A-Za-z]|[!?]))/.test(token)\n );\n}\n\nexport function bindLineStarts(\n tokens: readonly string[],\n startsLine: (token: string, next: string | undefined) => boolean = startsBlock,\n): string[] {\n const units: string[] = [];\n\n for (const [index, token] of tokens.entries()) {\n if (units.length > 0 && startsLine(token, tokens[index + 1])) {\n units[units.length - 1] += ` ${token}`;\n } else {\n units.push(token);\n }\n }\n\n return units;\n}\n\nfunction breaksBefore(previous: string, last: string, next: string, options: WrapOptions): boolean {\n return (options.avoidSentenceOrphans && isOrphan(previous, last)) || startsBlock(next);\n}\n\nfunction lineLength(tokens: readonly string[]): number {\n let length = 0;\n\n for (const token of tokens) {\n length += token.length;\n }\n\n return length + Math.max(0, tokens.length - 1);\n}\n\n/**\n * The first line can have a different limit than the rest, e.g. when a list marker takes up space\n * only on the first line.\n */\nexport function wrapTokens(\n tokens: readonly string[],\n firstLimit: number,\n restLimit: number,\n options: WrapOptions,\n): string[][] {\n return wrapFrom(tokens, 0, firstLimit, restLimit, options, Infinity);\n}\n\n/**\n * Wraps the tokens from the one at `from` on, and stops after `maxLines` lines. A line\n * never depends on the lines after it, so the first lines of a wrap are the same with or\n * without the rest.\n */\nfunction wrapFrom(\n tokens: readonly string[],\n from: number,\n firstLimit: number,\n restLimit: number,\n options: WrapOptions,\n maxLines: number,\n): string[][] {\n const lines: string[][] = [];\n\n let line: string[] = [];\n let length = 0;\n\n for (let index = from; index < tokens.length; index += 1) {\n const token = tokens[index];\n\n while (line.length > 0) {\n const limit = lines.length === 0 ? firstLimit : restLimit;\n const nextLength = length + 1 + token.length;\n const extra = isParenthetical(token) ? (options.parentheticalOverflow ?? 0) : 0;\n\n if (nextLength <= limit + extra) {\n break;\n }\n\n const carry: string[] = [];\n\n while (line.length >= 2 && breaksBefore(line.at(-2) ?? '', line.at(-1) ?? '', carry[0] ?? token, options)) {\n carry.unshift(line.pop() ?? '');\n }\n\n lines.push(line);\n\n if (lines.length >= maxLines) {\n return lines;\n }\n\n line = carry;\n length = lineLength(carry);\n }\n\n line.push(token);\n\n length = lineLength(line);\n }\n\n if (line.length > 0) {\n lines.push(line);\n }\n\n return lines;\n}\n\nconst MIN_LAST_LINE_WORDS = 3;\n\nexport function hasWidow(lines: readonly (readonly string[])[]): boolean {\n const last = lines.at(-1) ?? [];\n const words = last.reduce((count, token) => count + (isParenthetical(token) ? tokenize(token).length : 1), 0);\n\n return lines.length >= 2 && words < MIN_LAST_LINE_WORDS;\n}\n\nexport interface LayoutOptions extends WrapOptions {\n /**\n * Keeps the first line out of the balancing, e.g. for a JSDoc tag whose first\n * line holds `@param {Type} name -`.\n */\n readonly keepFirstLine?: boolean;\n\n /**\n * Keeps the fewest lines even when a line sticks out far past the line before it.\n */\n readonly keepSteps?: boolean;\n\n /**\n * Lets a line end with the first few words of a sentence at no extra cost.\n */\n readonly ignoreSentenceStarts?: boolean;\n}\n\n/**\n * Sets how heavily lines that come close to the width count against lines that are even. With too\n * much weight, lines of 86 and 89 columns beat two of 86 at a width of 100, which leaves a word\n * like `as` at the end of the longer line, away from the words that it belongs to.\n */\nconst FULLNESS_WEIGHT = 0.07;\nconst PARENTHETICAL_SPLIT_WEIGHT = 2;\n\n/**\n * Sets how much a line costs when it ends with the first `MAX_SENTENCE_START_WORDS` words or fewer\n * of a sentence, like `It allocates` in `...UDP opens. It allocates`. It costs as much as a line\n * that ends 5 columns before the others, so lines that are only a little less even win.\n */\nconst SENTENCE_START_COST = 25;\nconst MAX_SENTENCE_START_WORDS = 2;\n\n/**\n * Sets how many columns a line may stick out past the line before it.\n */\nconst MAX_STEP = 15;\n\n/**\n * Sets how many lines more than its fewest a layout may take to keep a line from sticking out.\n */\nconst EXTRA_LINES = 2;\n\n/**\n * Holds what the cost of a line depends on, besides the length that the lines aim for.\n */\ninterface BalanceInput {\n /**\n * Holds the column that each token starts at when all tokens share a line, and the column after\n * the last token.\n */\n readonly starts: readonly number[];\n readonly count: number;\n\n /**\n * Holds how many fewer columns the first line has than the other lines.\n */\n readonly offset: number;\n\n /**\n * Holds for each token whether it may not start a line.\n */\n readonly blockStarts: readonly boolean[];\n\n /**\n * Holds for each token whether a line of two or more tokens may not end before it.\n */\n readonly afterOrphan: readonly boolean[];\n readonly splitCosts: ReadonlyMap<number, number>;\n}\n\n/**\n * Holds the cheapest layouts that end one line, one for each token that the line can end before.\n */\ninterface LineCosts {\n /**\n * Holds the token that the first entry of `costs` and `origins` ends before.\n */\n readonly base: number;\n readonly costs: readonly number[];\n\n /**\n * Holds the token that the line starts with in the layout of each entry of `costs`.\n */\n readonly origins: readonly number[];\n\n /**\n * Holds the first token that the line can end before.\n */\n readonly low: number;\n\n /**\n * Holds the last token that the line can end before.\n */\n readonly high: number;\n}\n\n/**\n * Returns, for each token, the fewest lines of at most `target` columns that hold the tokens from\n * it on. No token is longer than `target`.\n */\nfunction fewestLines(starts: readonly number[], target: number): Int32Array {\n const total = starts.length - 1;\n const fewest = new Int32Array(total + 1);\n\n let end = total;\n\n for (let index = total - 1; index >= 0; index -= 1) {\n while (starts[end] - starts[index] - 1 > target) {\n end -= 1;\n }\n\n fewest[index] = fewest[end] + 1;\n }\n\n return fewest;\n}\n\n/**\n * Returns the first index from `from` on whose value is at most `limit`. The values never grow as\n * the index rises, so a binary search can find it.\n */\nfunction firstAtMost(values: Int32Array, limit: number, from: number): number {\n let low = from;\n let high = values.length - 1;\n\n while (low < high) {\n const middle = Math.floor((low + high) / 2);\n\n if (values[middle] <= limit) {\n high = middle;\n } else {\n low = middle + 1;\n }\n }\n\n return low;\n}\n\n/**\n * Returns the cheapest layouts that end the given line, or `undefined` when the line can't end\n * anywhere. It only looks at the ends that a whole layout can use. The lines before it reach up\n * to an end, and the lines after it can hold the rest from an end on, which leaves a narrow band\n * of tokens instead of all of them.\n */\nfunction costsOfLine(\n input: BalanceInput,\n target: number,\n line: number,\n previous: LineCosts,\n fewest: Int32Array,\n): LineCosts | undefined {\n const { starts, afterOrphan } = input;\n const offset = line === 1 ? input.offset : 0;\n const linesLeft = input.count - line;\n\n // each line after this one needs a token\n const last = starts.length - 1 - linesLeft;\n const base = firstAtMost(fewest, linesLeft, Math.max(line, previous.low + 1));\n const costs: number[] = [];\n const origins: number[] = [];\n\n let low = -1;\n let high = -1;\n\n for (let end = base; end <= last; end += 1) {\n const nearest = Math.min(end - 1, previous.high);\n\n // from here on, a line is too long even when it starts at the last end of the line before it\n if (end > previous.high && starts[end] - starts[nearest] - 1 + offset > target) {\n break;\n }\n\n let cost = Infinity;\n let origin = -1;\n\n if (!input.blockStarts[end]) {\n const splitCost = input.splitCosts.get(end) ?? 0;\n\n for (let start = nearest; start >= previous.low; start -= 1) {\n const length = starts[end] - starts[start] - 1 + offset;\n\n if (length > target) {\n break;\n }\n\n const before = previous.costs[start - previous.base];\n\n if (before === Infinity || (afterOrphan[end] && end - start >= 2)) {\n continue;\n }\n\n const candidate = before + (target - length) ** 2 + splitCost;\n\n if (candidate <= cost) {\n cost = candidate;\n origin = start;\n }\n }\n }\n\n costs.push(cost);\n origins.push(origin);\n\n if (cost !== Infinity) {\n low = low === -1 ? end : low;\n high = end;\n }\n }\n\n return low === -1 ? undefined : { base, costs, origins, low, high };\n}\n\n/**\n * Lays the tokens out in `count` lines that are as even as possible. It tries every length that\n * the lines can aim for and keeps the layout with the lowest cost. Of two layouts with the same\n * cost, it keeps the one that aims for longer lines, and then the one with the longer last line.\n */\nfunction balanceTokens(\n tokens: readonly string[],\n firstLimit: number,\n restLimit: number,\n count: number,\n options: LayoutOptions,\n preferredLimit = restLimit,\n splitCosts: ReadonlyMap<number, number> = new Map(),\n): string[][] | undefined {\n const total = tokens.length;\n const starts = [0];\n\n let longest = -Infinity;\n\n for (const token of tokens) {\n starts.push((starts.at(-1) ?? 0) + token.length + 1);\n\n longest = Math.max(longest, token.length);\n }\n\n const input: BalanceInput = {\n starts,\n count,\n offset: restLimit - firstLimit,\n blockStarts: tokens.map((token) => startsBlock(token)),\n afterOrphan: tokens.map((_, index) => {\n return options.avoidSentenceOrphans && index >= 2 && isOrphan(tokens[index - 2], tokens[index - 1]);\n }),\n splitCosts: withSentenceStartCosts(tokens, splitCosts, options),\n };\n\n const targets: number[] = [];\n\n for (let target = restLimit; target >= Math.max(longest, Math.floor(restLimit / 2)); target -= 1) {\n targets.push(target);\n }\n\n // the lines before the last one hold at most this many columns together, which is when the last line is empty\n const most = starts[total] - count + input.offset;\n\n let best: { cost: number; target: number; breaks: number[] } | undefined;\n\n /**\n * Short targets come first. They rule most layouts out, so they are cheap, and their cost lets\n * the loop skip the long targets, whose lines can't all be full.\n */\n for (const target of targets.toReversed()) {\n const fullness = FULLNESS_WEIGHT * (count - 1) * (preferredLimit - target) ** 2;\n const spare = (count - 1) * target - most;\n\n // lines that are `spare` columns short together cost the least when they are all equally short\n const least = count > 1 && spare > 0 ? Math.max(spare ** 2 / (count - 1) - 1, 0) : 0;\n\n if (best !== undefined && least + fullness > best.cost) {\n continue;\n }\n\n const fewest = fewestLines(starts, target);\n const rows: LineCosts[] = [{ base: 0, costs: [0], origins: [-1], low: 0, high: 0 }];\n\n for (let line = 1; line < count; line += 1) {\n const row = costsOfLine(input, target, line, rows[line - 1], fewest);\n\n if (!row) {\n break;\n }\n\n rows.push(row);\n }\n\n if (rows.length < count) {\n continue;\n }\n\n const row = rows[count - 1];\n\n for (let start = row.low; start <= row.high; start += 1) {\n const score = row.costs[start - row.base] + fullness;\n\n const improves = score < (best?.cost ?? Infinity) || (score === best?.cost && target > best.target);\n\n if (!improves || starts[total] - starts[start] - 1 > target || hasWidow([[], tokens.slice(start)])) {\n continue;\n }\n\n const breaks = [start];\n\n for (let line = count - 1; line > 1; line -= 1) {\n breaks.unshift(rows[line].origins[breaks[0] - rows[line].base]);\n }\n\n best = { cost: score, target, breaks };\n }\n }\n\n if (!best) {\n return undefined;\n }\n\n const bounds = [0, ...best.breaks, total];\n\n return bounds.slice(0, -1).map((start, index) => tokens.slice(start, bounds[index + 1]));\n}\n\n/**\n * Returns whether a line sticks out more than `MAX_STEP` columns past the line before it.\n * The first line starts `offset` columns after the others, so it ends that much later too.\n */\nfunction hasStep(lines: readonly (readonly string[])[], offset = 0): boolean {\n return lines.some((line, index) => {\n if (index === 0) {\n return false;\n }\n\n const before = lineLength(lines[index - 1]) + (index === 1 ? offset : 0);\n\n return lineLength(line) - before > MAX_STEP;\n });\n}\n\nfunction splitCostsOf(tokens: readonly string[], limit: number): Map<number, number> {\n const splitCosts = new Map<number, number>();\n\n for (const group of parentheticalGroups(tokens, limit)) {\n const length = lineLength(tokens.slice(group.start, group.end));\n\n for (let at = group.start + 1; at < group.end; at += 1) {\n splitCosts.set(at, length * PARENTHETICAL_SPLIT_WEIGHT);\n }\n }\n\n return splitCosts;\n}\n\n/**\n * Adds `SENTENCE_START_COST` to each line break that leaves the first words of a sentence at the\n * end of the line before it. A break after a whole sentence of that few words costs nothing.\n */\nfunction withSentenceStartCosts(\n tokens: readonly string[],\n splitCosts: ReadonlyMap<number, number>,\n options: LayoutOptions,\n): ReadonlyMap<number, number> {\n if (!options.avoidSentenceOrphans || options.ignoreSentenceStarts) {\n return splitCosts;\n }\n\n const costs = new Map(splitCosts);\n\n for (const [index, token] of tokens.entries()) {\n if (!endsSentence(token)) {\n continue;\n }\n\n const last = Math.min(index + 1 + MAX_SENTENCE_START_WORDS, tokens.length - 1);\n\n for (let end = index + 2; end <= last && !endsSentence(tokens[end - 1]); end += 1) {\n costs.set(end, (costs.get(end) ?? 0) + SENTENCE_START_COST);\n }\n }\n\n return costs;\n}\n\nfunction hasParentheticals(tokens: readonly string[], restLimit: number, options: WrapOptions): boolean {\n if (options.parentheticalOverflow === undefined) {\n return false;\n }\n\n return bindParentheticals(tokens, restLimit + options.parentheticalOverflow).some((token) => isParenthetical(token));\n}\n\n/**\n * Lays the tokens out in even lines. When a line sticks out far past the line before it, like a\n * full second line after a first line that the orphan rule keeps short, the layout takes up to\n * `EXTRA_LINES` more lines if those avoid the step.\n */\nexport function layoutTokens(\n tokens: readonly string[],\n firstLimit: number,\n restLimit: number,\n options: LayoutOptions,\n): string[][] {\n const lines = layoutLines(tokens, firstLimit, restLimit, options);\n const offset = restLimit - firstLimit;\n\n // the first line of a JSDoc tag is never balanced, and the lines after it get their own layout\n if (options.keepFirstLine || options.keepSteps || !hasStep(lines, offset)) {\n return lines;\n }\n\n const overflow = hasParentheticals(tokens, restLimit, options) ? (options.parentheticalOverflow ?? 0) : 0;\n const splitCosts = overflow > 0 ? splitCostsOf(tokens, restLimit + overflow) : new Map<number, number>();\n\n for (let count = lines.length + 1; count <= lines.length + EXTRA_LINES; count += 1) {\n const even = balanceTokens(\n tokens,\n firstLimit + overflow,\n restLimit + overflow,\n count,\n options,\n restLimit,\n splitCosts,\n );\n\n if (even && !hasStep(even, offset)) {\n return even;\n }\n }\n\n return lines;\n}\n\nfunction layoutLines(\n tokens: readonly string[],\n firstLimit: number,\n restLimit: number,\n options: LayoutOptions,\n): string[][] {\n if (options.parentheticalOverflow !== undefined) {\n const grouped = bindParentheticals(tokens, restLimit + options.parentheticalOverflow);\n\n if (grouped.some((token) => isParenthetical(token))) {\n const minimumLines = Math.max(\n 2,\n Math.ceil((lineLength(tokens) + 1 + restLimit - firstLimit) / Math.max(1, restLimit + 1)),\n );\n\n let preserved = wrapTokens(grouped, firstLimit, restLimit, options);\n\n if (minimumLines === 2 && preserved.length > 2) {\n const wide = wrapTokens(\n grouped,\n firstLimit + options.parentheticalOverflow,\n restLimit + options.parentheticalOverflow,\n { ...options, parentheticalOverflow: undefined },\n );\n\n if (wide.length === 2 && !hasWidow(wide)) {\n preserved = wide;\n }\n }\n\n if (preserved.length <= 2 && !hasWidow(preserved)) {\n let cursor = 0;\n\n return preserved.map((line) => {\n const expanded: string[] = [];\n const length = lineLength(line);\n\n let used = -1;\n\n while (cursor < tokens.length && used < length) {\n const token = tokens[cursor++];\n\n expanded.push(token);\n\n used += token.length + 1;\n }\n\n return expanded;\n });\n }\n\n const splitCosts = splitCostsOf(tokens, restLimit + options.parentheticalOverflow);\n const preferred = wrapTokens(tokens, firstLimit, restLimit, { ...options, parentheticalOverflow: undefined });\n\n for (let count = minimumLines; count <= Math.max(preferred.length + 1, preserved.length); count += 1) {\n const balanced = balanceTokens(\n tokens,\n firstLimit + options.parentheticalOverflow,\n restLimit + options.parentheticalOverflow,\n count,\n options,\n restLimit,\n splitCosts,\n );\n\n if (balanced) {\n return balanced;\n }\n }\n }\n }\n\n const greedy = wrapTokens(tokens, firstLimit, restLimit, options);\n\n if (greedy.length < 2) {\n return greedy;\n }\n\n if (options.keepFirstLine) {\n const [head] = greedy;\n const rest = layoutTokens(tokens.slice(head.length), restLimit, restLimit, { ...options, keepFirstLine: false });\n const lines = [head, ...rest];\n\n if (!hasWidow(lines)) {\n return lines;\n }\n }\n\n /**\n * Lines may take up to `tolerance` columns past the limit when that saves a line. A paragraph of\n * two lines keeps them, since its second line would have to join the first.\n */\n if (options.tolerance > 0 && greedy.length > 2) {\n const fewer = balanceTokens(\n tokens,\n firstLimit + options.tolerance,\n restLimit + options.tolerance,\n greedy.length - 1,\n options,\n restLimit,\n );\n\n if (fewer && !hasStep(fewer, restLimit - firstLimit)) {\n return fewer;\n }\n }\n\n const balanced = balanceTokens(tokens, firstLimit, restLimit, greedy.length, options);\n\n if (balanced) {\n return balanced;\n }\n\n const stretched = wrapTokens(tokens, firstLimit + options.tolerance, restLimit + options.tolerance, options);\n\n return hasWidow(stretched) ? greedy : stretched;\n}\n\n/**\n * Ends the first of two lines with a sentence when the rest fits on the second line without\n * sticking out past the first. A second line that would stick out keeps the lines as they are.\n */\nexport function breakAfterSentence(\n lines: string[][],\n firstLimit: number,\n restLimit: number,\n options: WrapOptions,\n): string[][] {\n if (lines.length !== 2 || !options.avoidSentenceOrphans) {\n return lines;\n }\n\n const tokens = lines.flat();\n\n // the first line starts this many columns after the second, so it ends that much later too\n const offset = restLimit - firstLimit;\n\n for (let end = tokens.length - 1; end > 0; end -= 1) {\n const first = tokens.slice(0, end);\n const second = tokens.slice(end);\n\n // a second line that is no longer than a first line within its limit is within its own limit too\n if (\n endsSentence(tokens[end - 1]) &&\n !hasWidow([first, second]) &&\n !breaksBefore(tokens[end - 2] ?? '', tokens[end - 1], tokens[end], options) &&\n lineLength(first) <= firstLimit &&\n lineLength(second) <= lineLength(first) + offset\n ) {\n return [first, second];\n }\n }\n\n return lines;\n}\n\n/**\n * A wrap produced by `wrapTokens` with the same arguments never has issues, unless a single token\n * is wider than the limit.\n */\nexport function findLineIssues(\n lines: readonly (readonly string[])[],\n firstLimit: number,\n restLimit: number,\n options: WrapOptions,\n): LineIssue[] {\n const issues: LineIssue[] = [];\n const tokens = lines.flat();\n\n let next = 0;\n\n for (const [index, line] of lines.entries()) {\n const limit = index === 0 ? firstLimit : restLimit;\n const length = lineLength(line);\n const start = next;\n\n next += line.length;\n\n if (line.length > 1 && length > limit + options.tolerance) {\n issues.push({ type: 'overflow', line: index });\n }\n\n if (index === lines.length - 1 || line.length === 0) {\n continue;\n }\n\n if (options.avoidSentenceOrphans && line.length >= 2 && isOrphan(line.at(-2) ?? '', line.at(-1) ?? '')) {\n issues.push({ type: 'orphan', line: index });\n }\n\n /**\n * Only the first line of a wrap of the rest counts. A line that may end with the start of a\n * sentence is measured too, because the words that fit on it don't depend on its last word.\n */\n const [expected] = wrapFrom(tokens, start, limit, restLimit, options, 1);\n\n if (expected.length > line.length) {\n issues.push({ type: 'underfill', line: index });\n }\n }\n\n return issues;\n}\n","import { spawn } from 'node:child_process';\nimport { readFileSync } from 'node:fs';\nimport path from 'node:path';\nimport type { CommentReport, IssueReport, IssueType, ParagraphReport } from './analyze.js';\nimport type { AgentConfig, AgentRunner } from './config.js';\n\n/**\n * This variable is set in the agent's environment so that the agent's own stop hook doesn't\n * spawn another agent.\n */\nexport const CHILD_ENV = 'COMMENTS_HOOK_CHILD';\n\nexport const DEFAULT_RULES = `\n# Comment rules\n\n## Priorities\n\nWhen rules pull in different directions, the earlier one wins.\n\n1. 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).\n2. Keep the meaning. An edit may make a comment clearer, never different.\n3. Follow the style preferences below.\n4. Change as little as possible. Wording that already reads clearly stays, even when a note points at it.\n\n## Layout\n\n- 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.\n- 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.\n- 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.\n- 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.\n- Separate paragraphs with an empty comment line.\n- Never split a backtick code span across lines.\n- 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.\n- 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.\n- 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.\n- 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.\n- 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.\n- 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.\n\n## Style\n\nThese are preferences for comment prose. They never apply to code or exact text (see the priorities above), and a punctuation character on its own is never a reason to edit prose that already reads clearly.\n\n- Write clear sentences that flow. Descriptions read best as complete sentences with a subject and a verb, so prefer \"Returns the bytes the guest sent\" over a fragment like \\`Bytes from the guest.\\` or \\`Optional.\\` Imperative or third-person verb phrases like \"Resolve a hostname...\" or \"Aborts every channel...\" are fine, since the preference is against verb-less fragments, not against concise sentences.\n- Prefer plain sentence punctuation. Where a semicolon, an em-dash (—), or a spaced hyphen, en-dash, or double hyphen standing in for one only joins two thoughts, a period, a comma, parentheses, or a word like \"and\", \"so\", or \"because\" usually reads better. Keep the punctuation where it carries meaning or reads more clearly than the alternatives.\n- 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.\n- 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*\\` \\`\\`.\n- 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.\n- Block, JSDoc, and Rust doc comments are capitalized and end with punctuation.\n- 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.\n\n## Rust doc comments\n\nRust doc comments are Markdown that rustdoc renders.\n\n- 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)\\`.\n- A link reference definition, like \\`\\` [\\`Foo\\`]: crate::Foo \\`\\`, stays on its own line as it is. Its colon is syntax, not a label.\n- 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.\n\n## Editing\n\n- Keep the wording unless it goes against a rule above. Then reword as little as needed and keep the meaning.\n- Don't reword anything else unless the instructions below allow it.\n`.trim();\n\nconst DEFAULT_TIMEOUT_SECONDS = 300;\nconst DEFAULT_CLAUDE_MODEL = 'haiku';\n\n/**\n * Sets the longest time an agent gets to exit after its timeout before it is killed.\n */\nconst KILL_GRACE_MS = 5000;\n\n/**\n * Sets how long to wait for the rest of the output after the agent exited. A process that the agent\n * left behind keeps the pipes open for as long as it runs.\n */\nconst DRAIN_GRACE_MS = 1000;\n\nconst RUNNER_COMMANDS: Record<AgentRunner, string> = {\n claude: 'claude',\n codex: 'codex',\n cursor: 'cursor-agent',\n};\n\nexport interface AgentInvocation {\n readonly command: string;\n readonly args: readonly string[];\n readonly stdin?: string;\n}\n\nexport interface AgentResult {\n readonly ok: boolean;\n readonly output: string;\n readonly error?: string;\n}\n\n// hints are phrased as possibilities, since they come from heuristics that the agent may reject\nconst ISSUE_DESCRIPTIONS: Record<Exclude<IssueType, 'overflow'>, string> = {\n underfill: 'ends early, and words from the next line fit on it',\n widow: 'leaves fewer than three words on the last line of its paragraph',\n unbalanced:\n 'is in a paragraph whose lines are less even than the suggested rewrap, or that splits a link or code span that the rewrap keeps on one line',\n orphan:\n '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)',\n 'split-parenthetical':\n '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)',\n 'em-dash':\n \"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)\",\n banner:\n \"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)\",\n semicolon:\n \"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)\",\n 'label-colon':\n 'may use a colon after a label (leave it if it introduces a list or code, defines a term or a link like `` [`Foo`]: crate::Foo ``, is notation like a time or `key: value`, or is a `Note:` or `Optional:` that starts the comment)',\n emphasis:\n 'may use asterisk emphasis (leave the asterisks of a name or pattern like `*test*`, which belongs in a code span with its asterisks)',\n dash: \"may use a spaced hyphen, en-dash, or double hyphen as an em-dash (leave it if it's a range, a CLI argument, arithmetic, code, a term's definition, or reads more clearly than the alternatives)\",\n};\n\nconst HINT_MARKER = 'possible:';\n\nfunction describeIssue(issue: IssueReport, width: number, maxWidth?: number): string {\n if (issue.type === 'overflow') {\n const limit = maxWidth ?? width;\n const name = maxWidth === undefined ? 'width' : 'maximum';\n\n return `line ${issue.line} is ${issue.length} columns, over the ${name} of ${limit}`;\n }\n\n const description = `line ${issue.line} ${ISSUE_DESCRIPTIONS[issue.type]}`;\n\n return issue.tier === 'hint' ? `${HINT_MARKER} ${description}` : description;\n}\n\nfunction describeIssues(paragraph: ParagraphReport, width: number, maxWidth?: number): string[] {\n return paragraph.issues.map((issue) => describeIssue(issue, width, maxWidth));\n}\n\nfunction describeComment(comment: CommentReport): string {\n const notes: string[] = [];\n\n for (const paragraph of comment.paragraphs) {\n const issues = describeIssues(paragraph, comment.width, comment.maxWidth);\n\n if (issues.length > 0 && !paragraph.reflowable) {\n const range = `lines ${paragraph.startLine}-${paragraph.endLine}`;\n\n issues.push(\n `${range} are kept as written (${paragraph.reason ?? 'layout'}), so the tool never rewraps them and any fix to them is yours`,\n );\n }\n\n notes.push(...issues);\n }\n\n const fence = comment.text.includes('```') ? '~~~~' : '```';\n\n return [\n `### ${comment.file}:${comment.startLine}-${comment.endLine} (width ${comment.width}${comment.maxWidth === undefined ? '' : `, maximum ${comment.maxWidth}`})`,\n '',\n ...(notes.length > 0 ? notes.map((note) => `- ${note}`) : ['- no measured issues; review the wording and flow']),\n '',\n `${fence}${comment.file.endsWith('.rs') ? 'rust' : 'ts'}`,\n comment.text,\n fence,\n ].join('\\n');\n}\n\nexport function loadRules(agent: AgentConfig, configPath: string | undefined): string {\n let rules = DEFAULT_RULES;\n\n if (agent.rules) {\n const base = configPath ? path.dirname(configPath) : process.cwd();\n\n rules = readFileSync(path.resolve(base, agent.rules), 'utf8');\n }\n\n return agent.instructions ? `${rules.trimEnd()}\\n\\n## Instructions\\n\\n${agent.instructions.trim()}\\n` : rules;\n}\n\nexport function buildPrompt(options: {\n readonly rules: string;\n readonly root: string;\n readonly comments: readonly CommentReport[];\n}): string {\n return [\n options.rules.trimEnd(),\n '',\n '## Task',\n '',\n `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.`,\n '',\n '- 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.',\n '- 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.',\n '- 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.',\n '- 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.',\n `- Notes marked \\`${HINT_MARKER}\\` come from heuristics. If the rule doesn't actually apply, don't change anything for that note.`,\n '- 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.',\n \"- Edit only these comments. Don't change code, other comments, or other files.\",\n '- When done, reply with one line per comment saying what you changed or why you left it.',\n '',\n ...options.comments.map((comment) => describeComment(comment)).flatMap((section) => [section, '']),\n ].join('\\n');\n}\n\nexport function shellQuote(value: string): string {\n return /^[\\w./:@=-]+$/.test(value) ? value : `'${value.replaceAll(\"'\", String.raw`'\\''`)}'`;\n}\n\nexport function buildInvocation(options: {\n readonly agent: AgentConfig;\n readonly runner: AgentRunner;\n readonly prompt: string;\n readonly root: string;\n}): AgentInvocation {\n const { agent, prompt, root } = options;\n const extra = agent.args ?? [];\n\n if (agent.command && agent.command.length > 0) {\n const [command, ...args] = agent.command;\n const hasPlaceholder = args.some((arg) => arg.includes('{prompt}'));\n\n return {\n command,\n // a function keeps the prompt as it is, while a string would expand `$&` or `$'` in a comment's text\n args: args.map((arg) => arg.replaceAll('{prompt}', () => prompt)),\n stdin: hasPlaceholder ? undefined : prompt,\n };\n }\n\n const model = agent.model ? ['--model', agent.model] : [];\n\n // the agent only reads and edits files, so it gets no shell\n if (options.runner === 'claude') {\n return {\n command: 'claude',\n args: [\n '-p',\n '--no-session-persistence',\n '--model',\n agent.model ?? DEFAULT_CLAUDE_MODEL,\n '--permission-mode',\n 'acceptEdits',\n '--allowedTools',\n 'Read',\n 'Edit',\n 'Grep',\n 'Glob',\n ...extra,\n ],\n stdin: prompt,\n };\n }\n\n if (options.runner === 'codex') {\n return {\n command: 'codex',\n args: [\n 'exec',\n '--ephemeral',\n '--sandbox',\n 'workspace-write',\n '--cd',\n root,\n '-c',\n 'features.hooks=false',\n ...model,\n ...extra,\n '-',\n ],\n stdin: prompt,\n };\n }\n\n // cursor-agent has no flag to skip hooks, so the child relies on the `COMMENTS_HOOK_CHILD` guard\n return {\n command: 'cursor-agent',\n args: ['-p', '--trust', '--force', '--workspace', root, ...model, ...extra, prompt],\n };\n}\n\nexport function describeAgent(agent: AgentConfig, runner: AgentRunner): string {\n if (agent.command && agent.command.length > 0) {\n return path.basename(agent.command[0]);\n }\n\n const model = runner === 'claude' ? (agent.model ?? DEFAULT_CLAUDE_MODEL) : agent.model;\n\n return model ? `${RUNNER_COMMANDS[runner]}, model ${model}` : RUNNER_COMMANDS[runner];\n}\n\nexport function formatInvocation(invocation: AgentInvocation): string {\n const args = invocation.args.map((arg) => (arg.length > 80 ? '<prompt>' : shellQuote(arg)));\n\n return [invocation.command, ...args].join(' ');\n}\n\nexport function runAgent(\n invocation: AgentInvocation,\n options: { root: string; timeout?: number },\n): Promise<AgentResult> {\n const timeout = options.timeout ?? DEFAULT_TIMEOUT_SECONDS;\n\n return new Promise((resolve) => {\n const child = spawn(invocation.command, invocation.args, {\n cwd: options.root,\n env: { ...process.env, [CHILD_ENV]: '1' },\n });\n\n const timers: NodeJS.Timeout[] = [];\n\n let stdout = '';\n let stderr = '';\n let timedOut = false;\n let settled = false;\n\n const settle = (result: Omit<AgentResult, 'output'>): void => {\n if (settled) {\n return;\n }\n\n settled = true;\n\n for (const timer of timers) {\n clearTimeout(timer);\n }\n\n // pipes that a process left behind by the agent still holds would keep this process from exiting\n child.stdout.destroy();\n child.stderr.destroy();\n child.unref();\n\n resolve({ ...result, output: `${stdout}${stderr}`.trim() });\n };\n\n const finish = (code: number | null, signal: NodeJS.Signals | null): void => {\n if (timedOut) {\n settle({ ok: false, error: `the agent timed out after ${timeout}s` });\n } else if (code === 0) {\n settle({ ok: true });\n } else {\n settle({ ok: false, error: `the agent exited with code ${code ?? signal}` });\n }\n };\n\n timers.push(\n setTimeout(() => {\n timedOut = true;\n\n child.kill();\n\n // an agent that ignores the signal is killed, and it counts as stopped even if that fails too\n timers.push(\n setTimeout(\n () => {\n child.kill('SIGKILL');\n finish(null, 'SIGKILL');\n },\n Math.min(KILL_GRACE_MS, timeout * 1000),\n ),\n );\n }, timeout * 1000),\n );\n\n child.stdout.setEncoding('utf8').on('data', (chunk: string) => {\n stdout += chunk;\n });\n\n child.stderr.setEncoding('utf8').on('data', (chunk: string) => {\n stderr += chunk;\n });\n\n child.on('error', (error: NodeJS.ErrnoException) => {\n settle({\n ok: false,\n error: error.code === 'ENOENT' ? `\\`${invocation.command}\\` is not installed` : error.message,\n });\n });\n\n child.on('close', finish);\n\n // `close` waits for the pipes, so it may come long after the agent exited, or never\n child.on('exit', (code, signal) => {\n timers.push(\n setTimeout(() => {\n finish(code, signal);\n }, DRAIN_GRACE_MS),\n );\n });\n\n // an agent that exits without reading its stdin would otherwise crash the process with EPIPE\n child.stdin.on('error', () => {});\n child.stdin.end(invocation.stdin ?? '');\n });\n}\n","import { existsSync, readFileSync } from 'node:fs';\nimport { createRequire } from 'node:module';\nimport path from 'node:path';\nimport { pathToFileURL } from 'node:url';\n\nexport type AgentRunner = 'claude' | 'codex' | 'cursor';\nexport type HookMode = 'agent' | 'report' | 'off';\nexport type AgentScope = 'suspicious' | 'failing' | 'all';\n\nexport interface AgentConfig {\n /**\n * Names the agent CLI to spawn. Defaults to the host that triggered the hook, or `claude`\n * when run standalone.\n */\n readonly runner?: AgentRunner;\n\n /**\n * Names the model passed to the runner. Defaults to `haiku` for Claude Code and to the\n * CLI's default otherwise.\n */\n readonly model?: string;\n\n /**\n * Lists extra arguments appended to the runner's command line.\n */\n readonly args?: readonly string[];\n\n /**\n * Sets a fully custom command, e.g. `['my-agent', '--prompt', '{prompt}']`. Without a `{prompt}`\n * placeholder, the prompt is written to stdin. Overrides `runner`, `model`, and `args`.\n */\n readonly command?: readonly string[];\n\n /**\n * Sets how many seconds to wait for each agent before giving up. Defaults to 300.\n */\n readonly timeout?: number;\n\n /**\n * Sets the most comments to hand to one agent. More comments are split across several agents.\n * A file's comments always go to the same agent, since parallel agents editing one file would\n * overwrite each other's edits, so a file with more comments than this gets an agent of its\n * own. Defaults to 20.\n */\n readonly batchSize?: number;\n\n /**\n * Sets the most files to hand to one agent, since an agent editing many files is slower and less\n * careful. More files are split across several agents. Defaults to 10.\n */\n readonly maxFiles?: number;\n\n /**\n * Caps how many agents run at once. The other agents wait, and each one starts when a running\n * agent finishes. Defaults to 6.\n */\n readonly maxAgents?: number;\n\n /**\n * Points to a Markdown file with formatting rules, relative to the config file.\n * Replaces the built-in rules.\n */\n readonly rules?: string;\n\n /**\n * Holds extra instructions appended to the rules.\n */\n readonly instructions?: string;\n\n /**\n * Selects which comments the agent sees. `suspicious` (default) is a triage, not a full review.\n * It only sends comments with hints, like a possible semicolon or a line that may end with the\n * first word of a sentence, and comments with issues in parts the tool never rewraps, so most\n * runs never call the agent. The tool fixes the layout of prose itself. `failing` sends only\n * the comments with verdicts, the ones that fail `comments check`, and no comment that only has\n * hints. `all` sends every comment in changed lines, so that the agent reviews the wording of\n * each against its rules.\n */\n readonly scope?: AgentScope;\n}\n\nexport interface HookConfig {\n /**\n * Sets what the stop hook does with comments in changed lines.\n *\n * - `agent` (default): hand them to an agent and report any that still don't fit afterwards.\n * - `report`: only report suspicious comments.\n * - `off`: do nothing.\n */\n readonly mode?: HookMode;\n}\n\nexport interface CommentsConfig {\n /**\n * Sets the preferred line length for comments, including indentation and the comment\n * prefix. Defaults to prettier's `printWidth`, or 80. In Rust files, it defaults to\n * rustfmt's `comment_width` instead, which is 80 by default too.\n */\n readonly width?: number;\n\n /**\n * Sets the hard limit for measured prose. Defaults to 120, or `width` when it is larger.\n * Parentheticals may use the space between `width` and this limit to stay together.\n */\n readonly maxWidth?: number;\n\n /**\n * Sets the maximum length of the line that holds a comment whose whole text is one prose\n * paragraph. Such a comment stays on one line up to this length plus `tolerance`, even past\n * `width`, and is wrapped to `width` beyond it. Defaults to 120, and the line never exceeds\n * `maxWidth`.\n */\n readonly singleLineWidth?: number;\n\n /**\n * Sets how many columns prose lines may go past `width` when that saves a line in a paragraph\n * of three or more lines. A paragraph that already has the layout for a wider `width`, up to\n * `width` plus this many columns, also passes as it is. So does a paragraph with as many lines\n * as the rewrap whose lines are each at most this many columns shorter or longer than in the\n * rewrap. It never goes past `maxWidth`. When `maxWidth` is omitted, it also sets a legacy hard\n * limit of `width + tolerance`.\n */\n readonly tolerance?: number;\n\n /**\n * Keep the first word of a new sentence or parenthetical off the end of a line, e.g.\n * `...the module. This` or `...reflows (the` followed by the rest on the next line.\n * A paragraph of two lines also ends its first line with a sentence when the rest fits\n * on the second line and is no longer than the first, instead of with the start of\n * the next sentence, like `...the module. This one`. Other lines avoid ending with the\n * first two words of a sentence, like `...opens. It allocates`, unless that makes them\n * much less even. Defaults to `true`.\n */\n readonly avoidSentenceOrphans?: boolean;\n\n /**\n * Looks for possible style issues (semicolons, em-dashes and dashes, `label: explanation` colons,\n * asterisk emphasis, and section banners) and reports them as hints, so that the `suspicious` scope\n * hands those comments to the agent too. It only controls this search, and the agent's rules apply\n * to every comment the agent sees either way. Defaults to `true`.\n */\n readonly style?: boolean;\n\n /**\n * Lists the file extensions to process. Defaults to JavaScript, TypeScript, and Rust extensions.\n */\n readonly extensions?: readonly string[];\n\n /**\n * Lists glob patterns, relative to the repository root, of the only files to process.\n * Without patterns, every file with one of the `extensions` is processed. `ignore` still\n * skips files that match both.\n */\n readonly include?: readonly string[];\n\n /**\n * Lists glob patterns, relative to the repository root, of files to skip.\n */\n readonly ignore?: readonly string[];\n readonly agent?: AgentConfig;\n readonly hook?: HookConfig;\n}\n\nexport interface LoadedConfig {\n readonly config: CommentsConfig;\n readonly filepath?: string;\n}\n\nexport const DEFAULT_EXTENSIONS = Object.freeze(['.js', '.jsx', '.mjs', '.cjs', '.ts', '.tsx', '.mts', '.cts', '.rs']);\n\nconst CONFIG_FILES = [\n 'comments.config.ts',\n 'comments.config.mts',\n 'comments.config.js',\n 'comments.config.mjs',\n 'comments.config.cjs',\n '.commentsrc',\n '.commentsrc.json',\n];\n\nconst PACKAGE_JSON_KEY = 'comments';\n\nexport function defineConfig(config: CommentsConfig): CommentsConfig {\n return config;\n}\n\nfunction readJson(filepath: string): unknown {\n try {\n return JSON.parse(readFileSync(filepath, 'utf8'));\n } catch (error) {\n throw new Error(`comments: failed to parse ${filepath}: ${(error as Error).message}`, { cause: error });\n }\n}\n\nasync function readConfigFile(filepath: string): Promise<unknown> {\n if (filepath.endsWith('rc') || filepath.endsWith('.json')) {\n return readJson(filepath);\n }\n\n const module = (await import(pathToFileURL(filepath).href)) as { default?: unknown };\n\n return module.default ?? module;\n}\n\nfunction assertType(value: unknown, type: string, key: string, filepath: string): void {\n const actual = Array.isArray(value) ? 'array' : typeof value;\n\n if (value !== undefined && actual !== type) {\n throw new Error(`comments: \\`${key}\\` in ${filepath} must be a ${type}, got ${actual}`);\n }\n}\n\nfunction assertStrings(value: unknown, key: string, filepath: string): void {\n if (Array.isArray(value) && !value.every((item) => typeof item === 'string')) {\n throw new Error(`comments: \\`${key}\\` in ${filepath} must only hold strings`);\n }\n}\n\nexport function validateConfig(raw: unknown, filepath: string): CommentsConfig {\n if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {\n throw new Error(`comments: ${filepath} must export an object`);\n }\n\n const config = raw as Record<string, unknown>;\n\n assertType(config.width, 'number', 'width', filepath);\n assertType(config.maxWidth, 'number', 'maxWidth', filepath);\n assertType(config.singleLineWidth, 'number', 'singleLineWidth', filepath);\n assertType(config.tolerance, 'number', 'tolerance', filepath);\n assertType(config.avoidSentenceOrphans, 'boolean', 'avoidSentenceOrphans', filepath);\n assertType(config.style, 'boolean', 'style', filepath);\n assertType(config.extensions, 'array', 'extensions', filepath);\n assertType(config.include, 'array', 'include', filepath);\n assertType(config.ignore, 'array', 'ignore', filepath);\n assertType(config.agent, 'object', 'agent', filepath);\n assertType(config.hook, 'object', 'hook', filepath);\n assertStrings(config.extensions, 'extensions', filepath);\n assertStrings(config.include, 'include', filepath);\n assertStrings(config.ignore, 'ignore', filepath);\n\n for (const key of ['width', 'maxWidth', 'singleLineWidth', 'tolerance'] as const) {\n const value = config[key];\n const minimum = key === 'tolerance' ? 0 : 1;\n\n if (value !== undefined && (!Number.isInteger(value) || (value as number) < minimum)) {\n throw new Error(`comments: \\`${key}\\` in ${filepath} must be an integer of at least ${minimum}`);\n }\n }\n\n if (typeof config.width === 'number' && typeof config.maxWidth === 'number' && config.maxWidth < config.width) {\n throw new Error(`comments: \\`maxWidth\\` in ${filepath} must be at least \\`width\\``);\n }\n\n const agent = (config.agent ?? {}) as Record<string, unknown>;\n\n if (agent.runner !== undefined && !['claude', 'codex', 'cursor'].includes(agent.runner as string)) {\n throw new Error(`comments: \\`agent.runner\\` in ${filepath} must be one of claude, codex, cursor`);\n }\n\n assertType(agent.model, 'string', 'agent.model', filepath);\n assertType(agent.args, 'array', 'agent.args', filepath);\n assertType(agent.command, 'array', 'agent.command', filepath);\n assertType(agent.timeout, 'number', 'agent.timeout', filepath);\n assertStrings(agent.args, 'agent.args', filepath);\n assertStrings(agent.command, 'agent.command', filepath);\n\n // a timeout of zero would stop every agent right away, and `NaN` is a number too\n if (agent.timeout !== undefined && !((agent.timeout as number) > 0)) {\n throw new Error(`comments: \\`agent.timeout\\` in ${filepath} must be a positive number`);\n }\n\n for (const key of ['batchSize', 'maxFiles', 'maxAgents'] as const) {\n const value = agent[key];\n\n if (value !== undefined && (!Number.isInteger(value) || (value as number) < 1)) {\n throw new Error(`comments: \\`agent.${key}\\` in ${filepath} must be a positive integer`);\n }\n }\n\n assertType(agent.rules, 'string', 'agent.rules', filepath);\n assertType(agent.instructions, 'string', 'agent.instructions', filepath);\n\n if (agent.scope !== undefined && !['suspicious', 'failing', 'all'].includes(agent.scope as string)) {\n throw new Error(`comments: \\`agent.scope\\` in ${filepath} must be one of suspicious, failing, all`);\n }\n\n const hook = (config.hook ?? {}) as Record<string, unknown>;\n\n if (hook.mode !== undefined && !['agent', 'report', 'off'].includes(hook.mode as string)) {\n throw new Error(`comments: \\`hook.mode\\` in ${filepath} must be one of agent, report, off`);\n }\n\n return config as CommentsConfig;\n}\n\nexport async function loadConfig(cwd: string, options: { explicitPath?: string; stopDir?: string } = {}) {\n if (options.explicitPath) {\n const filepath = path.resolve(cwd, options.explicitPath);\n\n return { config: validateConfig(await readConfigFile(filepath), filepath), filepath } satisfies LoadedConfig;\n }\n\n let directory = path.resolve(cwd);\n\n for (;;) {\n for (const name of CONFIG_FILES) {\n const filepath = path.join(directory, name);\n\n if (existsSync(filepath)) {\n return { config: validateConfig(await readConfigFile(filepath), filepath), filepath } satisfies LoadedConfig;\n }\n }\n\n const packageJsonPath = path.join(directory, 'package.json');\n\n if (existsSync(packageJsonPath)) {\n const value = (readJson(packageJsonPath) as Record<string, unknown>)[PACKAGE_JSON_KEY];\n\n if (value !== undefined) {\n return { config: validateConfig(value, packageJsonPath), filepath: packageJsonPath } satisfies LoadedConfig;\n }\n }\n\n const parent = path.dirname(directory);\n\n if (directory === options.stopDir || parent === directory) {\n return { config: {} } satisfies LoadedConfig;\n }\n\n directory = parent;\n }\n}\n\ninterface PrettierOptions {\n readonly printWidth?: number;\n readonly tabWidth?: number;\n}\n\ninterface PrettierModule {\n resolveConfig: (file: string) => Promise<PrettierOptions | null>;\n}\n\nconst PRETTIER_FILES = ['.prettierrc', '.prettierrc.json', '.prettierrc.json5'];\n\nfunction readPrettierFile(directory: string, stopDir: string): PrettierOptions | undefined {\n let current = directory;\n\n for (;;) {\n for (const name of PRETTIER_FILES) {\n const filepath = path.join(current, name);\n\n if (!existsSync(filepath)) {\n continue;\n }\n\n const text = readFileSync(filepath, 'utf8');\n\n // `.prettierrc` may be YAML, so fall back to plain key lookups\n const printWidth = /[\"']?printWidth[\"']?\\s*:\\s*(\\d+)/.exec(text)?.[1];\n const tabWidth = /[\"']?tabWidth[\"']?\\s*:\\s*(\\d+)/.exec(text)?.[1];\n\n return {\n printWidth: printWidth === undefined ? undefined : Number(printWidth),\n tabWidth: tabWidth === undefined ? undefined : Number(tabWidth),\n };\n }\n\n const packageJsonPath = path.join(current, 'package.json');\n\n if (existsSync(packageJsonPath)) {\n const prettier = (readJson(packageJsonPath) as { prettier?: unknown }).prettier;\n\n if (prettier && typeof prettier === 'object') {\n return prettier as PrettierOptions;\n }\n }\n\n const parent = path.dirname(current);\n\n if (current === stopDir || parent === current) {\n return undefined;\n }\n\n current = parent;\n }\n}\n\nexport class PrettierOptionsResolver {\n private _prettier: PrettierModule | null | undefined;\n private readonly _cache = new Map<string, PrettierOptions>();\n\n constructor(private readonly _root: string) {}\n\n private async _loadPrettier(): Promise<PrettierModule | null> {\n if (this._prettier !== undefined) {\n return this._prettier;\n }\n\n try {\n const require = createRequire(path.join(this._root, 'package.json'));\n const resolved = require.resolve('prettier');\n\n this._prettier = (await import(pathToFileURL(resolved).href)) as PrettierModule;\n } catch {\n this._prettier = null;\n }\n\n return this._prettier;\n }\n\n async resolve(file: string): Promise<PrettierOptions> {\n const prettier = await this._loadPrettier();\n\n /**\n * Prettier resolves per file, since `overrides` can give files in the same\n * directory different widths.\n */\n if (prettier) {\n try {\n const options = await prettier.resolveConfig(file);\n\n if (options) {\n return options;\n }\n } catch {\n // fall back to reading the config files directly\n }\n }\n\n const directory = path.dirname(file);\n const cached = this._cache.get(directory);\n\n if (cached) {\n return cached;\n }\n\n const options = readPrettierFile(directory, this._root) ?? {};\n\n this._cache.set(directory, options);\n\n return options;\n }\n}\n\nconst RUSTFMT_FILES = ['rustfmt.toml', '.rustfmt.toml'];\n\n// rustfmt indents with four spaces by default\nconst RUSTFMT_TAB_SPACES = 4;\n\n/**\n * Reads the widths of Rust files from the nearest rustfmt config, like rustfmt does.\n * It maps `comment_width` to `printWidth`, since that is the width rustfmt wraps comments\n * at, and `tab_spaces` to `tabWidth`.\n */\nexport class RustfmtOptionsResolver {\n private readonly _cache = new Map<string, PrettierOptions>();\n\n constructor(private readonly _root: string) {}\n\n private _read(directory: string): PrettierOptions {\n const cached = this._cache.get(directory);\n\n if (cached) {\n return cached;\n }\n\n let options: PrettierOptions | undefined;\n\n for (const name of RUSTFMT_FILES) {\n const filepath = path.join(directory, name);\n\n if (existsSync(filepath)) {\n const text = readFileSync(filepath, 'utf8');\n const commentWidth = /^\\s*comment_width\\s*=\\s*(\\d+)/m.exec(text)?.[1];\n const tabSpaces = /^\\s*tab_spaces\\s*=\\s*(\\d+)/m.exec(text)?.[1];\n\n options = {\n printWidth: commentWidth === undefined ? undefined : Number(commentWidth),\n tabWidth: tabSpaces === undefined ? RUSTFMT_TAB_SPACES : Number(tabSpaces),\n };\n\n break;\n }\n }\n\n const parent = path.dirname(directory);\n\n options ??=\n directory === this._root || parent === directory ? { tabWidth: RUSTFMT_TAB_SPACES } : this._read(parent);\n\n this._cache.set(directory, options);\n\n return options;\n }\n\n resolve(file: string): PrettierOptions {\n return this._read(path.dirname(file));\n }\n}\n"]}