パーティーゲームのサイトを個人で作っていて、記事が 40 本たまりました (2026-08-09 時点。この記事を入れて 41 本)。宣伝のつもりで書き始めたのですが、ある晩だけリポジトリの検査になりました。

その晩にマージした記事の、コミットタイトルがこれです。

Add GA4 event design article, and correct the repo's wrong reason
Add a genre guide, the first article carrying affiliate products
Add topic combination design article, and fix the rule it was based on
Add theme token design article, and audit where the one layer leaks
Add きもちバレ acting guide, and fix the biased feeling draw

5 本中 4 本が、記事を足すだけで終わっていません。ただし先に打率を出します。記事を追加したコミットは全部で 40 本。そのうち「記事を書く過程で見つかった、記事より前からあった誤り」を直しているのは 6 本でした。7 本に 1 本です。この記事で扱う 4 件は 01:27 から 04:01 までの同じ晩ですが (3 本が修正、1 本は issue 止まり)、残る 3 本は 8/2・8/3・8/4 に 1 本ずつです。

ちなみにこの数字は2 回間違えました。走査結果を途中で切ったまま数えて「3 本」、判定の基準を決めずに数えて「5 本」。1 回目には、自分のコミットメッセージに Found by the article fact-check と書いてある 1 本を落としていました。

出てきた 4 件

書いていた記事壊れていた主張実際は
計測イベントの設計壊れていた主張数値で送ると内訳が取れない (コメント・設計ドキュメント・コミットメッセージの 3 か所にあった)実際はウェブなら整数値もディメンションに登録できる。公式ヘルプに逆が書いてあった
お題の組み合わせ生成壊れていた主張もの生成型のパターンは構造的に収束する (作問規則の正本)実際は効く軸は「ぶつかる相手が手元で引ける形で閉じているか」。有限とは別
テーマトークンの設計壊れていた主張色の定義は CSS の 1 か所に閉じている実際は3 か所あり、1 本のゲームが実際に別テーマの色で出ていた
演技当てゲームの攻略壊れていた主張(仕様どおり動いていた)実際は抽選が「直前と同じなら 1 つ隣にずらす」実装で、隣の 1 つだけが 2 倍出やすかった

上の 2 件は「処方は合っていたが、理由が違った」型です。文字列でパラメータを送るのも、お題の主語を架空の合成語にするのも、実際に動いていました。動いたので疑う機会がなく、間違った理由が正本に残った。

3 件目はそこに収まりません。手で運ぶという処方自体が、12 本のゲームのうち 1 本で失敗していました。それでも QR は読めるので誰も気づきません。4 件目は「意図どおり動いていたが、副作用を見ていなかった」型です。

なお 2 件目は、書き直した規則のほうも誤っていました。2 人のレビュアーに独立に指摘され、2 度目の書き直しをしています。1 巡では収束しません。

ただし、2 件は仕組みで拾えたはずだった

正直に分けます。4 件を一色に塗ると嘘になります。

1 件目と 2 件目は、散文で書かれた因果の誤りです。型もテストも lint も当たりません。一次情報を引き直す以外に検出手段がありません。

3 件目と 4 件目は違います。色の直値は grep 一発で見つかりますし、このリポジトリには「規約を静的に検査する check-*.mjs」がすでに 2 本あって pnpm check に組み込まれています。3 本目を書いていなかっただけです。抽選の偏りも一様性テスト 1 本で落ちます——というより、この抽選にはテストが 1 本もありませんでした。しかも個別の見落としではなく、抽選のモジュールは 11 本あって、どれもエンジンのディレクトリの外にいます。純関数の規約とテスト群が効くのは中だけで、当時テストがあった抽選は 1 本、いまでも 11 本中 9 本にありません。

この 2 件について正確なのは「原理的に拾えない」ではなく「一度差分レビューを通したあと、誰も読み返さなかった」です。対策は記事より gate とテストのほうが安い。

いちばん新しい 1 件を、最初から

その 4 件目を詳しく書きます。

きもちバレは、セリフに秘密の「きもち」を込めて言い、聞いていた全員がどのきもちだったかを当てるゲームです。その攻略記事で、当てる側のコツを 1 つ書きたいと思っていました。直前の手番と同じきもちは出ない——2 手番目からは 8 択が実質 7 択になる、という話です。

仕様としては本当で、抽選のコードには「直前の手番と同じきもちは避ける (連続すると投票が消去法になりやすいため)」というコメントまで付いていました。問題は実装のほうです。

let feeling = Math.min(deck.feelings.length - 1, Math.floor(random() * deck.feelings.length));
if (feeling === prevFeeling) {
  feeling = (feeling + 1) % deck.feelings.length;
}

衝突したら 1 つ隣にずらす。 これだと押し出されたぶんが隣に上乗せされます。8 択なら、直前のきもちの「次」だけが 1/8 + 1/8 = 25%、ほかは 12.5%。直前と同じものが出ないという目的は達成できているので、遊んでいて気づく理由がありません。

厄介なことに、これは攻略として書けてしまいます。「迷ったら直前の正解の次を選べ」は当たる確率が 2 倍になる助言で、載せた瞬間、読んだ人だけが有利になります。なので書く前に直しました。直前のきもちを候補から外し、残り 7 つから均等に引く形です。

そのテストがこれです。乱数は外から注入しているので、配列から順に返す関数に差し替えれば、2 手番目の抽選値を総当たりできます (衝突したら引き直す形にしなかったのも同じ理由で、消費回数が変わるとこのテストが書けません)。

// セリフのシャッフルに乱数を食われないよう、1 行だけのデッキを使う
const oneLineDeck = { ...deck, lines: ['セリフ'] };
// 刻み数は 7 でも 8 でも割り切れる数にして、偏りを丸め誤差と区別する
const GRID = 5600;
const counts = new Array<number>(8).fill(0);
for (let k = 0; k < GRID; k++) {
  // 1 手番目を index 3 に固定し、2 手番目の抽選値を等間隔に刻む
  const cards = drawCards(oneLineDeck, 2, queuedRandom([3 / 8, (k + 0.5) / GRID]));
  counts[cards[1]!.feeling] += 1;
}
expect(counts).toEqual([800, 800, 800, 0, 800, 800, 800, 800]);

確率の話を、決定的な等値比較に落とせるのが利点です。

気をつけたのは、新しい実装にだけ都合よく通るテストにしないことでした。最初は 5600 でなく 7 等分で書いていて、これは旧実装でも通ります (7 分割の代表点を *8 して切り捨てると、厚くなる帯を踏まない)。旧実装に当てて初めて分かりました。

AssertionError: expected [ 700, 700, 700, +0, 1400, 700, …(2) ]
                to deeply equal [ 800, 800, 800, +0, 800, 800, …(2) ]

ひとつだけ 1400。これが探していた 2 倍です。

そして、この修正は新しいバグを 1 本作りました。 候補から 1 つ外したせいで、きもちが 1 個のデッキだと候補が空になり index が -1 で返ります。実データでは起きませんが、型の上では作れてしまう。捕まえたのはレビューで、上のテストではありません (8 択のデッキ 1 つしか使っていないので当たらない)。落ちるテストは、そのあとに足しました。誤りを取り除く工程が、同時に誤りを作っています。

検出していたのは、書いたことではないかもしれない

ここがこの記事でいちばん自信のないところです。

4 件のうち 3 件は、記事のレビューで指摘されて気づきました。レビュアーは人間ではなく LLM の subagent で、1 本につき 3〜4 体を並列で走らせ、別々の役をやらせます。効くのは役柄より参照先の指定です。「主張を実装ファイルと 1 つずつ突合せよ」「根拠のファイル・行を書け」と指示しておくと、粗探しの過程で実装まで読み直します

分かりやすかった指摘を 1 つ。僕は攻略記事に「後半の 4 つのきもちは互いにはっきり違う」と書き、根拠に紹介ページのプレイ例を引きました。返ってきたのは——その例の 2 つは、どちらも後半の 4 つですよ。自分で引いた実例が、自分の主張を反証していました。書いた本人は、根拠として引いたものを根拠のつもりでしか読みません。

QR の件も同じです。僕は「QR は Canvas に描いているので CSS を読めない」と書き、レビュアーがその主張を検証して覆しました。実際には DOM に挿さる SVG で、fillvar(--color-ink) と書けば済む。境界の外に出たから手で運んでいたのではなく、外に出たと思い込んで手で運んでいたわけです。

だとすると、効いていたのは「記事を書いたこと」ではなく「敵対的な読み手が、古い実装を一次情報に当てて読み直したこと」です。それは記事がなくてもできます。月 1 で「このモジュールの設計意図を一次情報つきで説明してみろ」と読ませれば同じかもしれない。この 4 件からは、その可能性を排除できません。 記事が足しているのは、締切と、公開する以上いいかげんに書けない圧力と、読者向けの粒度の要求だけです。

コストと、拾えないもの

拾えるのは「説明すると壊れる」型の欠陥だけです。性能・並行処理・メモリ・環境依存のように、説明が破綻しないまま存在できる欠陥は何本書いても出てきません。

コストのほうも。攻略記事の回では、126 行の記事を書いて、得たのは 19 行の修正と 75 行のテストでした。レビューは 3〜4 観点 × 1〜2 巡。しかも検出しても直るとは限りません——1 件は、10 ファイルに触る変更なので issue に切ったまま開いています。

同じ回で、返ってきた指摘のうち実装やデータの誤りだったのは 6 件、残りは文章の順番・規約・表現の話でした。6 件は全部、指摘のほうが正しかったです。ただし巡ごとの指摘総数は数えていません。「全部正しかった」は分母のない数字です。

まとめ

持ち帰れるものを、手法ではなく規則の形で 2 つ。1 つ、因果を書くときにリポジトリ内の既存コメントを根拠にしない (公式ドキュメントか実ファイルを引き直す)。2 つ、レビュアーが中心的な主張を否定してきたら、自分で一次情報を取って確かめる

打率は 7 本に 1 本で、半分は gate を書いていれば拾えたものでした。それでも、動いているものを説明させると、まず説明のほうが壊れます。

4 件の中身は計測イベントの設計お題の組み合わせ生成テーマトークンの設計に書きました。

ゲームのほうは mawashite.com で遊べます。