[{"data":1,"prerenderedAt":624},["ShallowReactive",2],{"content-/code-review-iterative-refactor":3,"all-pages-for-dir":622,"og-image-/code-review-iterative-refactor":623},{"id":4,"title":5,"body":6,"category":602,"description":603,"extension":604,"meta":605,"navigation":606,"ogImage":607,"path":608,"project_name":609,"published":610,"publishedAt":611,"seo":612,"stem":613,"tags":614,"todo":620,"unpublished":610,"updatedAt":607,"__hash__":621},"pages/2026-05/2026-05-18/code-review-iterative-refactor.md","コードが散らかってきたのでサブエージェント並列レビュー＋Codex 3回ループで一気に大掃除した",{"type":7,"value":8,"toc":581},"minimark",[9,13,22,25,31,38,42,47,50,66,73,77,80,85,88,95,99,103,110,230,233,237,245,249,260,264,343,346,349,356,360,371,375,381,384,392,396,406,409,426,436,439,454,508,521,528,531,570,577],[10,11,12],"h2",{"id":12},"散らかってきたから一掃したくなった",[14,15,16,17,21],"p",{},"朝、",[18,19,20],"code",{},"git log"," をスクロールしながら直近のコミットを眺めていたら、機能追加と応急処置が層のように重なっていることに気づいた。Stripe Webhookの周辺、認証ミドルウェア、Excel関連ビュー、テストの skip 行。ファイルを開くたびに「ここ前から気になってた」が積み上がっていた。",[14,23,24],{},"そこでターミナルに一言だけ投げた。",[26,27,28],"blockquote",{},[14,29,30],{},"「コードが散らかってきたから vitest / playwright / coverage で大掃除したい。サブエージェントを並列に派遣して、構造・セキュリティ・パフォーマンス・SRE の4視点でレビューさせて」",[14,32,33,34,37],{},"人間がやったのはここまで。あとは Claude Code が4本のサブエージェントを同時に立ち上げて、それぞれの視点で ",[18,35,36],{},"apps/web/"," を走り回り始めた。",[10,39,41],{"id":40},"並列レビュー-codex-3回ループの流れ","並列レビュー → Codex 3回ループの流れ",[43,44,46],"h3",{"id":45},"ステップ1-サブエージェント4本を派遣","ステップ1: サブエージェント4本を派遣",[14,48,49],{},"4視点のレビュアーを並列に走らせた。",[51,52,53,57,60,63],"ul",{},[54,55,56],"li",{},"構造レビュー: ディレクトリ責務、循環依存、命名の一貫性",[54,58,59],{},"セキュリティレビュー: 認証境界、入力検証、シークレット露出",[54,61,62],{},"パフォーマンスレビュー: N+1、不要な watch、バンドル肥大",[54,64,65],{},"SRE レビュー: エラーハンドリング、ログ、Cloudflare 環境での挙動",[14,67,68,69,72],{},"各レビュアーが Markdown レポートを書き出したら、それらを統合した総合レポートを ",[18,70,71],{},"memo/2026-05-18/code-review-comprehensive.md"," に1本にまとめてもらった。指摘は Critical / High / Medium の3階建てに分類した。",[43,74,76],{"id":75},"ステップ2-codex-に致命的なものだけ言ってと頼む","ステップ2: Codex に「致命的なものだけ言って」と頼む",[14,78,79],{},"統合レポートと優先順位の妥当性を Codex (gpt-5.5) にレビューしてもらった。1回目で返ってきた指摘が刺さった。",[26,81,82],{},[14,83,84],{},"「PERF系をCriticalに入れているが、これは緊急度が下がる。本当に直さないと事故るのはアトミック性が崩れている Stripe Webhook と、認可フラグの既定値、依存ライブラリの CVE の3つだ」",[14,86,87],{},"優先順位を間違えていた。PERF を Critical から下ろして、見落としていた依存パッケージの CVE を Critical に繰り上げた。",[14,89,90,91,94],{},"その後、修正方針を反映するたびに ",[18,92,93],{},"codex exec resume --last"," で文脈を引き継ぎながらレビューを回し、3回目でようやく「致命的な指摘なし」が返ってきた。Critical の輪郭がここで固まった。",[10,96,98],{"id":97},"critical-3件で実際に直したもの","Critical 3件で実際に直したもの",[43,100,102],{"id":101},"stripe-webhookのアトミック化","Stripe Webhookのアトミック化",[14,104,105,106,109],{},"Webhookでサブスクリプション情報を反映するときに、複数テーブルへの更新を逐次走らせていた。途中で落ちると一部だけ書き込まれた状態でDBが残る、典型的な事故ポイント。D1 の ",[18,107,108],{},"batch()"," でまとめて投げる形に書き換えた。",[111,112,117],"pre",{"className":113,"code":114,"language":115,"meta":116,"style":116},"language-ts shiki shiki-themes vitesse-light vitesse-light","await db.batch([\n  db.prepare('UPDATE subscriptions SET ...').bind(...),\n  db.prepare('UPDATE users SET ...').bind(...),\n  db.prepare('INSERT INTO webhook_events ...').bind(...),\n])\n","ts","",[18,118,119,143,176,200,224],{"__ignoreMap":116},[120,121,124,128,132,136,140],"span",{"class":122,"line":123},"line",1,[120,125,127],{"class":126},"sHkkW","await",[120,129,131],{"class":130},"s4oTP"," db",[120,133,135],{"class":134},"shFtX",".",[120,137,139],{"class":138},"senZ8","batch",[120,141,142],{"class":134},"([\n",[120,144,146,149,151,154,157,161,165,167,170,173],{"class":122,"line":145},2,[120,147,148],{"class":130},"  db",[120,150,135],{"class":134},[120,152,153],{"class":138},"prepare",[120,155,156],{"class":134},"(",[120,158,160],{"class":159},"sMJiu","'",[120,162,164],{"class":163},"sdGka","UPDATE subscriptions SET ...",[120,166,160],{"class":159},[120,168,169],{"class":134},").",[120,171,172],{"class":138},"bind",[120,174,175],{"class":134},"(...),\n",[120,177,179,181,183,185,187,189,192,194,196,198],{"class":122,"line":178},3,[120,180,148],{"class":130},[120,182,135],{"class":134},[120,184,153],{"class":138},[120,186,156],{"class":134},[120,188,160],{"class":159},[120,190,191],{"class":163},"UPDATE users SET ...",[120,193,160],{"class":159},[120,195,169],{"class":134},[120,197,172],{"class":138},[120,199,175],{"class":134},[120,201,203,205,207,209,211,213,216,218,220,222],{"class":122,"line":202},4,[120,204,148],{"class":130},[120,206,135],{"class":134},[120,208,153],{"class":138},[120,210,156],{"class":134},[120,212,160],{"class":159},[120,214,215],{"class":163},"INSERT INTO webhook_events ...",[120,217,160],{"class":159},[120,219,169],{"class":134},[120,221,172],{"class":138},[120,223,175],{"class":134},[120,225,227],{"class":122,"line":226},5,[120,228,229],{"class":134},"])\n",[14,231,232],{},"「Stripe側でリトライされる前提でも、自分側のDBが整合してないとリトライがさらに事故を呼ぶ」というレビュー時の指摘がそのまま腑に落ちた。",[43,234,236],{"id":235},"purchasegating-の既定値を-true-に","purchaseGating の既定値を true に",[14,238,239,240,244],{},"機能ゲートの設定が「環境変数を読んで、未定義なら false にフォールバック」という実装になっていた。本番で環境変数が剥がれた瞬間、有料機能がすべて開放される構造。",[241,242,243],"strong",{},"未設定 = 安全側（true でゲートをかける）"," に既定値を寄せて、明示的に false を入れない限り解除されないように直した。",[43,246,248],{"id":247},"rollup-を-4571-4604-へ","rollup を 4.57.1 → 4.60.4 へ",[14,250,251,252,255,256,259],{},"依存ツリーの中に残っていた rollup の旧バージョンに CVE が紐づいていた。",[18,253,254],{},"pnpm why rollup"," で経路を確認しつつ、",[18,257,258],{},"pnpm.overrides"," で最新へ寄せた。",[10,261,263],{"id":262},"high-8件で直したもの","High 8件で直したもの",[265,266,267,280],"table",{},[268,269,270],"thead",{},[271,272,273,277],"tr",{},[274,275,276],"th",{},"領域",[274,278,279],{},"内容",[281,282,283,299,307,315,323,335],"tbody",{},[271,284,285,289],{},[286,287,288],"td",{},"Stripe checkout / portal",[286,290,291,294,295,298],{},[18,292,293],{},"origin"," 解決を ",[18,296,297],{},"Origin"," ヘッダ → 環境変数 → デフォルトの順に直し、全体を try/catch で包んでエラーを構造化ログに落とした",[271,300,301,304],{},[286,302,303],{},"/api/admins",[286,305,306],{},"セッション認証だけだったところに、admin ロールの二重チェックを追加",[271,308,309,312],{},[286,310,311],{},"ExcelHtmlViewer",[286,313,314],{},"サーバー側で生成された HTML をクライアント描画する経路に、script/iframe 等を弾く簡易サニタイザを通した",[271,316,317,320],{},[286,318,319],{},"better-auth singleton",[286,321,322],{},"テスト/本番で D1 binding が混ざる懸念があったので、WeakMap で binding ごとにインスタンスを分離",[271,324,325,328],{},[286,326,327],{},"構造化ログ",[286,329,330,331,334],{},"Cloudflare Logs から拾いやすいよう ",[18,332,333],{},"event/level/userId/traceId"," を統一フィールドで出すように整備",[271,336,337,340],{},[286,338,339],{},"/api/health",[286,341,342],{},"DB と KV だけ見ていたところに Stripe API への疎通チェックを追加",[14,344,345],{},"Medium 7件は割愛するが、内部APIのレスポンス型を Zod で固定したり、不要な watch を computed に置き換えたりといった「気持ちのいい掃除」をまとめて入れた。",[10,347,348],{"id":348},"テスト基盤の修復で見えたバグ",[14,350,351,352,355],{},"ここからが本日のハイライト。Critical / High を直し終えたあとで ",[18,353,354],{},"pnpm test:run"," を走らせたら、テストが大量に落ちていた。",[43,357,359],{"id":358},"i18n-プラグインがテスト環境で起動していなかった","i18n プラグインがテスト環境で起動していなかった",[14,361,362,363,366,367,370],{},"落ちていたテストを1本ずつ追ったら、i18n プラグインがテスト環境では nuxt config を読み込めずに ",[18,364,365],{},"undefined.locales"," を参照していた。",[18,368,369],{},"vitest.config.ts"," で nuxt の override を入れて、テスト時は最小構成の i18n だけ立ち上がるようにした。",[43,372,374],{"id":373},"skip-されていた-authtestts-を起こした","skip されていた auth.test.ts を起こした",[14,376,377,380],{},[18,378,379],{},"describe.skip"," がついた認証テストが7件あった。コミットログを辿ると「環境の問題で動かないから一旦 skip」というメモ。今回 better-auth の WeakMap 分離を入れた副作用で、skip を外しても全件パスした。",[14,382,383],{},"結果として:",[51,385,386,389],{},[54,387,388],{},"Test Files: 8 失敗 → 0 失敗",[54,390,391],{},"Tests: 1738 → 1788 全件パス（skip 解除分を含む）",[43,393,395],{"id":394},"section-が違うのに-slug-が同じを発見","「section が違うのに slug が同じ」を発見",[14,397,398,401,402,405],{},[18,399,400],{},"quizTopicsDisplayNumber.test.ts"," が1件落ちていた。期待値と実値を見比べたら、別セクションに属する問題が同じ slug を持っていた。",[241,403,404],{},"実装ではなくテスト側の期待値が現実に追いついていなかった","パターン。テストデータを正して通した。",[10,407,408],{"id":408},"カバレッジ取得で詰まったところ",[14,410,411,414,415,417,418,421,422,425],{},[18,412,413],{},"pnpm test:coverage"," を v8 並列で回したら、hook timeout が再発してプロセスが固まった。以前にも見た症状で、",[18,416,369],{}," で ",[18,419,420],{},"singleThread: true"," に切り替えて回避。並列度を犠牲にする代わりに、",[18,423,424],{},"coverage/index.html"," が確実に生成された。",[14,427,428,431,432,435],{},[18,429,430],{},"app/utils/**"," と ",[18,433,434],{},"app/composables/**"," のカバレッジを眺めて、薄かったところに失敗ケースを足した。100% は目指さず、触った関数のエラーパスだけ埋めた。",[10,437,438],{"id":438},"ビルドエラーの最後の障害物",[14,440,441,442,445,446,449,450,453],{},"最後に ",[18,443,444],{},"pnpm build"," を回したら、Vue の SFC パーサーが意味不明な位置で ",[18,447,448],{},"Unexpected token"," を吐いた。エラー行をクリックして飛んだ先は ",[18,451,452],{},"ExcelHtmlViewer.vue"," のコメント。",[111,455,459],{"className":456,"code":457,"language":458,"meta":116,"style":116},"language-vue shiki shiki-themes vitesse-light vitesse-light","\u003Cscript setup lang=\"ts\">\n// 例: 元HTMLに含まれる \u003C/script> を弾く\n// ↑ このコメント内の閉じタグを Vue パーサーが script 終端と誤認していた\n\u003C/script>\n","vue",[18,460,461,488,494,499],{"__ignoreMap":116},[120,462,463,466,469,472,475,478,481,483,485],{"class":122,"line":123},[120,464,465],{"class":134},"\u003C",[120,467,468],{"class":126},"script",[120,470,471],{"class":130}," setup",[120,473,474],{"class":130}," lang",[120,476,477],{"class":134},"=",[120,479,480],{"class":159},"\"",[120,482,115],{"class":163},[120,484,480],{"class":159},[120,486,487],{"class":134},">\n",[120,489,490],{"class":122,"line":145},[120,491,493],{"class":492},"sxvE3","// 例: 元HTMLに含まれる \u003C/script> を弾く\n",[120,495,496],{"class":122,"line":178},[120,497,498],{"class":492},"// ↑ このコメント内の閉じタグを Vue パーサーが script 終端と誤認していた\n",[120,500,501,504,506],{"class":122,"line":202},[120,502,503],{"class":134},"\u003C/",[120,505,468],{"class":126},[120,507,487],{"class":134},[14,509,510,511,517,518,520],{},"コメント内に書いた ",[241,512,513,516],{},[18,514,515],{},"\u003C/script>"," という文字列を、Vue の SFC パーサーが本物の script 終端タグとして拾ってしまっていた","。コメントを「閉じスクリプトタグ」と書き換えて回避。ローカル ",[18,519,444],{}," が緑になった。",[14,522,523,524,527],{},"ただし Codex のサンドボックス側ではビルドが ",[18,525,526],{},"commonjs--resolver spawn EPERM"," で落ちる。最初は実装の問題かと疑ったが、ローカルの build / dev / test が全て通っていることから、サンドボックス側のプロセス起動権限の問題と切り分けた。実装にはこれ以上手を入れない判断にした。",[10,529,530],{"id":530},"学び",[51,532,533,539,545,551,557],{},[54,534,535,538],{},[241,536,537],{},"派遣する係に徹する",": 4視点のサブエージェント並列も、Codex の優先度レビューも、「自分でレビューする」と思っていた頃の半日仕事が、判断を投げる側に回ったら2時間で終わった。",[54,540,541,544],{},[241,542,543],{},"Codex の優先度突っ込みが効く",": Critical / High の分類は自分一人だと甘くなる。「PERFはCriticalじゃない、本当に事故るのはこれだ」と外から指差してもらえるのが大きかった。",[54,546,547,550],{},[241,548,549],{},"「致命的なものだけ言って」と添える",": Codex はクソリプ気質があるので、最初の一言で「瑣末はいい、致命的だけ」と縛ると一気に使える助言が増える。",[54,552,553,556],{},[241,554,555],{},"skip テストはリファクタの副作用で蘇る",": 環境が変わった時に skip が外せるかを毎回見直すと、過去の自分の妥協を回収できる。",[54,558,559,565,566,569],{},[241,560,561,562,564],{},"コメント内の ",[18,563,515],{}," は地雷",": 仕様としては既知だが、レビューで増えがちな箇所なのでマクロで弾くより ",[18,567,568],{},"&lt;/script&gt;"," と書く運用にした。",[14,571,572,573,576],{},"明日は coverage が薄いままの ",[18,574,575],{},"app/composables/payments/**"," を埋めにいく。今日の Critical / High 修正でテストを足したが、エラーパスがまだスカスカ。",[578,579,580],"style",{},"html pre.shiki code .sHkkW, html code.shiki .sHkkW{--shiki-default:#1E754F;--shiki-dark:#1E754F}html pre.shiki code .s4oTP, html code.shiki .s4oTP{--shiki-default:#B07D48;--shiki-dark:#B07D48}html pre.shiki code .shFtX, html code.shiki .shFtX{--shiki-default:#999999;--shiki-dark:#999999}html pre.shiki code .senZ8, html code.shiki .senZ8{--shiki-default:#59873A;--shiki-dark:#59873A}html pre.shiki code .sMJiu, html code.shiki .sMJiu{--shiki-default:#B5695977;--shiki-dark:#B5695977}html pre.shiki code .sdGka, html code.shiki .sdGka{--shiki-default:#B56959;--shiki-dark:#B56959}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sxvE3, html code.shiki .sxvE3{--shiki-default:#A0ADA0;--shiki-dark:#A0ADA0}",{"title":116,"searchDepth":145,"depth":145,"links":582},[583,584,588,593,594,599,600,601],{"id":12,"depth":145,"text":12},{"id":40,"depth":145,"text":41,"children":585},[586,587],{"id":45,"depth":178,"text":46},{"id":75,"depth":178,"text":76},{"id":97,"depth":145,"text":98,"children":589},[590,591,592],{"id":101,"depth":178,"text":102},{"id":235,"depth":178,"text":236},{"id":247,"depth":178,"text":248},{"id":262,"depth":145,"text":263},{"id":348,"depth":145,"text":348,"children":595},[596,597,598],{"id":358,"depth":178,"text":359},{"id":373,"depth":178,"text":374},{"id":394,"depth":178,"text":395},{"id":408,"depth":145,"text":408},{"id":438,"depth":145,"text":438},{"id":530,"depth":145,"text":530},"dev","構造・セキュリティ・パフォーマンス・SREの4視点でサブエージェントを並列に走らせ、Codexにレビューを3回ループさせてCritical 3件＋High 8件＋Medium 7件を一気に潰したセッションログ。テスト基盤の修復とビルドエラーの最後の障害物まで含めて記録","md",{},true,null,"/code-review-iterative-refactor","eurekapu-nuxt4",false,"2026-05-18T00:00:00.000Z",{"title":5,"description":603},"2026-05/2026-05-18/code-review-iterative-refactor",[615,616,617,618,619],"コードレビュー","Codex","リファクタリング","Vitest","サブエージェント","done","SRnONJ4r3VF8WiMj3MRV2e4bMsjHzxZsYRfKNVBuoyU",[],"https://log.eurekapu.com/og/blog/code-review-iterative-refactor.png?v=2026-05-18T00%3A00%3A00.000Z&title=%E3%82%B3%E3%83%BC%E3%83%89%E3%81%8C%E6%95%A3%E3%82%89%E3%81%8B%E3%81%A3%E3%81%A6%E3%81%8D%E3%81%9F%E3%81%AE%E3%81%A7%E3%82%B5%E3%83%96%E3%82%A8%E3%83%BC%E3%82%B8%E3%82%A7%E3%83%B3%E3%83%88%E4%B8%A6%E5%88%97%E3%83%AC%E3%83%93%E3%83%A5%E3%83%BC%EF%BC%8BCodex%203%E5%9B%9E%E3%83%AB%E3%83%BC%E3%83%97%E3%81%A7%E4%B8%80%E6%B0%97%E3%81%AB%E5%A4%A7%E6%8E%83%E9%99%A4%E3%81%97%E3%81%9F&author=Kei%20Komatsu&sig=2f3e68b1f5d8f7a3",1782528838873]