プログラミング
私の本を過剰に設計してしまった方法
How I Over-Engineered My Book (ben.balter.com)
要約
著者は、自身の書籍執筆プロセスにおいて、開発者が日常的に使用するツール(Git、Markdown、CIパイプライン)を適用し、リンターや自動テストを含む高度な自動化システムを構築しました。このシステムは、文法チェックから内容の矛盾検出まで、約5,500もの自動チェックを実行し、執筆の品質と効率を極限まで高めることを目的としています。
全文翻訳
2026-08-17 目次
私の本には、「open source」のハイフン化を咎めるリンターがあります。
これは約5,500の自動チェックを実行し、git pushのたびに5つのフォーマットを再構築し、私がすでに辞めた職場でまだ働いているかのように示唆するだけでビルドを失敗させます。本のためにです。
私がこれを意図したわけではありません。
ほとんどの人は、WordやGoogle Docsを起動して執筆プロジェクトを開始しますが、私も同じ道を歩み始めました。目的特化型の執筆ツールも試しましたが、どのツールも開発者として毎日使うツールに劣っているように感じました。
私は「唯一合理的な」ことをして、それらをすべて捨て、Git、Markdown、CIビルドパイプライン全体で本を書き上げました。
私の家ネットワークを過剰に設計した話(2回)を読んだことがあるなら、これに驚くことはないでしょう。
私は何十年もウェブサイトを作成してきたので、その飛躍は短かったです。ウェブサイトを構築するのと同じツールで本を構築できるのです。標準的なWord文書を本格的な本に変えるための、最後の魔法のトリックの明かしは必要ありません。
間違いもありましたが、結局のところ、私はOpen and Asyncを他の方法では書きませんでした。
以下にその方法を示します。
コンテンツ
当然のことながら、コンテンツ自体はGitリポジトリ内のMarkdownファイルとして保存されていました。
結局のところ、私は一日の大半をそこで過ごしています。
私はVS Codeを使用し、いくつかのプローズ拡張機能(以下にリスト)を使用しました。
各章は独自のMarkdownファイルであり、単一のindex.ymlファイルが順序を定義しており、章の並べ替えや新しい章の追加が容易でした。
実際には、この本のほとんどをiPadで書きました。ブラウザタブのCodespaces、Bluetoothキーボード、しばしばデスクから離れた夜や週末に書きました。そしてGitリポジトリは、どこで開いてもすべてを同期させてくれました。
私は言葉に集中でき、悪いアイデアはgit revert一つで元に戻せました。
言うまでもなく、ESLintやPrettierでコードのリアルタイムフィードバックを得るのと同じように、IDEで執筆に関するリアルタイムフィードバックを得ることができました。
テスト
コンテンツをコードとして扱うことで、次の論理的なステップ、そして「合理的」が静かに姿を消した点は、自動テストを設定することでした。
プローズをコードをテストするのと同じようにテストすることは、私が何年も主張してきたことです。これは私がそれを不条理な極限まで推し進めた例です。
私はそれを2つの方法で行いました。リアルタイムとプッシュ時(CI)です。
リアルタイム
ローカルでは、タイプするたびに、いくつかのVS Code拡張機能がリアルタイムのフィードバックを提供していました。
具体的には:
Markdownlint — Markdownの構文とフォーマットの一貫性
Harper3 — 文法と単語の選択、完全にオンデバイス
LanguageTool — 文法、句読点、スタイル
Vale — 私自身のハウススタイルルールと禁止用語
Alex — 敏感または排除的な表現
Write-good — 弱いプローズ:受動態、曖昧な言葉、決まり文句
これら6つすべてがCIでも実行されました(AlexとWrite-goodはそこでValeに統合されました。詳細は後述)。
これらはすべて、完全な文法エンジン上にキュレーションされた数百のスタイルルールを重ね合わせました。すべてが、コードのエラーを赤色の波線でフラグ付けするのと同じように、私の間違いをリアルタイムで下線で示していました。
プッシュごと
CIでこれらのオープンソースリンターを実行すること(一部はブロック)に加えて、独自のカスタムテストスイートを構築しました。スタンドアロンのNodeスクリプトによるコンテンツバリデーター、Vitestスイート、Playwrightスペックです。
バリデーターは楽しい部分です。
それぞれが数行でMarkdownを読み取り、ファイルと行のポインター付きでエラーをフラグ付けします。
私のお気に入り:私はもうGitHubで働いていないので、まだ働いていると主張する文はビルドを失敗させます。
// 私のGitHubでの時間は過去形として読まれる必要があります。現在の雇用に関する現在形の主張はビルドを失敗させます。
function validateGitHubTense(files) {
const patterns = [
// "is/are ... at GitHub" — 現在の雇用を主張する
/(is|are)[^.!?
]{1,80}?at GitHub/i,
// "works/leads/runs at GitHub" — 現在の雇用活動
/(works?|leads?|runs?|manages?|directs?)(at|for)GitHub/i,
];
return flagLinesMatching(files, patterns);
}
そのバリデーターが存在するのは、一度間違えたからです。それが全体のパターンです。
最初のエラーが私を通り抜けたとき、私はその文を修正しただけではありませんでした。二度と目で見つけなくて済むようにルールを書きました。
プローズにとって、それはペットではなく家畜です。各章を手で世話するのではなく、ポリシーで群れ全体を管理します。
一度見つかった間違いは、すべての章をスキャンし、それが再び現れた場合にビルドを失敗させるチェックになります。
それは約30個のうちの1つです。
私が誇りに思っている他のものもあります。
validateOpenSourceHyphenation — 「open source」は動詞ではなく名詞であり、決してハイフンでつながれません。
validateHypotheticalHooks — 段落の冒頭にある定型的なAI風のオープナー(「Picture this…」、「Imagine…」、「Consider a…」)。
validateSentenceStarters — 同じ単語で始まる3つ以上の連続した文。
validateNoBareUrlLinkText — 表示テキストが単なる生のURLであるリンクはありません。
validateCalloutBalance — 本はマネージャーと個人貢献者の両方に語りかけるので、「For managers」というコールアウトには近くに「For ICs」の対応するものが必要です。
validateCrossReferences — すべての[text](#anchor)クロスリファレンスが実際の見出しに解決されます。
…さらに、小文字、emダッシュ、二重単語、enダッシュ範囲、TL;DRの長さ、その他私が名前を付けられるあらゆる癖のために、さらに数十個あります。
これらのうちの1つが私を捕らえました。
validateHypotheticalHooks は、私が自分で書いたと確信していた段落の冒頭をフラグ付けしました。そして実際にそうでした。
しかし、それを冷たく読み返すと、ゴーストライターに書かれたように聞こえました。生成されたテキストを読みすぎたことでそのリズムを吸収し、何も響かない流暢な模倣を生成してしまいました。
リンターは良いプローズと悪いプローズを区別できません。
それがフラグを立てることができるのは、あなたが考えるのをやめたときに手を伸ばすパターンです。これは、自分のドラフトでは見えないものとまったく同じことです。
eslint がその価値を発揮するのと同じ理由です。それは良いコードと悪いコードを区別できませんが、あなたの目がすぐに通り過ぎてしまう、自動操縦のミスを捉えます。
全体として、CIスイートは2,204のテストケースと約3,900のexpect()アサーションを持つ約70のテストファイルを実行しました。さらに、上記のバリデーターからの章ごとの構造チェックが約1,600件ありました。
監査
リンターは間違いを捉えましたが、本が繰り返し出てくるかどうか、または章が良いかどうかを教えてくれませんでした。
そのため、私は第二層のツールを構築しました。執筆をチェックするだけでなく、評価する監査です。
しかし、それらが決してしなかったことの一つは、それを書くことでした。
すべての言葉は私のものです。これらのツールは最後の手段の読者です。私が見えなくなったものを見つけます。
重複検出
本を何度も読み返した後、私は同じアイデアが章をまたいで繰り返されていると確信しました。
私は証拠が欲しかったのです。推測ではなく。そのため、前の層が見逃したものを見つける、3層の重複検出を構築しました。
jscpd — トークンレベルのコピー&ペースト検出。
章間で貼り付けた長い元のブロックを捉えますが、それより微妙なものは捉えません。
jscpd — トークンレベルのコピー&ペースト検出。章間で貼り付けた長い元のブロックを捉えますが、それより微妙なものは捉えません。
n-gram — index.ymlの各章をトークン化し、Markdown/Pandoc構文を削除し、単語のnグラム(n個の単語のフレーズ)を構築し、複数の章に出現するフレーズをフラグ付けします(さらに「最も繰り返される定型文」ランキングも)。
2つのモードがあります。クロスチャプター(デフォルト --n)とイントラチャプター(--scope=intra --n=8)で、章がそれ自体を繰り返す場合です。
semantic — 他の2つができないものです。同じ点が異なる言葉で言い換えられています。
オンデマンドのLLM監査で、モデルに本全体を渡さないように設計されています。3つのパスがあり、それぞれが小さな単位に対して実行されます。
intra — 章ごとに1回の呼び出し:「この章はどこで繰り返しを述べていますか?」
cross — 各章のTL;DR全体に対する単一の呼び出しで、概念的に重複する章のマップを生成します。
1回の呼び出しのコストで、本全体のスキャンが可能です。
arguments — 各章の負荷を支える主張を1回ずつ抽出し、最終的な1回の呼び出しで全章にわたる同じ主張をクラスタリングします。
クロスが見逃すものにまたがる本文の主張を捉えます。
私は本当に自分自身を繰り返していましたか?
この最終パスまでに、2つの機械的な層はクリーンになりました。しかし、それらはそれを獲得しました。jscpdとn-gramスキャンはすでにコピー&ペーストとリサイクルされたフレーズ(ここで二重のモチーフ、あそこで再利用されたTL;DR構造)を捉えており、私はそれぞれを修正しました。
どちらも捉えられなかったのは、より微妙な種類のものでした。もはや言葉通りに何も重複していませんでしたが、同じ点が異なる言葉で表現されていました。
セマンティックパスがそれを捉え、それは正しかったです。私は同じ主張をしました