diff --git a/.github/instructions/test.instructions.md b/.github/instructions/test.instructions.md new file mode 100644 index 0000000..54c9953 --- /dev/null +++ b/.github/instructions/test.instructions.md @@ -0,0 +1,50 @@ +--- +applyTo: '**/*.ts' +--- + + +## テスト + +完全なテストは次のように行います。 +README.mdのチェックをtextlintで行い、ルールのテストは`textlint-tester`を使用します。 + +``` +npm test +``` + +ルールのテストは`textlint-tester`を使用して、ルールのテストを実装する + +Unit Testの実行方法 + +```bash +npm run test:unit +``` + +特定のルールのみをUnit Testする場合は、以下のように実行します。 + +```bash +npm run test:unit -- --grep no-repetitive-expressions +``` + +実際に `textlint` コマンドを使ってルールを適用する場合は、以下のように実行します。 + +``` +npm test +``` + +## ダミーファイルを使ったテスト + +- Git管理下にダミーファイルは作らない +- `tmp/` ディレクトリを作成し、そこにダミーファイルを配置してテストを行う + +``` +#! /bin/bash +set -ex + +mkdir -p tmp/ +npm install --save-dev . textlint technological-book-corpus-ja --prefix tmp/ +cd tmp +# ダミーファイルを作成 +echo "ダミーファイル" > dummy.md +./node_modules/.bin/textlint --preset @textlint-ja/ai-writing dummy.md +``` \ No newline at end of file diff --git a/src/rules/ai-tech-writing-guideline.ts b/src/rules/ai-tech-writing-guideline.ts index 434c8fb..b03d555 100644 --- a/src/rules/ai-tech-writing-guideline.ts +++ b/src/rules/ai-tech-writing-guideline.ts @@ -186,90 +186,173 @@ const rule: TextlintRuleModule = (context, options = {}) => { structure: 0 }; - return { - [Syntax.Paragraph](node) { - // StringSourceを使用してコードブロックを除外したテキストを取得 - const source = new StringSource(node, { - replacer({ node, emptyValue }) { - // コードブロック、インラインコードを除外 - if (node.type === "Code" || node.type === "InlineCode") { - return emptyValue(); + /** + * コロン(:)で終わる段落の直後に箇条書きが続くパターンを検出 + */ + const detectColonListPattern = (node: any) => { + const children = node.children || []; + + for (let i = 0; i < children.length - 1; i++) { + const currentNode = children[i]; + const nextNode = children[i + 1]; + + // Paragraph → List のパターンを検出 + if (currentNode.type === "Paragraph" && nextNode.type === "List") { + // Paragraphの最後の文字列ノードを取得 + const paragraphSource = new StringSource(currentNode, { + replacer({ node, emptyValue }) { + if (node.type === "Code" || node.type === "InlineCode") { + return emptyValue(); + } + return undefined; + } + }); + const paragraphText = paragraphSource.toString(); + + // 「:」で終わる段落の後にリストが続く場合を検出 + if (/[::][\s]*$/.test(paragraphText.trim())) { + // 許可パターンのチェック + if (allows.length > 0) { + const matches = matchPatterns(paragraphText, allows); + if (matches.length > 0) { + continue; + } } - return undefined; + + documentQualityMetrics.structure++; + hasDocumentIssues = true; + + report(currentNode, { + message: + "【構造化】コロン(:)で終わる文の直後の箇条書きは機械的な印象を与える可能性があります。「たとえば、次のような点があります。」のような導入文を使った自然な表現を検討してください。" + }); } - }); - const text = source.toString(); + } + } + }; + + /** + * 構造化ガイダンスに関する文書レベルの検出処理 + */ + const processDocumentStructureGuidance = (node: any) => { + if (disableStructureGuidance) { + return; + } - // 許可パターンのチェック - if (allows.length > 0) { - const matches = matchPatterns(text, allows); - if (matches.length > 0) { - return; + // コロン + 箇条書きパターンの検出 + detectColonListPattern(node); + // 将来的にここに他の文書レベルの構造化パターンを追加できます + // 例: + // detectExcessiveNestedLists(node); + // detectInconsistentHeadingStructure(node); + // detectPoorSectionOrganization(node); + // detectInconsistentListFormatting(node); + }; + + /** + * 段落内のガイダンスパターンを検出・報告 + */ + const processParagraphGuidance = (node: any) => { + // StringSourceを使用してコードブロックを除外したテキストを取得 + const source = new StringSource(node, { + replacer({ node, emptyValue }) { + // コードブロック、インラインコードを除外 + if (node.type === "Code" || node.type === "InlineCode") { + return emptyValue(); } + return undefined; } + }); + const text = source.toString(); - // 各カテゴリのガイダンスを統合 - const allGuidancePatterns = [ - ...(disableRedundancyGuidance ? [] : redundancyGuidance), - ...(disableVoiceGuidance ? [] : voiceGuidance), - ...(disableClarityGuidance ? [] : clarityGuidance), - ...(disableConsistencyGuidance ? [] : consistencyGuidance), - ...(disableStructureGuidance ? [] : structureGuidance) - ]; + // 許可パターンのチェック + if (allows.length > 0) { + const matches = matchPatterns(text, allows); + if (matches.length > 0) { + return; + } + } - for (const { pattern, message, category } of allGuidancePatterns) { - const matches = text.matchAll(pattern); - for (const match of matches) { - const index = match.index ?? 0; + // 各カテゴリのガイダンスを統合 + const allGuidancePatterns = [ + ...(disableRedundancyGuidance ? [] : redundancyGuidance), + ...(disableVoiceGuidance ? [] : voiceGuidance), + ...(disableClarityGuidance ? [] : clarityGuidance), + ...(disableConsistencyGuidance ? [] : consistencyGuidance), + ...(disableStructureGuidance ? [] : structureGuidance) + ]; - // プレーンテキストの位置を元のノード内の位置に変換 - const originalIndex = source.originalIndexFromIndex(index); - const originalEndIndex = source.originalIndexFromIndex(index + match[0].length); + for (const { pattern, message, category } of allGuidancePatterns) { + const matches = text.matchAll(pattern); + for (const match of matches) { + const index = match.index ?? 0; - if (originalIndex !== undefined && originalEndIndex !== undefined) { - const originalRange = [originalIndex, originalEndIndex] as const; + // プレーンテキストの位置を元のノード内の位置に変換 + const originalIndex = source.originalIndexFromIndex(index); + const originalEndIndex = source.originalIndexFromIndex(index + match[0].length); - // カテゴリ別のメトリクスを更新 - documentQualityMetrics[category as keyof typeof documentQualityMetrics]++; - hasDocumentIssues = true; + if (originalIndex !== undefined && originalEndIndex !== undefined) { + const originalRange = [originalIndex, originalEndIndex] as const; - report(node, { - message: message, - padding: locator.range(originalRange) - }); - } - } - } - }, - [Syntax.DocumentExit](node) { - // 文書全体の分析を実行(enableDocumentAnalysisがtrueの場合) - if (enableDocumentAnalysis && hasDocumentIssues) { - const totalIssues = Object.values(documentQualityMetrics).reduce((sum, count) => sum + count, 0); + // カテゴリ別のメトリクスを更新 + documentQualityMetrics[category as keyof typeof documentQualityMetrics]++; + hasDocumentIssues = true; - // カテゴリ別の詳細な分析結果を含むメッセージを生成 - const categoryDetails = []; - if (documentQualityMetrics.redundancy > 0) { - categoryDetails.push(`簡潔性: ${documentQualityMetrics.redundancy}件`); - } - if (documentQualityMetrics.voice > 0) { - categoryDetails.push(`明確性: ${documentQualityMetrics.voice}件`); - } - if (documentQualityMetrics.clarity > 0) { - categoryDetails.push(`具体性: ${documentQualityMetrics.clarity}件`); - } - if (documentQualityMetrics.consistency > 0) { - categoryDetails.push(`一貫性: ${documentQualityMetrics.consistency}件`); - } - if (documentQualityMetrics.structure > 0) { - categoryDetails.push(`構造化: ${documentQualityMetrics.structure}件`); + report(node, { + message: message, + padding: locator.range(originalRange) + }); } + } + } + }; - const detailsText = categoryDetails.length > 0 ? ` [内訳: ${categoryDetails.join(", ")}]` : ""; + /** + * 文書全体の品質分析結果を報告 + */ + const processDocumentAnalysis = (node: any) => { + // 文書全体の分析を実行(enableDocumentAnalysisがtrueの場合) + if (enableDocumentAnalysis && hasDocumentIssues) { + const totalIssues = Object.values(documentQualityMetrics).reduce((sum, count) => sum + count, 0); - report(node, { - message: `【テクニカルライティング品質分析】この文書で${totalIssues}件の改善提案が見つかりました${detailsText}。効果的なテクニカルライティングの7つのC(Clear, Concise, Correct, Coherent, Concrete, Complete, Courteous)の原則に基づいて見直しを検討してください。詳細なガイドライン: https://github.com/textlint-ja/textlint-rule-preset-ai-writing/blob/main/docs/tech-writing-guidelines.md` - }); + // カテゴリ別の詳細な分析結果を含むメッセージを生成 + const categoryDetails = []; + if (documentQualityMetrics.redundancy > 0) { + categoryDetails.push(`簡潔性: ${documentQualityMetrics.redundancy}件`); + } + if (documentQualityMetrics.voice > 0) { + categoryDetails.push(`明確性: ${documentQualityMetrics.voice}件`); + } + if (documentQualityMetrics.clarity > 0) { + categoryDetails.push(`具体性: ${documentQualityMetrics.clarity}件`); + } + if (documentQualityMetrics.consistency > 0) { + categoryDetails.push(`一貫性: ${documentQualityMetrics.consistency}件`); + } + if (documentQualityMetrics.structure > 0) { + categoryDetails.push(`構造化: ${documentQualityMetrics.structure}件`); } + + const detailsText = categoryDetails.length > 0 ? ` [内訳: ${categoryDetails.join(", ")}]` : ""; + + report(node, { + message: `【テクニカルライティング品質分析】この文書で${totalIssues}件の改善提案が見つかりました${detailsText}。効果的なテクニカルライティングの7つのC(Clear, Concise, Correct, Coherent, Concrete, Complete, Courteous)の原則に基づいて見直しを検討してください。詳細なガイドライン: https://github.com/textlint-ja/textlint-rule-preset-ai-writing/blob/main/docs/tech-writing-guidelines.md` + }); + } + }; + + return { + [Syntax.Document](node) { + // 文書レベルの構造化ガイダンス処理 + processDocumentStructureGuidance(node); + }, + [Syntax.Paragraph](node) { + // 段落内のガイダンスパターンを検出・報告 + processParagraphGuidance(node); + }, + [Syntax.DocumentExit](node) { + // 文書全体の品質分析結果を報告 + processDocumentAnalysis(node); } }; }; diff --git a/test/rules/ai-tech-writing-guideline.test.ts b/test/rules/ai-tech-writing-guideline.test.ts index d1a383d..b1f2e73 100644 --- a/test/rules/ai-tech-writing-guideline.test.ts +++ b/test/rules/ai-tech-writing-guideline.test.ts @@ -17,6 +17,15 @@ tester.run("ai-tech-writing-guideline", rule, { { text: "システムがデータを検証します。エラーが発生した場合、ログファイルに記録されます。", options: { enableDocumentAnalysis: false } + }, + // 自然な箇条書きの導入例 + { + text: "Vueのリアクティビティシステムは確かに便利ですが、その仕組みの見えにくさが気になります。\nたとえば、次のような点が見えにくいと感じます。\n\n- refとreactiveの使い分けが最初は分からない。", + options: { enableDocumentAnalysis: false } + }, + { + text: "JSXはJavaScriptの中でUIを記述するため、プログラマーにとって理解しやすいです。\nたとえば、JSXの次のような点がわかりやすいと思っています。\n\n- 条件分岐やループは通常のJavaScriptの記法", + options: { enableDocumentAnalysis: false } } ], invalid: [ @@ -105,6 +114,27 @@ tester.run("ai-tech-writing-guideline", rule, { } ] }, + // 構造化の問題(コロンと箇条書きの組み合わせ) + { + text: "Vueのリアクティビティシステムは確かに便利ですが、その仕組みの見えにくさが気になります。例えば:\n\n- refとreactiveの使い分けが最初は分からない。", + options: { enableDocumentAnalysis: false }, + errors: [ + { + message: + "【構造化】コロン(:)で終わる文の直後の箇条書きは機械的な印象を与える可能性があります。「たとえば、次のような点があります。」のような導入文を使った自然な表現を検討してください。" + } + ] + }, + { + text: "JSXはJavaScriptの中でUIを記述するため、プログラマーにとって理解しやすいです:\n\n- 条件分岐やループは通常のJavaScriptの記法", + options: { enableDocumentAnalysis: false }, + errors: [ + { + message: + "【構造化】コロン(:)で終わる文の直後の箇条書きは機械的な印象を与える可能性があります。「たとえば、次のような点があります。」のような導入文を使った自然な表現を検討してください。" + } + ] + }, // 複数の問題が同時に存在する場合 { text: "まず最初に高速なパフォーマンスの実装を実施することができます。",