開発mdx-playground

きっかけ — ビルド時間を測っていたら postgenerate が exit 1 で止まった

measure-deploy.ps1 で Nuxt のビルド & デプロイのフェーズ別タイマーを回していた。pnpm generate がどこに時間を食っているのか、SSG 本体・サイトマップ生成・redirect 生成・dist 検証のどれが伸びているのかを切り分けたかった。

10 分待った末、ターミナルが赤くなった。pnpm generate 自体は通過したが、直後の postgeneratescripts/verify-unpublished-excluded.mjs が exit 1。標準出力に「Leaked links: 9」と並び、ビルド全体を止めた。

Leaked unpublished links found in published HTML: 9 see dist//index.html

時間計測のはずがリグレッション検知になった。

原因 — フラグを立てた当日、内部リンクが本文に残っていた

朝に Phase 1 で unpublished: true を 19 件に拡大した。その中の nvda-bofa-pt-raise-may2026.mdpublishedAt: "2026-05-21" で、同じ日の diary-2026-05-21.md から「BofA が NVIDIA の PT を引き上げ」への Markdown リンクで参照されている。

unpublished: true は「この記事の HTML を生成しない」を意味する。だが日記側の本文は普通の Markdown で、レンダラはリンクを <a href="(非公開記事のパス)"> に変換する。dist の HTML を grep すると、9 本の日記から 12 本の href が、もう生成されていない URL を指していた。

スクリプトはバグっていない。むしろ正しく漏えいを検出してビルドを止めてくれた。壊れていたのは運用側だった。

最初に「unpublished を 9 件外せば楽」と言って、ユーザーに止められた

検出された 9 件のリンク先を見て、最初に思いついた逃げ道は「リンク先 9 件の unpublished: true を外す」だった。フラグを外した瞬間、検証スクリプトは「unpublished 記事ではない URL への href なので関係ない」と判定する。1 行コメントアウトを 9 ファイル分流せば終わる。

ユーザーに提案した。返事は数秒で来た。

待って、unpublished を外したら公開記事になっちゃう。それは意図と違う。

朝に Tier 1〜2 を 19 件に絞り込んだ判断と矛盾する、と気づいた。フラグはそのまま、日記側の Markdown リンクをプレーンテキスト化する方針に戻した。

12 箇所の一括 Edit

Grep で \]\(/(nvda-bofa-pt-raise|amd-openai-...|memory-investment|...)\) を打って、9 本の日記から 12 箇所を全件洗い出した。Edit ツールで [テキスト](/path)テキスト(未公開) の形に書き換えた。同一ファイル内で完全一致するパターンは replace_all: true で一括処理した。

書き換え後にもう 4 件残ったが、これは unpublished 記事同士の相互リンクで、リンク元の HTML 自体が生成されない。検証スクリプトはこの 4 件を無視する。

node scripts/verify-unpublished-excluded.mjs --sources-only
# Unpublished: 25
# Published: 1050
# Leaked links: 0

dist を作らず Markdown ソースだけで判定するオプションを足し、Vitest からも回せる形にした。

Vitest 化 — pnpm generate の 10 分を待たずに拾う

postgenerate でしか落ちないバグ、という構造が気持ち悪い。フラグを立てた瞬間 / 日記を書いた瞬間に拾える経路を作りたい。

app/utils/find-leaked-unpublished-links.ts にロジックを切り出した。ArticleSource[] を受け取り LeakedLink[] を返す純粋関数。frontmatter 抽出、unpublished 判定、リンク抽出を別関数に分け、入出力だけで完結させた。

tests/unpublished-link-leakage.test.ts で実 content/ を流し込むテストを書いた。1075 ファイルを scan して 1 秒で終わる。これで「フラグを立てた次の Vitest 実行で落ちる」流れに変わった。

build に test:run を前置きする案 → revert

ここまで来て「measure-deploy.ps1package.jsonpredeploy:cloudflare チェーンに pnpm test:run を前置きすれば、漏えいリンクを残したまま pnpm generate が走ることは無くなる」と思いついた。10 分後に postgenerate で落ちるのは最悪のフィードバックループだ。前置きすれば 1 秒で落ちる。

組み込んでみた。pnpm test:run が常に赤い。漏えいリンク検出は通る。だが別件で 22/15688 件のテストが落ちていた。連結精算表エンジンが 16 件、OG メタタグが 4 件、URL 移行整合性が 2 件。build まで届かない。

前置きを足しても build が永遠に通らない。measure-deploy.ps1package.json を revert した。「組み込みは諦めて、まず 22 件側を計画書化しよう」と方針を切り替えた。

計画書 — test-failures-recovery-plan.md

memo/2026-05-22/test-failures-recovery-plan.md に失敗 23 件(うち 1 件はあとから判明したタイムアウト含む)の内訳を整理した。あとで memo/2026-05-23/ に移動した。

グループテストファイル失敗数推定原因
Atests/check-sequential-ids.test.ts15 秒タイムアウト超過
Btests/consolidation-engine-consistency.test.ts12連結精算表の最終残高が期待値と乖離
Btests/consolidation-engine.test.ts4連結精算表の貸借一致が崩れている
Ctests/og-meta-tags.test.ts4useSeoMeta が無い / 不足
Dtests/url-migration.test.ts2title 無しが 1 件混入

A/C/D は 30 分以内で消化できそうだったが、Group B が一番重い。連結精算表の最終残高が、追加取得 70→80% パターンで expected 524600 に対して received 1199600、約 2.3 倍ズレている。仕様変更でロジックが動いたのか、fixture/期待値が古いのか、切り分けが必要だった。

Codex 4 ラウンドレビュー

計画書が書けたところで、毎回やっているレビューフロー(~/.claude/rules/plan-codex-review.md)に従って Codex に投げた。

codex exec -m gpt-5.5 "このプランをレビューして。瑣末な点へのクソリプはしないで。致命的な点だけ指摘して: \
  memo/2026-05-22/test-failures-recovery-plan.md (ref: CLAUDE.md)"

ラウンド 1 — 致命的指摘 3 件

Codex の最初のレビューが返ってきた。3 件の致命傷が並んでいた。

  1. Group B-2 の「ブラウザで目視一致を確認するだけでは不十分」。当初の計画書には「dev でブラウザを開き、画面の数字が期待値と一致するか確認する」と書いていた。Codex は「画面も同じエンジンで描画しているので、壊れた実装同士で整合して見える危険がある」と指摘してきた。確かにブラウザの数字が合っても、エンジン側のバグがテスト fixture と同じ向きにズレているだけ、という事故が起きうる。独立検算ベース(手計算 / スプレッドシート)で「真の期待値」を出す手順に書き直した。
  2. consolidation-engine.ts のパスが composables/ ではなく app/utils/ 配下。計画書が誤ったパスを書いていた。Grep で app/composables/consolidation-engine.ts を探しても見つからず、app/utils/consolidation-engine.ts だけが存在する。
  3. pnpm test:run を入れる位置が間違っているmeasure-deploy.ps1 の repo root のままで叩くと package.jsontest:run スクリプトが無く Missing script で失敗する。Push-Location -LiteralPath $webRoot直後 に置くか、pnpm --dir $webRoot test:run で明示する。

ラウンド 2 — パス修正と過去 commit 特定方法の補強

ラウンド 1 を反映したあと、resume で 2 回目を回した。

codex exec resume --last -m gpt-5.5 "プランを更新したからレビューして。瑣末な点へのクソリプはしないで。致命的な点だけ指摘して: \
  memo/2026-05-22/test-failures-recovery-plan.md"

Codex は「過去に pass していた commit を特定する方法が git log -- tests/... だけでは不十分」と指摘してきた。テストファイルが変わっていないリグレッションは log では拾えない。エンジン本体の変更履歴も追う必要がある、と。git log -p --since="2026-04-01" -- apps/web/app/utils/consolidation-engine.ts apps/web/tests/consolidation-engine* の形に直した。

ラウンド 3 — git checkout を git worktree に直す

ラウンド 3 で Codex が一番大きい指摘を投げてきた。

過去 commit に戻して二分探索するなら、現在の作業ツリーで git checkout <sha> するのは危険。未コミット変更や進行中の修正が混線する。git worktree add ../mdx-playground-bisect <sha> で別ディレクトリに展開して、そっちで pnpm install / test:run を回せ。

完全に正しい。当初の計画書には「git checkout <sha> で過去に戻して」と書いていたが、いま走らせている修正と二分探索が同じ作業ツリーを取り合う。worktree なら本線をいじらず、別ディレクトリで完全に独立した state を持てる。書き換えた。

# 【cwd: repo root (mdx-playground/)】から実行
git worktree add ../mdx-playground-bisect <commit-sha>
pnpm --dir ../mdx-playground-bisect/apps/web install
pnpm --dir ../mdx-playground-bisect/apps/web test:run tests/consolidation-engine*
# 終わったら git worktree remove ../mdx-playground-bisect

ラウンド 4 — コマンドの実行ディレクトリを明記

ラウンド 4。Codex は「コマンドの cwd を明記してほしい」と返してきた。「apps/web/ から打つと ../mdx-playground-bisect が repo 外ではなく apps/ 配下に作られてしまう。git log のパスも apps/web/ から打つと pathspec がズレる」。

計画書の Bash ブロックすべてに # 【cwd: repo root (mdx-playground/)】から実行する のコメントを足した。再度 resume で投げると、Codex から「致命的な指摘なし」が返ってきた。

4 ラウンドで終わった。最初のラウンドで「ブラウザ目視で確認」と書いていた箇所は、Codex の独立検算ベースの指摘で完全に潰れた。同じエンジンで描いた画面を信用して fixture を作り直す事故を未然に防げた、と思う。

Google タスクに積み残し登録

add-task スキルで「明日 23 件のテスト失敗を消化する」を Google タスクに登録した。スキル実行中に小さい不具合を踏んだ。スキル本体が /tmp/<temp_file> を作って Python (Windows) に渡そうとして、Git Bash 側の /tmp と Python 側の tempfile.gettempdir() の解決パスがズレていた。mktemp ベースに直し、Python 側でも Path() で受けるようにしてもらった。

今日の学び

pnpm generate の最後に postgenerate で漏えい検出が走るのは正しい設計だが、10 分待ってから落ちるのはフィードバックが遅すぎる。検証スクリプトを Vitest 化して 1 秒で落ちる経路を増やしたのは正解だった。

「unpublished を外せば楽」と提案してユーザーに止められた瞬間、フラグの意味そのものを忘れかけていた。AI に書かせた一括 Edit でも、判断(フラグを外すか / リンク側を直すか)は人間が握る。

Codex のレビューが 4 ラウンドかかったが、特にラウンド 3 の git worktree 指摘は自分だけでは絶対に出てこなかった。二分探索を git checkout で本線ツリー上でやっていたら、進行中の Phase 1 修正と混ざって週末に取り返しがつかない事故を起こしていた可能性がある。