Skip to content

feat(textlint): AI 文体を検出するプリセットとフックを追加 - #103

Closed
rin2yh wants to merge 17 commits into
mainfrom
claude/textlint-ai-word-filter-67nl2h
Closed

feat(textlint): AI 文体を検出するプリセットとフックを追加#103
rin2yh wants to merge 17 commits into
mainfrom
claude/textlint-ai-word-filter-67nl2h

Conversation

@rin2yh

@rin2yh rin2yh commented Jul 31, 2026

Copy link
Copy Markdown
Owner

3 本に分割したのでこの PR は閉じる。

内容は分割時点のものをそのまま引き継いでいる。レビューでいただいた指摘はすべて反映済み。

claude added 7 commits July 31, 2026 01:22
LLM が書く日本語は、内容ではなく体裁で「ちゃんと書いている感」を出す癖がある。
preset-ja-technical-writing が形式(文長・表記ゆれ・文法)を見るのに対し、
情報を運んでいない語という中身の薄さを見るプリセットを自作して併用する。

- home/textlint/preset-ja-no-ai-tone: prh 辞書 YAML を単一の情報源にしたプリセット。
  機械的に直せる規則(ai-tone)と人間の判断が要る規則(ai-tone-review)を
  別のルール ID に分け、後者に --fix が当たらないようにした。
  expected に書き直しの方針を書けるのはこの分離があるため。
- home/claude/hooks/ai-tone.sh: PostToolUse で Markdown を textlint にかけ、
  Stop で直前の応答を検査する。textlint の起動は 1.5〜2 秒かかり毎ターンには重いので、
  会話の検査だけは辞書から生成した ripgrep パターンを使う。
  強度は CLAUDE_AI_TONE_CHAT / CLAUDE_AI_TONE_FILE で切り替える。
- home/claude/skills/ai-tone-dict: 辞書を継続更新するスキル。
  語を足す手順だけでなく、誤検出を段階的に外す手順も持たせた。
  うるさい規則が 1 件あると警告全体が信用されなくなるため。
- home/claude/rules/ai-tone.md: Markdown を触るときだけ読まれる執筆ルール。

新規追加ファイルは自身のプリセットで検査して指摘ゼロを確認済み。
スキルを改訂したときに挙動が変わっていないかを確かめる基準。
実際に来そうな依頼を 3 つ(誤検出の報告、語の追加、警告がうるさい)用意し、
それぞれに [critical] 付きのアサーションを置いた。

specs の整合を直接検査しないのは、prh が specs の不一致で辞書のロード自体を
落とすため、npm test の通過がそのまま証拠になるため。
スキルの評価実行で辞書の欠陥が 2 件出た。どちらも ai-tone.yml の
「無条件に --fix して壊れない」という約束を破っていた。

- 「しっかり」は裸で拾って削っていたため「しっかりした作り」が「した作り」になった。
  述語用法(しっかりした・しっかりしている)を除外するため、後続 1 文字を捕まえて
  $1 で書き戻す形にした。ripgrep に先読みが無く (?!し) は生成物から落ちるため。
  prh は複数 patterns を 1 本の選択に畳んでグループ番号がずれるので規則も分けた。
- 「〜を行う」は任意の漢字 2 文字以上で拾っていたため「儀式を行う」が「儀式する」になった。
  置換が成立するのはサ変名詞だけなので、対象を明示的に列挙した。

「幅広い」は ai-tone-review.yml から外した。「幅広い層に使われている」から語を消すと
命題が変わるので、この規則の基準(消しても意味が変わらない語)を満たしていない。

再発防止として dict/fix-safety.txt を追加した。ai-tone.yml の全規則を当てても
変わってはいけない文の一覧で、npm test が検査する。specs は規則の中でしか効かず、
別の規則を足した結果として壊れる事故は防げないため。

スキルと README には、from と to を同じにした specs を「この規則にマッチしないこと」の
表明として使う手法を追加した。判断の記録がそのまま見張りになる。
除外を [^し] で済ませていたため「土台をしっかりする」が「土台をする」に、
「しっかりされている」が「されている」になっていた。した/している だけを見て
除外文字を決めると、同じ語がつくる する/される/せず/させる を取りこぼす。
除外を [^しすせさ](と付きは [^しすせさと])に広げ、壊れる 4 形を fix-safety.txt に足した。

スキルの記述も直した。改訂で足した例が [^し] という狭い形だったため、
読んだ側が原理ではなく例をなぞって同じ穴を開けた(スキル改訂版の評価実行で再現)。
除外は思いついた形からではなく活用の一覧から作る、と手順を書き換えている。
iteration-1 の 3 ケースのうち 2 件は、そこで見つかった辞書のバグを直した結果
状況が消滅した。代わりに --fix の安全性と、外した語の見張りを突くケースを置いた。

add-adverb-fix-safety は実際に改訂版スキルの欠陥を検出したケース。
合格率だけを見ず、辞書に --fix を当てて本文が壊れないかを確かめること。
textlint が見るのは Markdown の本文だけで、YAML・シェル・JS のコメントは素通りする。
この構成では日本語の大半がコメント側にあり、プリセットを追加した変更自体が
22 ファイル中 17 ファイルを検査できていなかった。実際に当てたところ指摘が 11 件出た。

- lint-comments.mjs: コメント行を抜き出して疑似 Markdown に組み直し textlint にかける。
  文末の句点や文長のように断片には当てはまらない規則を外した設定を使う。
  「」で囲んだ語は Markdown のインラインコードと同じ扱いで検査対象から外す
  (辞書のコメントは検出対象の語を引用して説明するため)。
- フックの PostToolUse を .yml / .sh / .nix / .js などにも広げた。
- npm test に 5 番目の検査を足した。辞書を書く側の日本語が野放しなのは筋が通らない。

辞書の欠陥も 2 件直した。

- 「まとめると」が「一つの辞書にまとめると」のような条件の「〜と」まで拾っていた。
  prh には否定後読みが無いので、直前の助詞を捕まえて regexpMustEmpty で空を要求する。
- 「しっかり感」が「感のある作り」に壊れていた。除外にサ変語幹だけでなく感も並べた。
  スキルの評価実行で 1 エージェントが「きちんと感」として報告した形。
Comment thread home/claude/settings.json Outdated
},
{
"type": "command",
"command": "/Users/yuuki/.claude/hooks/ai-tone.sh"

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

絶対パスで書くのは微妙

claude added 3 commits July 31, 2026 02:30
「効く」は何がどう働くのかを名指ししない。「2 つの経路で効く」と書いても、
検査するのか止めるのか警告するのかが決まらない。空虚な動詞の節に入れた。
自分の書いた 12 箇所すべてが曖昧な用法で、うち 4 箇所は同じ文の繰り返しだった。

薬・ブレーキ・キャッシュのように語の側で動作が決まる用法は regexpMustEmpty で外した。
会話では「効いてる?」が自然なので scope: chat は付けず、文書だけで様子を見る。

settings.json のフックのパスを ~/.claude/hooks/ に変えた(レビュー指摘)。
statusLine が既にチルダを使っており、ユーザー名を埋め込む理由がない。
notify.sh も同じ問題を持っていたので揃えた。
この仕組みを入れた PR の本文自体に em dash が 2 箇所と「効く」が 1 箇所あった。
リポジトリに残らない文章はフックが拾えず、検査から漏れる。

lint-comments.mjs に --prose を足し、npm run lint:text で通せるようにした。
「」で囲んだ語をインラインコードとして外す扱いは共通で、
コミットメッセージが「効く」のように語そのものを引用する場面に必要になる。
lint:text を用意しただけでは足りなかった。実際に、それを追加したコミットの
メッセージ自体が検査を通っておらず、助詞の重複を 1 件そのまま push した。
「書いたら検査する」を記憶に任せると抜ける。

PreToolUse フックを足し、GitHub へ投稿する MCP ツールの body と title、
および git commit のメッセージを送信の直前で検査するようにした。
指摘があれば permissionDecision: deny で投稿そのものを止める。

コミットメッセージは -F・ヒアドキュメント・-m の 3 通りから取り出す。
取り出せなければ黙って通す。フックのせいでコミットできなくなるほうが困る。
日本語を含まない本文も対象外にした。英語の PR を止める理由がない。

強度は CLAUDE_AI_TONE_POST で off / warn / block を切り替えられる。
Comment thread README.md Outdated
└── Makefile
```

## 日本語の校正

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

linterのことを記載しなくて良い

Comment thread home/claude/hooks/ai-tone.sh Outdated
# 取り出せなければ何も返さず、フックは黙って通す。
commit_message_of() {
local command=$1 file
file=$(sed -n 's/.*-F[[:space:]]\{1,\}\([^[:space:]]\{1,\}\).*/\1/p' <<<"$command" | head -n 1)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sedとかむずいsh禁止

Comment thread home/claude/settings.json Outdated
{
"type": "command",
"command": "/Users/yuuki/.claude/hooks/notify.sh"
"command": "~/.claude/hooks/notify.sh"

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

この差分謎。別PRにして

Comment thread home/claude/skills/ai-tone-dict/SKILL.md

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

評価系はコミット不要なはず

レビューで 5 件の指摘を受けた。

sed と awk での文字列処理をやめた。コミットメッセージの取り出しと
コードブロックの除去を Node に移し、シェルは node を探して exec するだけにした。
非対話シェルでは mise の shim が PATH に無いことがあるので、その探索だけが残る。

SKILL.md から規則の書き方の実例を references/writing-rules.md へ分離した。
205 行から 164 行になり、手順を追うときに読む量が減る。

settings.json の notify.sh のパスは元に戻した。この PR の趣旨と関係がない。

評価用の JSON を削除した。実行結果はリポジトリに残す必要がない。

README から校正まわりの記述を落とした。詳細はプリセット側の README にある。
Comment thread home/claude/hooks/ai-tone.mjs Outdated

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jsマジで読みづらいので、なんとかして

claude added 6 commits August 12, 2026 14:16
JS が読みづらいという指摘と、共通実装を再利用したいという要望への対応。

シェルと Node に散っていた処理を Go の ai-tone に集約した。
フックも CLI も同じ実装を通る。textlint と prh は Node なので、
規則の当たり判定はこれまでどおりサブプロセスで呼ぶ。

減ったもの:

- ai-tone.sh と ai-tone.mjs
- gen-patterns.mjs、lint-comments.mjs、selftest.mjs
- dict/generated/ 一式

会話検査のために ripgrep 用パターンを生成していたが、Go の regexp は
ripgrep と同じ RE2 なので辞書 YAML を直接読める。生成物とその同期検査が
まるごと不要になった。npm の scripts も lint だけに減らした。

ai-tone check は辞書の回帰テストで、prh のロード、Go の regexp 互換、
--fix の安全性、辞書自身のコメントの 4 点を見る。
自分の判断で足したものを外した。

強度の環境変数(CLAUDE_AI_TONE_CHAT / FILE / POST)を削除した。
off / warn / block を 3 経路ぶん用意していたが、依頼は会話で出るたびに止めることなので
block だけでよい。分岐が 9 通りから 3 通りになった。

home/claude/rules/ai-tone.md を削除した。依頼はプリセット・スキル・フックの
3 つで、執筆ルールは 4 つ目として勝手に足したもの。
書き方の指針はスキルの references に残してある。

フックの応答を終了コード 2 と stderr に統一した。Stop・PostToolUse・PreToolUse の
いずれでも stderr が理由として渡り、その操作が止まると決まっているので、
発火点ごとに違う JSON を組み立てる必要がない。HookOutput と hookExtra を削除した。

コードは 1023 行から 940 行になった。
.textlintrc.fix.json を削除した。ai-tone-review を prh の linter だけで登録すれば
fixer が無いので --fix が構造的に届く先から外れる。設定で無効化する必要がなかった。
どの設定で --fix を実行するか、という運用上の約束も消えた。

.textlintrc.comments.json で外す規則を 6 つから 1 つに減らした。
原因は行のつなぎ方で、コメントは折り返した 1 文なのに 1 行ずつ渡していた。
そのため文末の句点や文の短さで誤検出が出て、それを設定で潰していた。
連続する行をつなぐようにしたら誤検出が 20 件から 2 件に減り、
残るのは節見出しの句点だけなので ja-no-mixed-period を外せば足りる。

正しい単位で判定するようになった結果、辞書のコメント 2 箇所が引っかかった。
読点の使いすぎと、節見出しが本文とつながって漢字が連続する形。どちらも直した。
誤検出を設定で黙らせていたせいで本物の指摘まで隠れていた。
コミットメッセージの検査を削除した。依頼は PR の本文で、コミットは私が足したもの。
git commit のコマンド文字列から -F・ヒアドキュメント・-m の 3 通りを取り出す処理と
その正規表現 4 本、テストが消えた。PreToolUse の対象も GitHub の MCP だけになった。

text サブコマンドを削除した。投稿の直前にフックが同じ検査を当てるので、
手で実行する入口を別に持つ理由がない。

check を圧縮した。辞書を lint に通すこと自体が prh のロードを兼ねるので、
ロード検証を独立した段として持つ必要がなかった。167 行から 116 行になった。

Go は 954 行から 813 行になった。
差分 7203 行のうち 5492 行が package-lock.json だった。生成物なので
.gitattributes に linguist-generated を書いてレビューで畳まれるようにした。
再現性のためファイル自体はコミットしたままにする。

プリセットの README からスキルの references と重なる部分を落とした。
prh の正規表現の制約と specs の書き方が両方に書いてあり、
片方を直してもう片方が古くなる形になっていた。書き手が読むのは references なので
そちらに寄せ、README からは参照だけにした。89 行から 41 行になった。

人が書いた分は 1665 行。
コードが多い原因は、フックが textlint を経由していたこと。
疑似 Markdown の組み立て、JSON の解析、段落から行番号への復元は
すべて textlint に渡すためだけの配管だった。

フックは辞書を直接当てるだけにした。3 つの発火点が同じ照合器を通る。
lint.go 186 行が scan.go 83 行になり、.textlintrc.comments.json も要らなくなった。

代わりに、書いたファイルへの文長や助詞の重複の指摘はフックでは出なくなる。
汎用プリセットの検査は ai-tone lint を手か CI で実行する側に寄せた。
フックが見るのは AI 文体の辞書だけ、という切り分けにしてある。

Go は 813 行から 678 行、人が書いた差分は 1665 行から 1524 行になった。
@rin2yh rin2yh closed this Aug 12, 2026
@rin2yh
rin2yh deleted the claude/textlint-ai-word-filter-67nl2h branch August 15, 2026 08:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants