Skip to content

feat: コロン(:)と箇条書きの機械的組み合わせパターンの検出機能を追加 #10

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions .github/instructions/test.instructions.md
Original file line number Diff line number Diff line change
@@ -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
```
219 changes: 151 additions & 68 deletions src/rules/ai-tech-writing-guideline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,90 +186,173 @@ const rule: TextlintRuleModule<Options> = (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);
}
};
};
Expand Down
30 changes: 30 additions & 0 deletions test/rules/ai-tech-writing-guideline.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down Expand Up @@ -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: "まず最初に高速なパフォーマンスの実装を実施することができます。",
Expand Down