プロジェクトのドキュメントにはルールがあった。

「1ドキュメント、1責任」「45行を超えたら分割せよ」
私が書いたものだ。一昨日のことだ。

実際はどうだったか。

  • README(インデックス)がないフォルダー: 37中25
  • エンジニアリングルールを置いたフォルダー: 11ファイル、インデックスはゼロ
  • 45行ルールに違反しているドキュメント: 47
  • 最大違反: 1,203行

ルールを書き留めたからといって、それがルールになるわけではない。

当たり前の話だ。でも、自分が書いたルールを自分で47も破っているリストを突きつけられると、笑えなくなる瞬間がある。

(二日前の私は、本気で守るつもりだった。)

ルール自体を書き直したら、ルールファイルが150行になった

計画はすでに決まっていた。機械に強制させることだ。CIを落とす。

そのためには、まずルールを正しく書かなければならない。README必須、フォルダー構成、更新義務、CIが実際に何をチェックするのか。すべて書き終えたとき、ルールファイルは150行を超えていた。

ルールファイル自体が45行ルールを破っていた。

ここで「ルールファイルは特別だから除外する」と言うと、何が起きるか?

「ルールは例外」という前例を永久に作ったことになる。

それ以降、45行を超えるたびに「ルールファイルもそうしてるじゃないか」と言われる。そしてその言い分は正しい。

そこで分割した。8ファイル、すべて45行未満。

自分が守れないルールなら、最初から書く価値がなかった。

CIが導入された瞬間、既存の63件の違反が牙をむいた

本番の作業へ。チェッカーを書き、CIに組み込む。

実行すると当然のことながら:

63 violations

Enter fullscreen mode Exit fullscreen mode

すべて失敗。全赤。自分がこれから触るファイルも、数ヶ月誰も開いていないファイルも、等しく赤。

人間には二つの選択肢がある。

  1. 63件すべてを先に直してからCIを有効にする(1,203行の大物を抱えたまま)
  2. ignoreリストを追加して63件を黙らせ、先に進む

魅力的だと思わないか? 2番目が。63行の.lintignoreに「後で削除」とコメントを残す。

そのリストは、いつ削除するのか?

そのファイルが実際には何なのか考えてみてほしい。それは第二の真実の源泉だ。

ルールはルール文書に書かれる。本当に免除されるものは、ignoreリストにしか存在しない。二つのファイルが、徐々に乖離していく。

そしてignoreリストは決して縮まない。誰も一行を削除するインセンティブを持たない。もし縮んだとしても、それは偶然だ。

(「後で削除」の「後で」が本当に来たプロジェクトを、私は見たことがない。)

では1番目の選択肢、すべて先に直すのはどうか? これも罠だ。直している間はCIがない。ドキュメントを最も頻繁に触る時期が、ちょうど門番がいない時期になる。

1,200行のファイルで1文字のタイポを修正する人が、それを分割させられるべきか?

ここで私は立ち止まった。

1,203行のドキュメントで1文字のタイポを修正するPRを出すとする。ボーイスカウト・ルールを素朴に適用すると、そのPRは「このドキュメントは45行を超えています」で失敗する。

あなたは、1文字を直す人に1,203行の分割リファクタを要求したことになる。

そんなCIを持つチームはどうなるか?

誰もタイポを修正しなくなる。

壊れた窓の前を通り過ぎることが合理的な選択になる。ルールは改善を罰する機械に変わる。

では、CIは何に対して怒るべきか? このPRが持ち込んだ変更だけに対してだ。

  • 新しいファイルが45行超 → 失敗(これ以上債務を増やさない)
  • 既存の1,203行に触れた → 合格(あなたのせいではない)
  • しかし既存の1,203行が1,250行に増えた → 失敗(悪化させない)

「新規か既存か」ではなく、「増えたかどうか」

それにはignoreリストは必要ない。gitがすでに知っているからだ。マージベースと比較して、行数が増えたかどうか。それだけで判断できる。第二の真実の源泉は生まれない。

自分のルール文書が、自分のlintに弾かれた

この設計をルール文書に書き込んでいる最中に、CIが赤くなった。

LONG  docs/rules/doc-ci.md: 46 lines > 45

Enter fullscreen mode Exit fullscreen mode

自分だ。犯人は私だ。

「既存ファイルへの行追加は失敗」と実装した本人が、その機能を説明するファイルに行を追加して、失敗した。

私は笑った。そして直した(44行まで)。

自分の設計が機能している証明が、自分の首元から来るとは思っていなかった。

正直、感心した。止めるべき人を止める。書いた人だからといって甘くしない。私は自分自身を使って、自分が例外ではないことを確認できた。

ファイルの移動は、新しいファイルの追加とみなされる

もう一つ。

フォルダーの再編成は大量のgit mvを伴う。新しいパスでは、そのファイルはマージベースに存在しない。

CIにとっては新しく追加されたファイルだ。新規ファイルは容赦なく失敗するので、単に移動しただけの1,203行のファイルが分割を要求された。一度に10件。

リネームを追跡するようチェックを修正した。移動したファイルは、旧パスでの行数と比較する。増えていなければ失敗しない。

構造を変えるだけで無関係なリファクタを強いられることになるとは、実行するまで気づかなかった。

シークレットをパスで除外しようとした(そしてそれは間違っていた)

二つ目の門番があった。コミットにシークレット(APIキー、トークン)が含まれていないか。

ドキュメントのPRで失敗した。2件ヒット。

  • Authorization: Bearer <TOKEN_NAME> — 山括弧、明らかにプレースホルダー
  • 末尾が固定のダミー値のサンプルID、明らかに例

偽陽性。しかも私が今書いた行ではない。何ヶ月も前からあった行が、ファイルを分割したことで「新しく追加された」と再検知された。

誘惑が訪れる。

「docsフォルダー全体をスキャンから除外しよう」

一行で済む。一行で解決する。

やらなくて本当に良かった。

同じセッションのどこかで、本番用の署名用シークレットが平文でdocsに置かれているのを発見したからだ。私が書いた。私が忘れていた。

それを見つけたとき頭に浮かんだのは、「一行の逃げ道を選ばなくてよかった」という感謝だった。

もしフォルダーを除外していたら、そのシークレットはスキャンの外に、静かに、永遠に置かれていただろう。

場所ではなく形状を除外せよ。

山括弧のプレースホルダーという形状だけを免除する。本物のキーは異なる形状なので、必ず検知される。

両方向をテストした。

  • プレースホルダー → 検知されない(意図通り)
  • 本物のキーのようなものを投入 → 3つのルールが同時に発火、正しく失敗

この2つ目のチェックの方が、緑のチェックより重要だ。

失敗を見届けたことのない門番は、門番ではない。

債務がゼロになった日、昇格は3行で済んだ

そこから47件を分割した。63 → 50 → 36 → 27 → 17 → 0。

ドキュメントは最終的に99フォルダー、488ファイルになった。すべてのフォルダーにインデックスがあり、すべてのファイルが45行未満(コピー&ペーストしたスクリプト全体は文書化された例外で15件)。

債務ゼロになったので、CIを「すべてのファイル、常時、容赦なし」に昇格させた。

差分:

  • 環境変数にSTRICT: '1'を追加
  • 移行期の「触ったものだけチェック」を削除

3行。

ignoreリストを作らなかったので、解体すべき第二の真実の源泉がなかった。段階的ロールアウトはgitにすでに存在する事実だけで動いていたので、畳むコストはゼロだった。

あの63行をあの日に書いていたら、今、一行ずつ「まだ必要か?」と確認しながら削除しているところだ。3行では済まなかっただろう。

機械が担えること、担えないこと

最後に正直な注記を。

このCIは構造を保証する。欠けているインデックスはない。45行超のものはない。壊れたリンクはない。新規のシークレットもない。

文章が実装と一致しているかどうかは、機械には担えない。

コード変更時にドキュメント更新を要求する門番は作れる。でもそれは推測だ。本来通すべきPRを落とすか、守られているという心地よい感覚を捏造するか。どちらも何もないより悪い。

だから鮮度についてはルールに書いた。ドキュメントを読んで食い違いに気づいたら:実システムで検証できるものだけを修正せよ。検証できないなら、本文には触れず、issueを立てて疑わしいとマークせよ

推測で書き直すのは最悪の結果だ。なぜなら、その間違いに「レビュー済み」のバッジが付くからだ。

そのルールは作業中に2回発火した。一つはフォルダー構成の記述が現実と乖離していたとき(修正せず、issueとして記録)。もう一つは分割中に、過去の提案を却下した理由が消えかかっているのに気づき、復元したときだ。

なぜ却下されたのかは再構築できない。そして一度失われると、同じ提案が半年後に再び現れる。

結び

機械強制ルールについての議論は、通常「検知」についてだ。どうやって捕まえるか。

実際に重要だったのは、何を通すかを設計することだった。

これを間違えると、門番は二つの死に方をする。すべてを赤くして人々が無視するようになるか、ignoreリストが肥大化して何も守れなくなるか。

「後で削除」と注釈されたignoreリストは、後で削除されない。

だから最初から作らないことだ。