prebuild自動正規化で検証順序の失敗を防ぐ方法
この記事で学べること
- ビルド失敗に見える問題を正規化と検証順序へ分けて調べる方法
- 再生成される値を個別修正せず、prebuild前に自動正規化する理由
- 自動修復と検証をハーネス(AIに渡すルール、作業手順、検証方法をまとめた仕組み)へ固定した後の確認フロー
検証前の自動正規化でビルド失敗を減らす
不整合を一度直せば次のビルドも通ると考えていましたが、生成値やメタ情報が更新されるたびに同じ問題が戻りました。原因は値だけでなく、正規化より先に検証していた処理順序にありました。機械的に直せる差分を検証前にそろえ、その順序自体を確認することで再発を減らせます。
この記事では、「再発する不整合を個別修正から検証前の自動正規化へどう移すか」という問いに焦点を当て、期待と結果の差、確認できた条件、再発を防ぐ判断を扱います。
最後まで読むと、「再発する不整合を個別修正から検証前の自動正規化へどう移すか」という問いを、自分の状況に照らして判断するための材料を持ち帰れます。
learning_timeは検証前に自動正規化する
この記事では、Astro / Starlightで運用している学習サイトで、ビルド失敗を再発しにくくするために実装した prebuild(本番ビルドの前に正規化や検証を実行する前処理) の自動正規化について整理します。
根本原因は、learning_time を人が手入力していたことではありませんでした。実際の問題は、ビルド前の検証パイプラインが「正規化済みの状態」を要求していた一方で、その状態に整える処理が検証より前に必ず実行される設計になっていなかったことです。
learning_timeの正規化不足がビルド失敗に見えていた
表面上は、Vercelのビルド失敗やAstroのビルド失敗に見えました。しかし、調べるとビルド本体より前の prebuild 段階で止まっているケースがありました。
このリポジトリでは、npm run build の前に prebuild が実行されます。prebuild では、slug(記事の公開URLや内部リンクで使う短い識別子)の補完、リンク検証、Mermaid(Markdown内で図やフローチャートを書くための記法)検証、記事レビュー、harness検証などを通して、公開前に壊れた状態を止めるようにしています。
その中に、docs記事の learning_time を検証する処理があります。learning_time は記事の本文量から機械的に算出できる値です。値が本文とずれている場合、ビルド前に検出できます。
ここまでは妥当な設計です。問題は、検出だけが先にあり、修正可能な差分をビルド前に自動で整える流れが不十分だったことでした。
根本原因は検証順序の設計だった
問題の中心は、normalize-learning-time.mjs --check が「正しい状態かどうか」を確認するのに対し、--write が「正しい状態へ更新する」処理である点です。
ビルド前のパイプラインが --check だけを実行すると、本文量の変化や既存記事の差分によって learning_time が正規化前の状態になったとき、その場でビルドが止まります。
これは「人が値を間違えたから失敗した」というより、機械的に直せる差分を、検証前に機械的に直す契約になっていなかったことが原因です。
そのため、対策は記事ごとの値を直すことではなく、dev:check と prebuild の両方で次の順序を保証することにしました。
"dev:check": "node scripts/normalize-learning-time.mjs --write && node scripts/normalize-learning-time.mjs --check && ..."実際の package.json では、slug補完やローカライズ同期などの既存処理の後に --write を置き、その直後に --check を実行しています。これにより、正規化できる差分は先に更新され、残った問題だけが検証エラーとして扱われます。
prebuildに自動修復を入れる理由
prebuild に自動正規化を入れる理由は、ビルド失敗の直前で毎回同じ種類の差分を手作業で直さないためです。
このサイトでは、記事本文、frontmatter(Markdown記事の先頭に置くtitle、description、dateなどの設定情報)、内部リンク、Mermaid図、日英ペアなど、Markdownコンテンツに由来する検証項目が複数あります。これらはビルド時に初めて表面化すると、原因がAstro、Vercel、記事本文、生成スクリプトのどこにあるのか分かりにくくなります。
そこで、ビルドの直前ではなく、日常的に実行する dev:check と本番ビルド前の prebuild に同じ契約を持たせました。
- 機械的に直せるものは、検証前に自動で整える
- 自動で直せないものだけを、検証エラーとして止める
- ローカル確認と本番ビルド前の確認で、同じ順序を使う
この設計にすると、ビルド失敗の原因を「最終工程で見つかった不明なエラー」ではなく、「どの検証契約に違反したか」として扱いやすくなります。
再発防止をharnessで固定する
一度 package.json を直しても、後からスクリプトの順序が変わると同じ問題が戻る可能性があります。そのため、scripts/validate-harness.mjs でも dev:check と prebuild の順序を検証するようにしました。
具体的には、両方のコマンドに次の条件を求めています。
node scripts/normalize-learning-time.mjs --writeが含まれているnode scripts/normalize-learning-time.mjs --checkが含まれている--writeが--checkより前にある
これにより、単に現在のビルドを通すだけでなく、将来の変更で同じ設計が崩れたときにも harness:check で検出できます。
実装後はdev:checkを先に実行し、必要時だけビルドする
実装後は、いきなり npm run build を実行するのではなく、まず npm run dev:check でprebuild相当の失敗要因を確認します。
このリポジトリでは npm run build を、承認制のローカル成果物生成コマンドとして扱っています。Vercel側のデプロイ状態は、別のホスティング側シグナルとして確認します。普段の確認では、dev:check を先に通すことで、リンク、slug、Mermaid、レビュー、harnessの問題をまとめて確認できます。
ビルドが必要な段階では、次のように分けて見ます。
dev:checkが通るか- 承認後に
npm run buildが通るか - Vercel側のデプロイステータスが成功しているか
この3つを分けることで、ローカルの検証失敗、ビルド失敗、リモートのデプロイ待ちを混同しにくくなります。
まとめ:正規化してから検証する順序をパイプラインの契約にする
今回の対策で重要だったのは、learning_time の値そのものではなく、ビルド前にリポジトリをどの状態へ整えるかをパイプラインの契約として明確にしたことです。
機械的に正規化できる差分は prebuild の前半で整え、その後に検証する。さらに、その順序自体をharnessで検証する。これにより、同じ種類のビルド失敗を個別修正として扱うのではなく、再発しにくい実装として管理できるようになりました。
最初の行動は、直近の失敗が「機械的に直せる値のずれ」か「人が判断すべき内容の誤り」かを分けることです。後者まで自動正規化すると誤りを隠すため、前処理へ移すのは決定的な規則で同じ結果に直せる差分だけに限定します。