From 35a07f550500b9cb1c9dc84e765d572a32f25013 Mon Sep 17 00:00:00 2001 From: Marc LeBlanc <7050295+marcleblanc2@users.noreply.github.com> Date: Fri, 11 Sep 2026 04:58:11 -0600 Subject: [PATCH 1/2] spell check: link summary items to the source, shorten them, flag unsorted dictionary entries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Summary comment: line and column link to the file in source view (?plain=1) with the word highlighted; each item is `word` → `first suggestion` instead of the whole line. - check-spelling.mjs reports entries added to cspell-allow-list.txt or cspell-block-list.txt out of alphabetical order (case- and accent-insensitive, like CSpell matches; comments and blank lines start a new run). Trailing '# comments' after a word are ignored, as CSpell does. - Drop the unused context field from the JSON findings. Amp-Thread-ID: https://ampcode.com/threads/T-01a08fee-74b4-76dc-aaf9-d1245d68fdc9 Co-authored-by: Amp --- dev/check-spelling.mjs | 57 +++++++++++++++++++++++++++++------- dev/post-spelling-review.mjs | 41 +++++++++++++++++++------- 2 files changed, 77 insertions(+), 21 deletions(-) diff --git a/dev/check-spelling.mjs b/dev/check-spelling.mjs index 733949636..43a2fddb7 100644 --- a/dev/check-spelling.mjs +++ b/dev/check-spelling.mjs @@ -1,7 +1,8 @@ #!/usr/bin/env node /** - * Reports CSpell findings on lines added by a Git diff. + * Reports CSpell findings on lines added by a Git diff, and dictionary entries + * added out of alphabetical order. * * Usage: node dev/check-spelling.mjs --base [--format text|json] * @@ -10,6 +11,7 @@ */ import {execFileSync, spawnSync} from 'child_process'; +import {readFileSync} from 'fs'; import path from 'path'; import {fileURLToPath} from 'url'; @@ -87,11 +89,45 @@ function runCSpell(files) { column: issue.col, word: issue.text, suggestions: issue.suggestions?.slice(0, 3) ?? [], - text: issue.line.text.replace(/\r?\n$/, ''), - context: issue.context?.text.trim() ?? issue.line.text.trim() + text: issue.line.text.replace(/\r?\n$/, '') })); } +// Entries in the dictionary files must be sorted, so duplicates stand out and +// merges are clean. Sorted the way CSpell matches: case- and accent-insensitive. +// Blank lines and comments start a new sorted run, so the lists can be sectioned. +const DICTIONARY_FILES = ['cspell-allow-list.txt', 'cspell-block-list.txt']; +const collator = new Intl.Collator('en', {sensitivity: 'base'}); + +function unsortedDictionaryEntries(files) { + const findings = []; + for (const file of files.filter(file => DICTIONARY_FILES.includes(file))) { + let previous; + readFileSync(file, 'utf8') + .split('\n') + .forEach((text, index) => { + if (/^\s*(#|$)/.test(text)) { + previous = undefined; + return; + } + const word = text.replace(/\s*#.*/, '').trim(); + if (previous && collator.compare(word, previous) < 0) { + findings.push({ + file, + line: index + 1, + column: 1, + word, + suggestions: [], + text, + message: `\`${word}\` is out of alphabetical order: it belongs before \`${previous}\`, the entry above it.` + }); + } + previous = word; + }); + } + return findings; +} + function findingsOnAddedLines(ranges, issues) { return issues.filter(issue => (ranges.get(issue.file) ?? []).some( @@ -102,15 +138,15 @@ function findingsOnAddedLines(ranges, issues) { function formatText(findings) { if (findings.length === 0) { - return 'No spelling errors found in added lines.\n'; + return 'No issues found in added lines.\n'; } const lines = [ - `Found ${findings.length} spelling error(s) in added lines:` + `Found ${findings.length} issue(s) in added lines:` ]; for (const finding of findings) { lines.push( - `${finding.file}:${finding.line}:${finding.column} - Unknown word (${finding.word})` + `${finding.file}:${finding.line}:${finding.column} - ${finding.message ?? `Unknown word (${finding.word})`}` ); } return lines.join('\n') + '\n'; @@ -125,10 +161,11 @@ async function main() { } const ranges = addedLineRanges(BASE); - const findings = findingsOnAddedLines( - ranges, - runCSpell([...ranges.keys()]) - ); + const files = [...ranges.keys()]; + const findings = findingsOnAddedLines(ranges, [ + ...runCSpell(files), + ...unsortedDictionaryEntries(files) + ]); process.stdout.write( FORMAT === 'json' ? JSON.stringify(findings, null, '\t') + '\n' diff --git a/dev/post-spelling-review.mjs b/dev/post-spelling-review.mjs index 38d18a88e..8501f367f 100644 --- a/dev/post-spelling-review.mjs +++ b/dev/post-spelling-review.mjs @@ -100,10 +100,27 @@ function groupByFile(findings) { return grouped; } +// Source view (?plain=1, so Markdown is not rendered) with the word highlighted +function sourceLink(finding) { + const {file, line, column, word} = finding; + const end = column + word.length; + return `https://github.com/${REPOSITORY}/blob/${HEAD_REF}/${file}?plain=1#L${line}C${column}-L${line}C${end}`; +} + +// `word` → `suggestion`, or the finding's own message for non-spelling +// findings such as an unsorted dictionary entry +function summaryItem(finding) { + if (finding.message) { + return finding.message; + } + const suggestion = bestSuggestion(finding); + return `\`${finding.word}\`${suggestion ? ` → \`${suggestion}\`` : ''}`; +} + function summaryBody(findings) { const lines = [ SUMMARY_MARKER, - `### ⚠️ CSpell found ${findings.length} spelling error(s) in this PR`, + `### ⚠️ Spell check found ${findings.length} issue(s) in this PR`, '', 'Only findings on lines added by this PR are shown.', ...(findings.length > MAX_INLINE_COMMENTS @@ -115,10 +132,10 @@ function summaryBody(findings) { ]; for (const [file, fileFindings] of groupByFile(findings)) { lines.push(`**\`${file}\`**`); - for (const {line, column, word, context} of fileFindings) { - const excerpt = context.replaceAll('`', "'").slice(0, 160); + for (const finding of fileFindings) { + const {line, column} = finding; lines.push( - `- line ${line}, column ${column}: \`${word}\` — \`${excerpt}\`` + `- [line ${line}, column ${column}](${sourceLink(finding)}): ${summaryItem(finding)}` ); } lines.push(''); @@ -207,13 +224,15 @@ function suggestionBlock(finding) { } function inlineBody(finding) { - return [ - `${INLINE_MARKER} ${finding.word} -->`, - `\`${finding.word}\` is not in the dictionary.`, - '', - ...suggestionBlock(finding), - `Please correct the spelling, or add the word to ${ALLOW_LIST_LINK} if it is correct.` - ].join('\n'); + const explanation = finding.message + ? [finding.message] + : [ + `\`${finding.word}\` is not in the dictionary.`, + '', + ...suggestionBlock(finding), + `Please correct the spelling, or add the word to ${ALLOW_LIST_LINK} if it is correct.` + ]; + return [`${INLINE_MARKER} ${finding.word} -->`, ...explanation].join('\n'); } async function syncInlineComments(findings) { From 2c4177cd4d1b339d55c9a0389dd4963fbe7b5a37 Mon Sep 17 00:00:00 2001 From: Marc LeBlanc <7050295+marcleblanc2@users.noreply.github.com> Date: Fri, 11 Sep 2026 05:32:09 -0600 Subject: [PATCH 2/2] spell check: put the fix under the line link; say which line an unsorted entry belongs on Amp-Thread-ID: https://ampcode.com/threads/T-01a08fee-74b4-76dc-aaf9-d1245d68fdc9 Co-authored-by: Amp --- dev/check-spelling.mjs | 27 +++++++++++++++++---------- dev/post-spelling-review.mjs | 19 ++++++++++++++----- 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/dev/check-spelling.mjs b/dev/check-spelling.mjs index 43a2fddb7..cf07ab1c9 100644 --- a/dev/check-spelling.mjs +++ b/dev/check-spelling.mjs @@ -102,27 +102,33 @@ const collator = new Intl.Collator('en', {sensitivity: 'base'}); function unsortedDictionaryEntries(files) { const findings = []; for (const file of files.filter(file => DICTIONARY_FILES.includes(file))) { - let previous; + let run = []; // [{word, line}] of the current sorted run readFileSync(file, 'utf8') .split('\n') .forEach((text, index) => { if (/^\s*(#|$)/.test(text)) { - previous = undefined; + run = []; return; } const word = text.replace(/\s*#.*/, '').trim(); - if (previous && collator.compare(word, previous) < 0) { + const line = index + 1; + const belongsBefore = run.find( + entry => collator.compare(word, entry.word) < 0 + ); + if (belongsBefore) { findings.push({ file, - line: index + 1, + line, column: 1, word, suggestions: [], text, - message: `\`${word}\` is out of alphabetical order: it belongs before \`${previous}\`, the entry above it.` + message: `\`${word}\` is out of alphabetical order: move it above \`${belongsBefore.word}\``, + relatedLine: belongsBefore.line // rendered as a link after the message }); + } else { + run.push({word, line}); } - previous = word; }); } return findings; @@ -144,10 +150,11 @@ function formatText(findings) { const lines = [ `Found ${findings.length} issue(s) in added lines:` ]; - for (const finding of findings) { - lines.push( - `${finding.file}:${finding.line}:${finding.column} - ${finding.message ?? `Unknown word (${finding.word})`}` - ); + for (const {file, line, column, word, message, relatedLine} of findings) { + const detail = message + ? `${message}${relatedLine ? ` on line ${relatedLine}` : ''}` + : `Unknown word (${word})`; + lines.push(`${file}:${line}:${column} - ${detail}`); } return lines.join('\n') + '\n'; } diff --git a/dev/post-spelling-review.mjs b/dev/post-spelling-review.mjs index 8501f367f..94eacb399 100644 --- a/dev/post-spelling-review.mjs +++ b/dev/post-spelling-review.mjs @@ -107,11 +107,19 @@ function sourceLink(finding) { return `https://github.com/${REPOSITORY}/blob/${HEAD_REF}/${file}?plain=1#L${line}C${column}-L${line}C${end}`; } -// `word` → `suggestion`, or the finding's own message for non-spelling -// findings such as an unsorted dictionary entry +// A non-spelling finding's own message, e.g. an unsorted dictionary entry, +// pointing at its `relatedLine` when it has one +function messageText(finding) { + const {file, message, relatedLine} = finding; + if (!relatedLine) return `${message}.`; + const url = `https://github.com/${REPOSITORY}/blob/${HEAD_REF}/${file}?plain=1#L${relatedLine}`; + return `${message} on [line ${relatedLine}](${url}).`; +} + +// `word` → `suggestion`, or the finding's own message function summaryItem(finding) { if (finding.message) { - return finding.message; + return messageText(finding); } const suggestion = bestSuggestion(finding); return `\`${finding.word}\`${suggestion ? ` → \`${suggestion}\`` : ''}`; @@ -135,7 +143,8 @@ function summaryBody(findings) { for (const finding of fileFindings) { const {line, column} = finding; lines.push( - `- [line ${line}, column ${column}](${sourceLink(finding)}): ${summaryItem(finding)}` + `- [line ${line}, column ${column}](${sourceLink(finding)})`, + ` - ${summaryItem(finding)}` ); } lines.push(''); @@ -225,7 +234,7 @@ function suggestionBlock(finding) { function inlineBody(finding) { const explanation = finding.message - ? [finding.message] + ? [messageText(finding)] : [ `\`${finding.word}\` is not in the dictionary.`, '',